pub struct RDataFrameBuilder {
nrow: usize,
names: Vec<String>,
columns: Vec<ColumnReg>,
}Expand description
Builder for assembling an R data.frame from per-column fill closures.
This is the heterogeneous-column analogue of with_r_matrix: instead of one
homogeneous matrix, you declare a set of typed columns (each with its own
element type and fill closure) and the builder fills them all, then assembles
the data.frame.
§Fill strategy (parallel with rayon, serial otherwise)
The builder is available regardless of the rayon feature; only the fill
pass differs (see the module docs). The rest of
this section describes the parallel pass that rayon enables.
§Two axes of parallelism, one work-stealing pass
There are two ways to parallelise a column fill:
- Column-granular — one task per column. Fan-out width equals the column count, so a 3-column × 10M-row frame only ever uses 3 threads.
- Row-slice-granular — split one column into contiguous row ranges. Great for one long column, but on its own it serialises across columns.
RDataFrameBuilder does not choose. With rayon,
build flattens the entire job into a single
work-list of (column_index, row-range) items — each native/character column
is split into chunk_size = max(1, nrow / (current_num_threads() * 4))-row
chunks (with 4× oversubscription) — then runs one par_iter over that
flat list. Rayon’s work-stealing balances both axes automatically:
- wide (100 cols × short) → ~100+ items, column-dominated.
- tall (3 cols × 10M rows) → each column shatters into
~nthreads*4chunks → hundreds of items, saturated even with 3 columns. - skewed (1 huge col + many tiny) → the huge column’s chunks get stolen by threads idle after finishing the tiny columns.
This also avoids the per-column barrier and repeated pool spin-up that the
naive “fill each column, each internally parallel” (nested par_iter) shape
would cause. Without rayon, each column is filled in one full-range pass.
§Phases
- Allocate each column’s backing storage serially on the R/worker thread
(native columns get a protected R vector; character columns get an owned
Vec<Option<String>>). Strict PROTECT discipline — the dangerous part. - Fill all columns (parallel flat pass with
rayon, serial otherwise). No R API calls happen inside the parallel region. - Set character
CHARSXPs serially on the R thread (CHARSXP allocation is forbidden on rayon threads), then assemble theVECSXP,names, compactrow.names(c(NA_integer_, -nrow)), andclass = "data.frame".
§Column kinds
column::<T>— a native-typed column (f64/i32/RLogical/u8/Rcomplex). The fill closure receives a mutable chunk and its offset. The buffer is R memory, filled directly with zero intermediate allocation.column_str— a character (STRSXP) column. The per-rowOption<String>values are computed during the fill pass, but theCHARSXPs are set serially afterward.NonebecomesNA_character_.
§Example
use miniextendr_api::dataframe_builder::RDataFrameBuilder;
let df = RDataFrameBuilder::new(1000)
.column::<f64>("x", |chunk, offset| {
for (i, slot) in chunk.iter_mut().enumerate() {
*slot = ((offset + i) as f64).sqrt();
}
})
.column::<i32>("y", |chunk, offset| {
for (i, slot) in chunk.iter_mut().enumerate() {
*slot = (offset + i) as i32;
}
})
.column_str("label", |i| Some(format!("row_{i}")))
.build();§Safety argument (disjoint mutation, no aliasing)
Neither fill path ever produces two items that overlap:
- Different columns address different backing buffers (distinct R vectors
/ distinct
Vecs), so cross-column items are trivially disjoint. - Within a column, the row ranges are a partition of
[0, nrow). The serial path uses the single full range; the parallel path chunksnrowinto fixed-size, non-overlapping spans. Each(offset, len)item therefore owns a unique slice of that column’s buffer.
Each RangeFiller reconstitutes its slice via
slice::from_raw_parts_mut(base.add(offset), len) and writes only that span.
Because the spans are disjoint, no two threads ever form overlapping &mut
references — there is no aliasing UB even though the work-list shares the raw
base pointers (ColPtr, Send + Sync).
§Protection
Every native column SEXP is PROTECTed from allocation through insertion into
the VECSXP; the names / row.names / class transients are likewise
protected across each subsequent allocation. After
build returns, the resulting data.frame SEXP is
unprotected and becomes the caller’s responsibility (return it from a
#[miniextendr] fn, or PROTECT it).
Fields§
§nrow: usize§names: Vec<String>§columns: Vec<ColumnReg>Implementations§
Source§impl RDataFrameBuilder
impl RDataFrameBuilder
Sourcepub fn column<T>(
self,
name: impl Into<String>,
f: impl Fn(&mut [T], usize) + Send + Sync + 'static,
) -> Self
pub fn column<T>( self, name: impl Into<String>, f: impl Fn(&mut [T], usize) + Send + Sync + 'static, ) -> Self
Add a native-typed column (f64/i32/RLogical/u8/Rcomplex).
The fill closure f(chunk, offset) is dispatched over chunks of the
(already-allocated) R column buffer — in parallel with rayon, or in one
full-range pass otherwise. Chunk boundaries are deterministic for a given
nrow and thread count.
Sourcepub fn column_str(
self,
name: impl Into<String>,
f: impl Fn(usize) -> Option<String> + Send + Sync + 'static,
) -> Self
pub fn column_str( self, name: impl Into<String>, f: impl Fn(usize) -> Option<String> + Send + Sync + 'static, ) -> Self
Add a character (STRSXP) column.
The fill closure f(i) returns the value for row i as Option<String>,
where None maps to NA_character_. Values are computed during the fill
pass (parallel with rayon, serial otherwise), then set into the R
STRSXP serially on the R thread (CHARSXP allocation cannot happen on
rayon threads).
Sourcepub fn build(self) -> DataFrame
pub fn build(self) -> DataFrame
Allocate, fill, and assemble the DataFrame.
With rayon, flattens every column into a single (column_index, row-range) work-list and runs one parallel pass over it (see the
type-level docs for the scheduling argument); without rayon, fills each
column serially. Then assembles the data.frame on the R thread.
Sourcefn build_sexp(self) -> SEXP
fn build_sexp(self) -> SEXP
Assemble and return the raw VECSXP SEXP (internal; prefer build).
Auto Trait Implementations§
impl !RefUnwindSafe for RDataFrameBuilder
impl !Sync for RDataFrameBuilder
impl !UnwindSafe for RDataFrameBuilder
impl Freeze for RDataFrameBuilder
impl Send for RDataFrameBuilder
impl Unpin for RDataFrameBuilder
impl UnsafeUnpin for RDataFrameBuilder
Blanket Implementations§
Source§impl<T> BorrowMut<T> for Twhere
T: ?Sized,
impl<T> BorrowMut<T> for Twhere
T: ?Sized,
Source§fn borrow_mut(&mut self) -> &mut T
fn borrow_mut(&mut self) -> &mut T
Source§impl<T> SizeHint for Twhere
T: ?Sized,
impl<T> SizeHint for Twhere
T: ?Sized,
Source§default fn lower_bound(&self) -> usize
default fn lower_bound(&self) -> usize
core_io_internals)[u8; 12] could return any value between 0 and
12 inclusively as a correct implementation. Read moreSource§impl<T> SizedTypeProperties for T
impl<T> SizedTypeProperties for T
Source§#[doc(hidden)]const SIZE: usize = _
#[doc(hidden)]const SIZE: usize = _
sized_type_properties)Source§#[doc(hidden)]const ALIGN: usize = _
#[doc(hidden)]const ALIGN: usize = _
sized_type_properties)Source§#[doc(hidden)]const ALIGNMENT: Alignment = _
#[doc(hidden)]const ALIGNMENT: Alignment = _
ptr_alignment_type)Source§#[doc(hidden)]const IS_ZST: bool = _
#[doc(hidden)]const IS_ZST: bool = _
sized_type_properties)Source§#[doc(hidden)]const LAYOUT: Layout = _
#[doc(hidden)]const LAYOUT: Layout = _
sized_type_properties)Source§#[doc(hidden)]const MAX_SLICE_LEN: usize = _
#[doc(hidden)]const MAX_SLICE_LEN: usize = _
sized_type_properties)[Self]. Read moreLayout§
Note: Most layout information is completely unstable and may even differ between compilations. The only exception is types with certain repr(...) attributes. Please see the Rust Reference's “Type Layout” chapter for details on type layout guarantees.
Size: 56 bytes