Compile-time dimensioned wrappers for R arrays, matrices, and vectors.

🔗Overview

RArray<T, NDIM> wraps an R array SEXP with the dimension count tracked at compile time via const generics. It provides safe, bounds-checked access to R’s column-major data with zero overhead (the wrapper is #[repr(transparent)] over a single SEXP).

🔗Table of Contents

🔗Type Aliases

AliasTypeR Equivalent
RVector<T>RArray<T, 1>vector (with dim attribute)
RMatrix<T>RArray<T, 2>matrix
RArray3D<T>RArray<T, 3>array(..., dim=c(a,b,c))

Supported element types (T) are those implementing RNativeType:

Rust TypeR SEXP Type
f64REALSXP
i32INTSXP
u8RAWSXP
RLogicalLGLSXP
RcomplexCPLXSXP

🔗Quick Start

use miniextendr_api::rarray::RMatrix;

// RMatrix parameters require main_thread (RArray is !Send)
#[miniextendr(unsafe(main_thread))]
pub fn matrix_sum(m: RMatrix<f64>) -> f64 {
    unsafe { m.as_slice().iter().sum() }
}

#[miniextendr(unsafe(main_thread))]
pub fn column_means(m: RMatrix<f64>) -> Vec<f64> {
    let nrow = unsafe { m.nrow() };
    let ncol = unsafe { m.ncol() };
    (0..ncol)
        .map(|col| {
            let sum: f64 = unsafe { m.column(col) }.iter().sum();
            sum / nrow as f64
        })
        .collect()
}

From R:

m <- matrix(1:12, nrow = 3, ncol = 4)
matrix_sum(m)
#> [1] 78

column_means(m)
#> [1] 2 5 8 11

🔗Thread Safety

RArray is !Send and !Sync. It cannot be transferred to or accessed from other threads because the underlying R APIs (DATAPTR_RO, etc.) must be called on the R main thread.

Functions that accept RArray, RMatrix, or RVector parameters must use #[miniextendr(unsafe(main_thread))]:

#[miniextendr(unsafe(main_thread))]
pub fn process(m: RMatrix<f64>) -> f64 {
    // Runs on main thread -- RMatrix access is safe
    unsafe { m.as_slice().iter().sum() }
}

To use the data in worker threads or parallel code, copy it first with to_vec():

#[miniextendr(unsafe(main_thread))]
pub fn parallel_process(m: RMatrix<f64>) -> f64 {
    // Copy on main thread -- Vec<f64> is Send
    let data: Vec<f64> = unsafe { m.to_vec() };
    // data can now be passed to rayon, worker threads, etc.
    data.iter().sum()
}

🔗Memory Layout

R arrays are stored in column-major (Fortran) order. For a 3x4 matrix:

Logical layout:           Memory layout (contiguous):
      col0 col1 col2 col3
row0 [ 0    3    6    9 ]   [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]
row1 [ 1    4    7   10 ]    ^col0^  ^col1^  ^col2^  ^--col3--^
row2 [ 2    5    8   11 ]

Columns are contiguous in memory. The column() method returns a proper slice with no striding. Rows are NOT contiguous – row access requires stepping through memory.

🔗Reading Data

🔗Slice Access (Fastest)

// Full buffer as a flat slice (column-major order)
let slice: &[f64] = unsafe { m.as_slice() };

// Iterate all elements
let sum: f64 = unsafe { m.as_slice().iter().sum() };

🔗Column Access (Fast, Matrix Only)

let ncol = unsafe { m.ncol() };
for col in 0..ncol {
    let col_data: &[f64] = unsafe { m.column(col) };
    // col_data is a contiguous slice of nrow elements
}

🔗Element Access (Slower)

// By N-dimensional indices (any NDIM)
let val: f64 = unsafe { m.get([row, col]) };

// By row/col (matrix convenience)
let val: f64 = unsafe { m.get_rc(row, col) };

🔗Dimensions

// All dimensions as array
let dims: [usize; 2] = unsafe { m.dims() };

// Individual dimension
let nrow = unsafe { m.dim(0) };
let ncol = unsafe { m.dim(1) };

// Matrix-specific helpers
let nrow = unsafe { m.nrow() };
let ncol = unsafe { m.ncol() };

// Total elements
let len = m.len();
let empty = m.is_empty();

🔗Copy to Vec

// Copy data to owned Vec (makes it Send)
let data: Vec<f64> = unsafe { m.to_vec() };

🔗Creating Arrays

🔗Allocate with Initializer

use miniextendr_api::rarray::{RMatrix, RArray3D};

// Matrix: 3 rows x 4 columns, initialized via closure
let matrix = unsafe {
    RMatrix::<f64>::new([3, 4], |slice| {
        for (i, v) in slice.iter_mut().enumerate() {
            *v = i as f64;
        }
    })
};

// 3D array: 2 x 3 x 4
let array = unsafe {
    RArray3D::<f64>::new([2, 3, 4], |slice| {
        slice.fill(1.0);
    })
};

🔗Allocate with Zeros

let matrix = unsafe { RMatrix::<f64>::zeros([100, 50]) };

🔗Important: GC Protection

RArray::new() and RArray::zeros() return an unprotected SEXP. The caller must protect it if any further R allocations occur before returning:

let scope = ProtectScope::new();
let matrix = unsafe { RMatrix::<f64>::new([3, 4], |s| s.fill(0.0)) };
let protected = scope.protect(matrix.as_sexp());

🔗Mutation

🔗Mutable Slice

let mut m: RMatrix<f64> = /* ... */;
let slice: &mut [f64] = unsafe { m.as_slice_mut() };
slice[0] = 42.0;

🔗Element-Wise Set

// By N-dimensional indices
unsafe { m.set([row, col], 42.0) };

// Matrix convenience
unsafe { m.set_rc(row, col, 42.0) };

🔗Mutable Column

let col_data: &mut [f64] = unsafe { m.column_mut(col) };
col_data.fill(1.0);

🔗Coerced Types

RArray supports non-native Rust types via coercion from the underlying R type. These wrap the source SEXP directly (zero-copy for construction), but as_slice() is not available – use to_vec_coerced() instead.

Target TypeSource R TypeCoercion
i8, i16, i64, isizeINTSXP (i32)Integer narrowing/widening
u16, u32, u64, usizeINTSXP (i32)Integer unsigned conversion
f32REALSXP (f64)Float narrowing
boolLGLSXP (RLogical)Logical conversion
#[miniextendr(unsafe(main_thread))]
pub fn process_bool_matrix(m: RMatrix<bool>) -> Vec<bool> {
    unsafe { m.to_vec_coerced() }
}

Coercion validation happens at construction time (TryFromSexp). If any element cannot be coerced (e.g., value out of range for narrowing), construction fails with an error returned to R.

🔗Attributes

RArray provides access to standard R attributes:

🔗Getters

let names: Option<SEXP> = unsafe { m.get_names() };
let class: Option<SEXP> = unsafe { m.get_class() };
let dimnames: Option<SEXP> = unsafe { m.get_dimnames() };
let rownames: Option<SEXP> = unsafe { m.get_rownames() };
let colnames: Option<SEXP> = unsafe { m.get_colnames() };

🔗Setters

unsafe { m.set_names(names_sexp) };
unsafe { m.set_class(class_sexp) };
unsafe { m.set_dimnames(dimnames_sexp) };

🔗Performance

🔗Access Method Comparison

MethodSpeedUse Case
as_slice()FastestFull-buffer iteration, SIMD
column()FastPer-column operations (matrices)
column_mut()FastPer-column mutation
get() / get_rc()SlowerSingle-element random access

Per-element methods like get() perform index translation and bounds checks on every call. For tight loops, this overhead dominates.

🔗Prefer Columns Over Rows

Columns are contiguous in R’s column-major layout. Column iteration is a straight memory scan; row iteration requires striding across columns.

// Fast: column-wise (contiguous memory)
for col in 0..ncol {
    for val in unsafe { m.column(col) } {
        // sequential memory access
    }
}

// Slow: row-wise (strided memory)
for row in 0..nrow {
    for col in 0..ncol {
        let val = unsafe { m.get_rc(row, col) };
        // jumping across columns in memory
    }
}

🔗Prefer Slice Iteration

For operations over all elements, use as_slice() instead of nested indexing:

// Fast
let sum: f64 = unsafe { m.as_slice() }.iter().sum();

// Slow
let mut sum = 0.0;
for row in 0..nrow {
    for col in 0..ncol {
        sum += unsafe { m.get_rc(row, col) };
    }
}

🔗See Also