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:

  1. Implement RNativeType - For types with the same memory layout as R’s native types
  2. Implement TryFromSexp/IntoR directly - For types requiring custom conversion logic

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:

  1. Be #[repr(transparent)] over an R-native element representation
  2. Implement Copy (required by the trait bound)
  3. Be 'static (no borrowed data)
  4. Define R_NA for missing sparse/padded slots and implement both dataptr_mut and ALTREP-aware elt

πŸ”—Memory Layout Correspondence

Rust TypeR TypeSEXPTYPE
i32integerINTSXP
f64numericREALSXP
u8rawRAWSXP
RLogicallogicalLGLSXP
RcomplexcomplexCPLXSXP

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 Username above)
  • 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

TraitMeaningUse When
WidensToI32Losslessly converts to i328/16-bit signed integers
WidensToF64Losslessly converts to f64Any 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

  1. Choose your approach:

    • #[repr(transparent)] newtype over primitive? β†’ RNativeType
    • Complex type or needs validation? β†’ Direct TryFromSexp/IntoR
    • Large/mutable? β†’ ExternalPtr
  2. Implement required traits:

    • RNativeType OR TryFromSexp + IntoR
    • Copy (if using RNativeType)
    • TypedExternal (if using ExternalPtr)
  3. Optional enhancements:

    • WidensToI32/WidensToF64 for coercion
    • Ord for BinaryHeap support
    • Hash for HashSet/HashMap key support
  4. Test your type:

    • Scalar round-trip: Rust β†’ R β†’ Rust
    • Vector round-trip: Vec<T> both directions
    • Option handling: None ↔ NULL or NA
    • Edge cases: empty vectors, single elements