Extending miniextendr
This guide explains how to extend miniextendr with custom types, enabling them to be passed between Rust and R.
This guide explains how to extend miniextendr with custom types, enabling them to be passed between Rust and R.
πQuick Start
To make your type work with miniextendr, you have two main options:
- Implement
RNativeType- For types with the same memory layout as Rβs native types - Implement
TryFromSexp/IntoRdirectly - For types requiring custom conversion logic
πOption 1: RNativeType (Recommended)
If your type has the same memory layout as i32, f64, u8, RLogical, or
Rcomplex, implement RNativeType to participate in the blanket scalar,
slice, vector, and collection conversions.
πExample: Newtype Wrapper
use miniextendr_api::{RNativeType, SEXP, SEXPTYPE, SexpExt};
use miniextendr_api::altrep_traits::NA_REAL;
/// A temperature in Celsius, stored as f64
#[repr(transparent)]
pub struct Celsius(pub f64);
impl RNativeType for Celsius {
const SEXP_TYPE: SEXPTYPE = SEXPTYPE::REALSXP;
const R_NA: Self = Self(NA_REAL);
unsafe fn dataptr_mut(sexp: SEXP) -> *mut Self {
// Safe because Celsius is repr(transparent) over f64
unsafe { miniextendr_api::sys::REAL(sexp).cast::<Self>() }
}
fn elt(sexp: SEXP, i: isize) -> Self {
Self(sexp.real_elt(i))
}
}πWhat You Get Automatically
With just that impl, these all work:
// Scalar
fn get_temp() -> Celsius { ... }
fn set_temp(t: Celsius) { ... }
// Vectors
fn get_temps() -> Vec<Celsius> { ... }
fn process_temps(temps: &[Celsius]) { ... }
// Collections
fn temp_map() -> HashMap<String, Celsius> { ... }
fn temp_deque() -> VecDeque<Celsius> { ... }
// Optional integrations (with features enabled)
fn temp_tinyvec() -> TinyVec<[Celsius; 8]> { ... }
fn temp_nalgebra() -> DVector<Celsius> { ... }
fn temp_ndarray() -> Array1<Celsius> { ... }
// All Option<> variants
fn maybe_temp() -> Option<Celsius> { ... }
fn maybe_temps() -> Option<Vec<Celsius>> { ... }πRequirements for RNativeType
Your type must:
- Be
#[repr(transparent)]over an R-native element representation - Implement
Copy(required by the trait bound) - Be
'static(no borrowed data) - Define
R_NAfor missing sparse/padded slots and implement bothdataptr_mutand ALTREP-awareelt
πMemory Layout Correspondence
| Rust Type | R Type | SEXPTYPE |
|---|---|---|
i32 | integer | INTSXP |
f64 | numeric | REALSXP |
u8 | raw | RAWSXP |
RLogical | logical | LGLSXP |
Rcomplex | complex | CPLXSXP |
Cannot be RNativeType: i8, i16, f32, i64, String - no matching R storage type.
πOption 2: Direct TryFromSexp/IntoR Implementation
For types that donβt match Rβs memory layout, implement the conversion traits directly.
πExample: Custom String Type
use miniextendr_api::sys::SEXP;
use miniextendr_api::from_r::{SexpError, TryFromSexp};
use miniextendr_api::into_r::IntoR;
pub struct Username(String);
impl TryFromSexp for Username {
type Error = SexpError;
fn try_from_sexp(sexp: SEXP) -> Result<Self, Self::Error> {
let s: String = TryFromSexp::try_from_sexp(sexp)?;
if s.is_empty() {
return Err(SexpError::InvalidValue("username cannot be empty".into()));
}
Ok(Username(s))
}
}
impl IntoR for Username {
type Error = std::convert::Infallible;
fn try_into_sexp(self) -> Result<SEXP, Self::Error> {
Ok(self.0.into_sexp())
}
}
try_into_sexp and the associated Error are the required IntoR surface;
the panicking and unchecked convenience methods have defaults. Likewise, omit
try_from_sexp_unchecked unless your type has a real checked-wrapper bypass.
For string-parsed scalar families, the exported
try_from_sexp_via_str_parse! macro generates T, Option<T>, Vec<T>, and
Vec<Option<T>> implementations and batches every vector parse failure into
one diagnostic:
pub struct Slug(String);
miniextendr_api::try_from_sexp_via_str_parse!(Slug, "slug", |s| {
(!s.is_empty())
.then(|| Slug(s.to_owned()))
.ok_or("slug cannot be empty")
});πWhen to Use Direct Implementation
- Type requires validation (like
Usernameabove) - Type stores borrowed data
- Conversion involves complex transformation
- Type maps to R list or other complex structure
πAdding Coercion Support
If your type can be losslessly converted to/from Rβs numeric types, implement the marker traits:
use miniextendr_api::markers::{WidensToF64, WidensToI32};
use miniextendr_api::coerce::Coerce;
// If Celsius can be losslessly widened to f64
impl WidensToF64 for Celsius {}
// Now this works:
impl From<Celsius> for f64 {
fn from(c: Celsius) -> f64 { c.0 }
}
// And you get automatic coercion:
// Vec<Celsius>.coerce() -> Vec<f64>πAvailable Marker Traits
| Trait | Meaning | Use When |
|---|---|---|
WidensToI32 | Losslessly converts to i32 | 8/16-bit signed integers |
WidensToF64 | Losslessly converts to f64 | Any numeric that fits in f64 |
πWorking with ExternalPtr
For complex types that shouldnβt be copied to R, use ExternalPtr:
use miniextendr_api::ExternalPtr;
#[derive(ExternalPtr)]
pub struct LargeDataset {
data: Vec<f64>,
metadata: HashMap<String, String>,
}
// Now you can pass it by reference:
#[miniextendr]
fn create_dataset() -> ExternalPtr<LargeDataset> {
ExternalPtr::new(LargeDataset { ... })
}
#[miniextendr]
fn process_dataset(data: &LargeDataset) -> f64 {
data.data.iter().sum()
}πWhen to Use ExternalPtr
- Large data structures (avoid copying)
- Mutable state between R calls
- Types that donβt have R equivalents
- Opaque handles to Rust resources
πComplete Example: Custom Numeric Type
use miniextendr_api::{RNativeType, SEXP, SEXPTYPE, SexpExt};
use miniextendr_api::altrep_traits::NA_REAL;
use miniextendr_api::markers::WidensToF64;
/// Probability value in [0, 1]
#[repr(transparent)]
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
pub struct Probability(f64);
impl Probability {
pub fn new(value: f64) -> Option<Self> {
if (0.0..=1.0).contains(&value) {
Some(Self(value))
} else {
None
}
}
pub fn value(self) -> f64 {
self.0
}
}
// Enable automatic conversions for all containers
impl RNativeType for Probability {
const SEXP_TYPE: SEXPTYPE = SEXPTYPE::REALSXP;
const R_NA: Self = Self(NA_REAL);
unsafe fn dataptr_mut(sexp: SEXP) -> *mut Self {
unsafe { miniextendr_api::sys::REAL(sexp).cast::<Self>() }
}
fn elt(sexp: SEXP, i: isize) -> Self {
Self(sexp.real_elt(i))
}
}
// Enable coercion to f64
impl WidensToF64 for Probability {}
impl From<Probability> for f64 {
fn from(p: Probability) -> f64 {
p.0
}
}
// Now all these work:
// Vec<Probability>, &[Probability], DVector<Probability>, etc.
πChecklist for New Types
-
Choose your approach:
#[repr(transparent)]newtype over primitive? βRNativeType- Complex type or needs validation? β Direct
TryFromSexp/IntoR - Large/mutable? β
ExternalPtr
-
Implement required traits:
-
RNativeTypeORTryFromSexp+IntoR -
Copy(if usingRNativeType) -
TypedExternal(if usingExternalPtr)
-
-
Optional enhancements:
-
WidensToI32/WidensToF64for coercion -
OrdforBinaryHeapsupport -
HashforHashSet/HashMapkey support
-
-
Test your type:
- Scalar round-trip: Rust β R β Rust
-
Vector round-trip:
Vec<T>both directions -
Option handling:
NoneβNULLorNA - Edge cases: empty vectors, single elements