Skip to main content

RDataFrameBuilder

Struct RDataFrameBuilder 

Source
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*4 chunks → 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

  1. 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.
  2. Fill all columns (parallel flat pass with rayon, serial otherwise). No R API calls happen inside the parallel region.
  3. Set character CHARSXPs serially on the R thread (CHARSXP allocation is forbidden on rayon threads), then assemble the VECSXP, names, compact row.names (c(NA_integer_, -nrow)), and class = "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-row Option<String> values are computed during the fill pass, but the CHARSXPs are set serially afterward. None becomes NA_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 chunks nrow into 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

Source

pub fn new(nrow: usize) -> Self

Start building a data.frame with nrow rows.

Source

pub fn column<T>( self, name: impl Into<String>, f: impl Fn(&mut [T], usize) + Send + Sync + 'static, ) -> Self
where T: RNativeType + Send + Sync,

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.

Source

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).

Source

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.

Source

fn build_sexp(self) -> SEXP

Assemble and return the raw VECSXP SEXP (internal; prefer build).

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> SizeHint for T
where T: ?Sized,

Source§

default fn lower_bound(&self) -> usize

🔬This is a nightly-only experimental API. (core_io_internals)
Returns a lower bound on the number of elements this container-like item contains. For example, an array [u8; 12] could return any value between 0 and 12 inclusively as a correct implementation. Read more
Source§

default fn upper_bound(&self) -> Option<usize>

🔬This is a nightly-only experimental API. (core_io_internals)
Returns an upper bound on the number of elements this container-like item contains if it can be determined, otherwise None. Read more
Source§

final fn size_hint(&self) -> (usize, Option<usize>)

🔬This is a nightly-only experimental API. (core_io_internals)
Returns an estimate for the number of elements this container like type contains. Read more
Source§

impl<T> SizedTypeProperties for T

Source§

#[doc(hidden)]
const SIZE: usize = _

🔬This is a nightly-only experimental API. (sized_type_properties)
Source§

#[doc(hidden)]
const ALIGN: usize = _

🔬This is a nightly-only experimental API. (sized_type_properties)
Source§

#[doc(hidden)]
const ALIGNMENT: Alignment = _

🔬This is a nightly-only experimental API. (ptr_alignment_type)
Source§

#[doc(hidden)]
const IS_ZST: bool = _

🔬This is a nightly-only experimental API. (sized_type_properties)
true if this type requires no storage. false if its size is greater than zero. Read more
Source§

#[doc(hidden)]
const LAYOUT: Layout = _

🔬This is a nightly-only experimental API. (sized_type_properties)
Source§

#[doc(hidden)]
const MAX_SLICE_LEN: usize = _

🔬This is a nightly-only experimental API. (sized_type_properties)
The largest safe length for a [Self]. Read more
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.

Layout§

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