This document describes how miniextendr converts between R types and Rust types. Conversions are governed by three modes (normal, coerce, strict) and apply to both directions: R-to-Rust (TryFromSexp) and Rust-to-R (IntoR).

See also: miniextendr-api/src/from_r.rs, miniextendr-api/src/into_r.rs, miniextendr-api/src/strict.rs, miniextendr-api/src/coerce.rs


πŸ”—Conversion Modes

πŸ”—Normal Mode (default)

Each Rust type accepts exactly one R type. For example, i32 only accepts INTSXP, f64 only accepts REALSXP. A type mismatch produces an error.

πŸ”—Coerce Mode

Coerced types (like i64, u64, isize, usize, and sub-integer types i8, i16, u16, u32, f32) accept multiple R types: INTSXP, REALSXP, RAWSXP, and LGLSXP. The value is extracted as the R native type, then converted to the target Rust type via TryCoerce. This is the default for these types – no attribute is needed.

πŸ”—Strict Mode (#[miniextendr(strict)])

Only INTSXP and REALSXP are accepted. RAWSXP and LGLSXP are rejected. Additionally, output values that don’t fit in R’s integer range (i32) cause a panic (R error) instead of silently widening to REALSXP (f64).


πŸ”—R-to-Rust Conversions (Input: TryFromSexp)

πŸ”—Native Scalar Types (Normal Mode)

These types require an exact R type match. Length must be 1.

Rust TypeAccepted R TypeOn NAOn Type Mismatch
i32INTSXPReturns i32::MIN (NA_integer_)Error
f64REALSXPReturns NA_real_ (specific NaN)Error
u8RAWSXPNo NA concept in rawError
RcomplexCPLXSXPReturns Rcomplex { r: NA_real_, i: NA_real_ }Error
boolLGLSXPError (NA is not true/false)Error
RbooleanLGLSXPError (NA not representable)Error
RLogicalLGLSXPReturns RLogical::NaError
StringSTRSXPError (NA_character_)Error
&strSTRSXPError (NA_character_)Error

πŸ”—Option Wrappers (Normal Mode)

Option<T> maps NA to None and NULL to None:

Rust TypeAccepted R TypeOn NAOn NULL
Option<i32>INTSXPNoneNone
Option<f64>REALSXPNoneNone
Option<u8>RAWSXPSome(val) (raw has no NA)None
Option<Rcomplex>CPLXSXPNoneNone
Option<bool>LGLSXPNoneNone
Option<Rboolean>LGLSXPNoneNone
Option<String>STRSXPNoneNone

πŸ”—Coerced Scalar Types (Multi-Source)

These types accept INTSXP, REALSXP, RAWSXP, and LGLSXP:

Rust TypeINTSXPREALSXPRAWSXPLGLSXPSTRSXP
i8Narrow i32->i8f64->i8 (reject frac/NaN)u8->i8logical->i32->i8Error
i16Narrow i32->i16f64->i16 (reject frac/NaN)u8->i16logical->i32->i16Error
u16i32->u16 (reject neg)f64->u16 (reject frac/neg/NaN)u8->u16logical->i32->u16Error
u32i32->u32 (reject neg)f64->u32 (reject frac/neg/NaN)u8->u32logical->i32->u32Error
f32i32 as f32f64 as f32u8 as f32logical as f32Error
i64Widen i32->i64f64->i64 (reject frac/NaN/Inf)u8->i64logical->i32->i64Error
u64i32->u64 (reject neg)f64->u64 (reject frac/neg/NaN)u8->u64logical->i32->u64Error
isizeWiden i32->isizef64->i64->isize (reject frac)u8->isizelogical->isizeError
usizei32->usize (reject neg)f64->u64->usize (reject frac/neg)u8->usizelogical->i32->usizeError

Notes on coercion checks:

  • Fractional check: f64 values with a non-zero fractional part are rejected (e.g., 3.14 fails)
  • NaN/Inf: Both are rejected when converting f64 to integer types
  • Range check: Values outside the target type’s range are rejected (e.g., 300 fails for i8)
  • NA propagation: NA_integer_ and NA_real_ produce errors for non-Option types; Option<i64> etc. map NA to None

πŸ”—Strict Mode Scalar Types

Only INTSXP and REALSXP accepted; RAWSXP and LGLSXP are rejected:

Rust TypeINTSXPREALSXPRAWSXPLGLSXP
i64 (strict)Widen i32->i64f64->i64 (reject frac/NaN)PanicPanic
u64 (strict)i32->u64 (reject neg)f64->u64 (reject frac/neg)PanicPanic
isize (strict)Delegates to i64Delegates to i64PanicPanic
usize (strict)Delegates to u64Delegates to u64PanicPanic

πŸ”—Vector Types

Vector conversions (Vec<T>) follow the same source-type rules as scalars:

Rust TypeAccepted R Type(s)Element Behavior
Vec<i32> / &[i32]INTSXP onlyDirect memcpy
Vec<f64> / &[f64]REALSXP onlyDirect memcpy
Vec<u8> / &[u8]RAWSXP onlyDirect memcpy
Vec<bool>LGLSXP onlyEach logical->bool; NA causes error
Vec<String>STRSXP onlyEach CHARSXP->String; NA causes error
Vec<Option<i32>>INTSXP onlyNA_integer_ -> None
Vec<Option<f64>>REALSXP onlyNA_real_ -> None
Vec<Option<bool>>LGLSXP onlyNA_logical -> None
Vec<Option<String>>STRSXP onlyNA_character_ -> None
Vec<i64> (strict)INTSXP or REALSXPPer-element checked coercion; RAWSXP/LGLSXP rejected
Vec<u64> (strict)INTSXP or REALSXPPer-element checked coercion; RAWSXP/LGLSXP rejected

πŸ”—Rust-to-R Conversions (Output: IntoR)

πŸ”—Scalar Types

Rust TypeR Output TypeNotes
i32INTSXPDirect via Rf_ScalarInteger
f64REALSXPDirect via Rf_ScalarReal
u8RAWSXPDirect via Rf_ScalarRaw
boolLGLSXPtrue->1, false->0
RbooleanLGLSXPDirect
RLogicalLGLSXPIncludes NA support
String / &strSTRSXPUTF-8 encoding via Rf_mkCharLenCE
charSTRSXPSingle UTF-8 character as string
()NILSXPReturns R NULL

πŸ”—Widening Scalar Types

Rust TypeR Output TypeNotes
i8, i16, u16INTSXPInfallible widening to i32
f32, u32REALSXPInfallible widening to f64

πŸ”—Smart Scalar Conversion (i64, u64, isize, usize)

These types use a smart conversion strategy: fit in i32 -> INTSXP, otherwise -> REALSXP.

Rust TypeConditionR Output TypeNotes
i64i32::MIN < val <= i32::MAXINTSXPExact representation
i64Otherwise (incl. i32::MIN)REALSXPMay lose precision >2^53
u64val <= i32::MAXINTSXPExact representation
u64val > i32::MAXREALSXPMay lose precision >2^53
isizeDelegates to i64INTSXP or REALSXPSame rules as i64
usizeDelegates to u64INTSXP or REALSXPSame rules as u64

Why i32::MIN is excluded from INTSXP: In R, i32::MIN (-2147483648) is NA_integer_. Returning it as INTSXP would create an unintended NA value.

πŸ”—Strict Output Conversion

With #[miniextendr(strict)], large integer types panic instead of falling back to REALSXP:

Rust TypeConditionStrict Behavior
i64Fits in (i32::MIN, i32::MAX]INTSXP (same as normal)
i64Outside rangePanic (R error)
u64val <= i32::MAXINTSXP (same as normal)
u64val > i32::MAXPanic (R error)
Vec<i64>All elements fitINTSXP vector
Vec<i64>Any element outside rangePanic (R error)

πŸ”—Option Types (NA Mapping)

Rust TypeSome(val)None
Option<i32>INTSXP scalarNA_integer_
Option<f64>REALSXP scalarNA_real_
Option<bool>LGLSXP scalarNA_logical
Option<Rboolean>LGLSXP scalarNA_logical
Option<String>STRSXP scalarNA_character_
Option<&str>STRSXP scalarNA_character_
Option<Vec<T>>R vectorNULL (R_NilValue)
Option<HashMap<...>>Named listNULL (R_NilValue)

πŸ”—Vector Types

Rust TypeR Output TypeNotes
Vec<i32> / &[i32]INTSXPBulk memcpy
Vec<f64> / &[f64]REALSXPBulk memcpy
Vec<u8> / &[u8]RAWSXPBulk memcpy
Vec<bool> / &[bool]LGLSXPElement-wise bool as i32
Vec<String>STRSXPElement-wise CHARSXP creation
Vec<Option<i32>>INTSXPNone -> NA_integer_
Vec<Option<f64>>REALSXPNone -> NA_real_
Vec<Option<bool>>LGLSXPNone -> NA_logical
Vec<Option<String>>STRSXPNone -> NA_character_

πŸ”—Smart Vector Conversion (Vec of large integers)

Vec<i64>, Vec<u64>, Vec<isize>, Vec<usize> check whether all elements fit in i32. If yes, the entire vector is INTSXP; otherwise, the entire vector is REALSXP.

Rust TypeAll Fit in i32?R Output Type
Vec<i64>Yes (all in (i32::MIN, i32::MAX])INTSXP
Vec<i64>No (any element outside)REALSXP
Vec<u64>Yes (all <= i32::MAX)INTSXP
Vec<u64>NoREALSXP

πŸ”—Collection Types

Rust TypeR Output Type
HashMap<String, V>Named list (VECSXP)
BTreeMap<String, V>Named list (VECSXP)
HashSet<T> / BTreeSet<T>Vector (order may vary for HashSet)
VecDeque<T>Vector (converted to Vec first)
BinaryHeap<T>Vector (arbitrary order)
Vec<Vec<T>>List of vectors (VECSXP)
(A, B, ...)Unnamed list (VECSXP), up to 8 elements (IntoR only, no TryFromSexp)
PathBufSTRSXP (lossy UTF-8 conversion)
OsStringSTRSXP (lossy UTF-8 conversion)

πŸ”—Result and Error Types

Rust TypeOk(val)Err(e)
Result<T, E: Debug> (default)T::into_sexp()Panic -> R error
Result<T, E: Display> (unwrap_in_r)T::into_sexp()list(error = msg)
Result<T, ()>T::into_sexp()NULL (R_NilValue)

πŸ”—Raw/Bytemuck Conversions (Feature-Gated)

Enabled with features = ["raw_conversions"]. Uses R’s RAWSXP for binary POD data.

WrapperDirectionFormatType Tag
Raw<T>BothHeaderless bytesNo
RawSlice<T>BothHeaderless byte sequenceNo
RawTagged<T>Both16-byte header + bytesYes (mx_raw_type attr)
RawSliceTagged<T>Both16-byte header + byte sequenceYes (mx_raw_type attr)

Safety checks: length validation, alignment (copy if misaligned), magic/version validation (tagged only), type name matching (tagged only).


πŸ”—Special Values Quick Reference

R ValueRust RepresentationNotes
NA_integer_i32::MIN (-2147483648)Excluded from valid i32 range in conversions
NA_real_Specific NaN bit patternDistinguished from regular NaN by bit comparison
NA_logical_i32::MINSame sentinel as NA_integer_
NA_character_R_NaString CHARSXPMapped to None in Option<String>
NaNf64::NANNot the same as NA_real_; passes through as valid f64
Inf / -Inff64::INFINITY / f64::NEG_INFINITYValid f64 values; rejected when coercing to integers
NULLR_NilValueMapped to None in Option<T>; () produces NULL

πŸ”—Cookbook: Common Conversion Recipes

πŸ”—β€œI have Vec<Option<i64>> β€” how does it convert to R?”

Each element uses the smart i64 conversion. If all Some values fit in i32, the whole vector is INTSXP; otherwise REALSXP. None values become NA_integer_ or NA_real_ accordingly.

#[miniextendr]
fn make_nullable_ids() -> Vec<Option<i64>> {
    vec![Some(1), None, Some(42), Some(i64::MAX)]
    // -> REALSXP because i64::MAX doesn't fit in i32
}

πŸ”—β€œI want to accept either integer or numeric from R”

Use a coerced type (i64, u64, f32) β€” they accept INTSXP, REALSXP, RAWSXP, and LGLSXP automatically:

#[miniextendr]
fn flexible_input(x: i64) -> i64 {
    x * 2  // works with integer(1) or numeric(1) from R
}

Or use #[miniextendr(strict)] to only accept INTSXP and REALSXP (no raw/logical):

#[miniextendr(strict)]
fn strict_input(x: i64) -> i64 { x * 2 }

πŸ”—β€œI want a named list from R as a HashMap”

use std::collections::HashMap;

#[miniextendr]
fn process_config(config: HashMap<String, f64>) -> f64 {
    config.get("threshold").copied().unwrap_or(0.5)
}

In R: process_config(list(threshold = 0.9, alpha = 0.05))

πŸ”—β€œI want to return NA for missing values”

Wrap in Option β€” None becomes the appropriate NA:

#[miniextendr]
fn safe_divide(a: f64, b: f64) -> Option<f64> {
    if b == 0.0 { None } else { Some(a / b) }
}

πŸ”—β€œI want to return NULL on failure, not an error”

Use Result<T, ()>:

#[miniextendr]
fn try_parse(s: String) -> Result<i32, ()> {
    s.parse::<i32>().map_err(|_| ())
    // Ok(42) -> 42L in R; Err(()) -> NULL in R
}

πŸ”—β€œI have a struct and want to pass it to R and back”

Use #[miniextendr] on an impl block β€” the struct is wrapped in an ExternalPtr:

struct Counter { n: i32 }

#[miniextendr]
impl Counter {
    fn new() -> Self { Counter { n: 0 } }
    fn increment(&mut self) { self.n += 1; }
    fn get(&self) -> i32 { self.n }
}

πŸ”—β€œI want to accept R’s ... (dots)”

Use _dots: &Dots as the last parameter:

#[miniextendr]
fn sum_all(x: f64, _dots: &Dots) -> f64 {
    // x is the first argument; _dots captures the rest
    x  // dots are validated but not directly accessible as Rust values
}

For typed dots validation, see DOTS_TYPED_LIST.md.