This guide documents how miniextendr converts between R and Rust types, including NA handling, coercion rules, and edge cases.

πŸ”—Basic Type Mappings

πŸ”—Scalar Types

R TypeRust TypeNotes
integer (length 1)i32NA β†’ panic
numeric (length 1)f64NA preserved as NA_REAL
logical (length 1)boolNA β†’ panic
character (length 1)String, &strNA β†’ "" (lossy); use Option to preserve NA
raw (length 1)u8No NA in raw
complex (length 1)RcomplexHas real/imag NA

πŸ”—Vector Types

R TypeRust TypeNotes
integerVec<i32>, &[i32]NA = i32::MIN
numericVec<f64>, &[f64]NA = special bit pattern
logicalVec<bool>NA β†’ error; use Vec<Option<bool>> to preserve it
characterVec<String>NA β†’ "" (lossy); use Vec<Option<String>> to preserve it
rawVec<u8>, &[u8]No NA
listVariousSee Lists and Collections sections

For Rust-to-R returns, [T; N] uses the same conversion as &[T], without an intermediate Rust allocation. This includes native values, smart integers (i64, u64, isize, usize, u32), coerced numbers, String, &str, bool and Rboolean. Arrays produce a vector, including typed empty vectors for N = 0; they do not add a dim attribute. Direct IntoR conversion of Option<[T; N]> maps None to NULL and Some through the array conversion.

πŸ”—Nested Collection Types

miniextendr supports converting nested collections to R lists:

Rust TypeR TypeNotes
Vec<Vec<T>>list of vectorsAny T with Vec<T>: IntoR; each element is what Vec<T> gives
Vec<Option<Vec<T>>> / Vec<Option<Box<[T]>>>list of vectorsAs Vec<Vec<T>>; None β†’ NULL
Vec<&[T]> / Vec<Option<&[T]>>list of vectorsAny T with &[T]: IntoR; slices are copied
Vec<Box<[T]>>list of vectorsBoxed slices β†’ vectors (any T with Vec<T>: IntoR)
Vec<[T; N]>list of vectorsFixed arrays β†’ vectors (any T with Vec<T>: IntoR)
Vec<HashSet<T>>list of vectorsSets β†’ unordered vectors
Vec<BTreeSet<T>>list of vectorsSets β†’ sorted vectors

These are particularly useful with #[derive(DataFrameRow)] where row fields can contain collections. For the wide integers (usize, u64, i64, isize, u32) each element decides on its own: integer when all its values fit, double otherwise.

πŸ”—Option Types (NA-Safe Scalars)

Use Option<T> to handle NA safely on both input and output.

Input (R β†’ Rust): NA becomes None; non-NA becomes Some(value). Output (Rust β†’ R): None becomes the appropriate R NA; Some(v) converts normally. NULL: Option<T> also maps R’s NULL to None on input.

R TypeRust TypeInput NAInput NULLOutput None
integerOption<i32>NoneNoneNA_integer_
numericOption<f64>NoneNoneNA_real_
logicalOption<bool>NoneNoneNA (logical)
characterOption<String>NoneNoneNA_character_
characterOption<&str>NoneNoneNA_character_

Note: i32 rejects NA_integer_ (= i32::MIN) on input; you must use Option<i32> to receive integer NA. Note: bool rejects logical NA on input; use Option<bool> to receive logical NA.

This table covers owned scalars only. A reference return (Option<&i32>) or a container return (Option<Vec<T>>, Option<HashMap<...>>) maps None to NULL instead of NA β€” same for Result::Err in most shapes. See the full β€œAbsence Contract” table in CONVERSION_MATRIX.md for every return-type category, including the Result side.

πŸ”—ALTREP-Aware Types

R frequently passes ALTREP vectors (e.g., 1:10, seq_len(N)) to Rust. All parameter types handle this transparently:

Rust TypeALTREP Handling
Vec<i32>, &[f64], etc.Auto-materialized during conversion
SEXPAuto-materialized via ensure_materialized
AltrepSexpAccepted only if ALTREP, !Send + !Sync

See Receiving ALTREP from R for details.


πŸ”—NA Value Representation

πŸ”—Integer NA

pub const NA_INTEGER: i32 = i32::MIN;  // -2147483648

In R, NA_integer_ is represented as i32::MIN. This means:

  • Valid integers: -2147483647 to 2147483647
  • i32::MIN is reserved for NA

Implication: You cannot represent i32::MIN as a valid value in R integers.

πŸ”—Logical NA

pub const NA_LOGICAL: i32 = i32::MIN;  // Same as integer

R logicals are stored as integers internally:

  • TRUE = 1
  • FALSE = 0
  • NA = i32::MIN

πŸ”—Real (Double) NA

pub const NA_REAL: f64 = f64::from_bits(0x7FF0_0000_0000_07A2);

R’s NA_real_ is an IEEE 754 NaN whose low 32-bit word is 1954.

Critical: This is different from regular f64::NAN:

use miniextendr_api::{IsNA, RNativeType};

let na = f64::R_NA;
let nan = f64::NAN;

assert!(na.is_na());
assert!(!nan.is_na());
assert!(na.is_nan() && nan.is_nan());

Only the low word is compared because arithmetic on NA_real_ quiets the NaN: NA_real_ * 1 (and so any computed NA, such as c(1, NA) * 2) has bits 0x7FF8_0000_0000_07A2, not NA_REAL’s 0x7FF0_0000_0000_07A2, and R still reports it as NA. A bit-exact comparison against NA_REAL would read it as a plain NaN.

Implication: When working with f64 vectors, regular NaN values pass through unchanged. Only values R considers NA are treated as NA.

IsNA is the native missing-value capability bound for i32, f64, RLogical and Rcomplex. Use T: IsNA for generic native NA handling and T::R_NA to construct a missing value. Raw u8 has no NA and does not implement it. Types without an in-band NA use Option<T> at conversion boundaries.

For a transparent native newtype, derive RNativeType and IsNA together to forward its missing-value check. Generic wrappers get the IsNA implementation only where their field implements it; a raw-byte instantiation remains usable as native storage.

IsNA follows R_IsNA. R’s is.na() / anyNA() and ALTREP no_na hints also count ordinary NaNs; their real/complex checks must continue to use is_nan().

πŸ”—String NA

R’s NA_character_ is a special CHARSXP pointer (R_NaString).

Plain String / &str conversion maps string NA to "", which loses the distinction between NA and an empty string. Use Option<String> for NA-safe access:

#[miniextendr]
pub fn handle_string(s: Option<String>) -> String {
    s.unwrap_or_else(|| "was NA".to_string())
}

πŸ”—Coercion System

miniextendr provides automatic type coercion for numeric types.

πŸ”—Coercion Precedence

Two traits control coercion:

  1. Coerce<R> - Infallible (always succeeds)
  2. TryCoerce<R> - Fallible (can fail)

When both exist for a type pair, Coerce takes precedence:

// Blanket impl ensures Coerce always wins
impl<T, R> TryCoerce<R> for T where T: Coerce<R> {
    fn try_coerce(self) -> Result<R, Infallible> {
        Ok(self.coerce())
    }
}

πŸ”—Infallible Coercions (Coerce)

FromToNotes
i32f64Widening (no precision loss)
i32i32Identity
f64f64Identity
Option<T>TNone β†’ NA value

πŸ”—Fallible Coercions (TryCoerce)

FromToFails When
f64i32NaN, infinity, fractional, overflow
i32u32Negative value
i32NonZeroI32Zero value
f64u64Negative, NaN, overflow

πŸ”—Enabling Coercion

Use #[miniextendr(coerce)] to enable automatic coercion:

// Without coerce: f64 parameter requires numeric input
#[miniextendr]
pub fn square(x: f64) -> f64 { x * x }

// With coerce: accepts integer, coerces to f64
#[miniextendr(coerce)]
pub fn square_coerce(x: f64) -> f64 { x * x }
square(2L)        # Error: expected numeric
square_coerce(2L) # 4.0 (integer coerced to double)

πŸ”—Per-Parameter Coercion

#[miniextendr]
pub fn mixed(
    #[miniextendr(coerce)] x: f64,  // Coerce this one
    y: i32,                          // No coercion
) -> f64 {
    x + y as f64
}

πŸ”—Option-to-NA Conversion

When returning Option<T>, None converts to R’s NA for the corresponding R type. When accepting Option<T>, R’s NA (or NULL) becomes None in Rust.

#[miniextendr]
pub fn maybe_value(x: i32) -> Option<i32> {
    if x > 0 { Some(x) } else { None }
}
maybe_value(5L)   # 5L
maybe_value(-1L)  # NA_integer_

String NA round-trip:

#[miniextendr]
pub fn handle_na_string(s: Option<String>) -> Option<String> {
    s.map(|x| x.to_uppercase())
}
handle_na_string("hello")     # "HELLO"
handle_na_string(NA_character_)  # NA_character_

NULL on input:

#[miniextendr]
pub fn nullable(x: Option<i32>) -> i32 {
    x.unwrap_or(-1)
}
nullable(NULL)        # -1
nullable(NA_integer_) # -1
nullable(42L)         # 42

πŸ”—Coercion for Options

Option<T> coerces to T with None β†’ NA:

// This works with coercion enabled:
// R's NA_integer_ β†’ None β†’ coerced to NA_real_
#[miniextendr(coerce)]
pub fn option_coerce(x: f64) -> f64 { x }

πŸ”—Vector NA Handling

πŸ”—NA-Aware Vector Types

Both Vec<Option<T>> and Box<[Option<T>]> are supported for reading and writing NA-aware vectors. Element-level semantics: None ↔ NA, Some(v) ↔ concrete value.

Input (R β†’ Rust):

R TypeRust TypeNA sentinel mapped to None
INTSXPVec<Option<i32>>, Box<[Option<i32>]>i32::MIN (NA_integer_)
REALSXPVec<Option<f64>>, Box<[Option<f64>]>NA_real_, including computed NAs (a NaN with low word 1954, as R_IsNA checks)
LGLSXPVec<Option<bool>>, Box<[Option<bool>]>i32::MIN (NA_logical_)
STRSXPVec<Option<String>>, Box<[Option<String>]>R_NaString (NA_character_)

Coerced numeric types also support Vec<Option<T>> (accepts INTSXP, REALSXP, RAWSXP, LGLSXP):

Rust TypeAccepted R TypesNA handling
Vec<Option<i64>>INTSXP, REALSXP, RAWSXP, LGLSXPNA_integer_/NA_real_ β†’ None
Vec<Option<u64>>INTSXP, REALSXP, RAWSXP, LGLSXPNA_integer_/NA_real_ β†’ None
Vec<Option<i8>>, Vec<Option<i16>>, etc.INTSXP, REALSXP, RAWSXP, LGLSXPNA β†’ None

Output (Rust β†’ R):

Rust TypeR OutputNone becomes
Vec<Option<i32>>INTSXPNA_integer_
Vec<Option<f64>>REALSXPNA_real_
Vec<Option<bool>>LGLSXPNA (logical)
Vec<Option<String>>STRSXPNA_character_

πŸ”—Reading Vectors with NA

For vectors with potential NA values, use Option element type:

#[miniextendr]
pub fn count_na(x: Vec<Option<i32>>) -> i32 {
    x.iter().filter(|v| v.is_none()).count() as i32
}

Use Box<[Option<T>]> when you prefer a fixed-size owned slice over Vec:

#[miniextendr]
pub fn sum_non_na(x: Box<[Option<f64>]>) -> f64 {
    x.iter().filter_map(|v| *v).sum()
}

πŸ”—Writing Vectors with NA

Return Vec<Option<T>> to include NA values:

#[miniextendr]
pub fn add_na_at_end(x: Vec<i32>) -> Vec<Option<i32>> {
    let mut result: Vec<Option<i32>> = x.into_iter().map(Some).collect();
    result.push(None);  // Adds NA
    result
}

πŸ”—Partial-results pattern

#[miniextendr]
pub fn parse_numbers(strings: Vec<String>) -> Vec<Option<f64>> {
    strings.iter()
        .map(|s| s.parse().ok())  // failed parses become None (NA in R)
        .collect()
}

πŸ”—Numbers from text, factors, or numbers (AsNumeric)

Data read from CSV files or spreadsheets often arrives as character, with numbers mixed with tokens such as "n/a" or "<0.1", or as a factor whose labels are the numbers. AsNumericVec (and the length-1 AsNumeric) read such an argument the way as.numeric() does, and fail with the offending values instead of turning them into NA:

use miniextendr_api::AsNumericVec;

#[miniextendr]
pub fn total(x: AsNumericVec) -> f64 {
    x.0.into_iter().flatten().sum()
}
total(c(1.5, NA))                  # 1.5    doubles as is
total(c(2L, NA))                   # 2      integer and logical widened
total(c(" 1.5 ", "1e3", "0x10"))   # 1017.5 parsed like as.numeric()
total(factor(c("10", "2")))        # 12     factor labels, not codes
total(c("", "NA", NA))             # 0      blank, "NA" and NA are NA
total(c("1", "n/a", "3", "<0.1"))
#> Error: ... non-numeric value(s): "n/a", "<0.1" (elements 2, 4)
total(list(1))
#> Error: 'x' must be numeric, logical, character, or factor
InputResult
doubleas is; NA_real_ β†’ None, NaN stays Some(NaN)
integer, logicalwidened to f64; NA β†’ None
characterR’s R_strtod on each value, the rest must be blank (surrounding spaces, Inf, -inf, NaN, 1e3, hex such as 0x1A all accepted); NA_character_, blank strings and the token "NA" β†’ None
factoreach label (levels(x)[x]) parsed as character; code NA β†’ None
raw, complex, list, …refused (R precondition, then SexpError::Type)

Every value that is not a number is reported in one error, with 1-based element numbers and at most 10 values listed (the rest counted as and N more). The token "NA" follows scan() / type.convert(): as.numeric("NA") also gives NA, but with a coercion warning. Option<AsNumeric> / Option<AsNumericVec> additionally accept NULL as None.

#[miniextendr(no_na)] refuses everything the markers read as missing, not only what R’s anyNA() sees: after the conversion the C wrapper also refuses the token "NA", blank strings, factor labels among those, and NaN read from the text "NaN" (as the R guard refuses a numeric NaN), with the same 'x' must not be NA / 'x' must not contain NA condition. On Option<AsNumeric>, NULL still passes as None.

A marker converts to R as its inner Vec<Option<f64>> / Option<f64>, which it reads back unchanged, so a #[miniextendr] trait method can take one as a parameter (the trait ABI converts each argument both ways). Returning .0 stays the idiom for a function.

For parsing into an arbitrary T: FromStr, use AsFromStr<T> / AsFromStrVec<T>: character input only, NA refused, and each failing value quoted in the error with its 1-based position ("n/a": invalid digit found in string (element 2)). They convert to R as their Display text.

πŸ”—Labels from any atomic vector (AsCharacter)

Identifiers in R data are often numbers or factors: subject IDs, visit numbers, grouping columns read by read.csv(). A Vec<String> parameter accepts character input only. AsCharacterVec (and the length-1 AsCharacter) take any atomic vector and read it the way as.character() does, so the strings are the ones R produces elsewhere:

use miniextendr_api::AsCharacterVec;
use std::collections::HashSet;

#[miniextendr]
pub fn n_subjects(ids: AsCharacterVec) -> i32 {
    let distinct: HashSet<String> = ids.0.into_iter().flatten().collect();
    i32::try_from(distinct.len()).expect("fewer than 2^31 subjects")
}
n_subjects(c(101L, 102L, 101L))        # 2
n_subjects(factor(c("S1", "S2", NA)))  # 2  factor labels, not codes
n_subjects(c(0.1 + 0.2, 0.3))          # 1  both read as "0.3"
n_subjects(list(1))
#> Error: 'ids' must be atomic
InputResult
characteras is; NA_character_ β†’ None, "NA" and "" stay values
integer, doubleR’s formatting (doubles to 15 significant digits): 0.1 + 0.2 β†’ "0.3", 1e6 β†’ "1e+06", 100 β†’ "100", NaN β†’ "NaN"; NA β†’ None
logical"TRUE" / "FALSE"; NA β†’ None
complex, raw"1+2i", "ff" (as as.character())
classed vector (factor, Date, POSIXct, …)its as.character() method: factor labels, "2024-01-15" for a Date
list, data frame, NULL, …refused (R precondition is.atomic(x), then SexpError::Type)

The text always comes from R, never from Rust formatting (format!("{}", 0.1 + 0.2) is "0.30000000000000004"). A plain vector goes through R’s coerceVector(), as the as.character() primitive does. A vector with a class attribute is passed to as.character(x) itself, because coerceVector() on the double underneath a Date would give its day count ("19737"); an error in that method becomes the conversion error. Names and dim are dropped, as as.character() drops them: a matrix is read in column-major order.

Unlike AsNumeric, which reads the token "NA" and blank strings as missing, AsCharacter keeps "NA" and "" as strings, as as.character() does: only NA itself is None. NaN becomes "NaN"; #[miniextendr(no_na)] refuses it along with NA, since anyNA() counts it. no_na also refuses an NA that only the conversion produces, checked after it: a factor NA level (factor(x, exclude = NULL), whose code is not NA) and a class’s as.character() method returning NA. Option<AsCharacter> / Option<AsCharacterVec> also accept NULL as None. Like AsNumeric, the markers convert to R as their inner Vec<Option<String>> / Option<String> (for trait-method parameters); returning .0 stays the idiom.


πŸ”—Slice Lifetimes

When using slice parameters (&[T]), be aware of lifetime implications:

// SAFE: Slice is only used during function execution
#[miniextendr]
pub fn sum(x: &[f64]) -> f64 {
    x.iter().sum()
}

Main-thread wrappers and trait shims convert explicit shared native slices, and Vecs of them, through TryFromRoot, tying the borrow to the rooted call argument. Use the slice during that call; copy to Vec when the data must outlive it.

Worker wrappers check the slice through TryFromRoot on R’s main thread, copy it into a Vec, and borrow that copy inside the worker, so the worker never reads R memory.

Raw TryFromSexp reference conversions and mutable slices still have unconstrained lifetimes (#1427). Their callers must keep the source rooted and enforce borrowing rules themselves. A root supplies liveness; it does not grant exclusive access for a mutable slice.


πŸ”—String Lifetimes

Borrowed strings point directly into a CHARSXP; they do not own or root the R object. When you get a &str from R:

#[miniextendr]
pub fn process_string(s: &str) -> String {
    // s is valid for this .Call while the argument remains reachable
    s.to_uppercase()
}

Main-thread wrappers and trait shims bind explicit &str, Option<&str>, Vec<&str> and Vec<Option<&str>> inputs to the call’s root through TryFromRoot. Worker wrappers convert &str, Option<&str>, Vec<&str> and Vec<Option<&str>> to the corresponding owned String forms on R’s main thread, move that storage into the worker, and create the borrows there. The worker’s strings do not point into R memory.

Raw TryFromSexp character references still expose an unconstrained lifetime (#1427). R can collect a CHARSXP once it becomes unreachable; copy to String before storing a raw borrow beyond its source’s lifetime. NA_character_ becomes "" on the plain borrowed/owned string paths. Use optional strings when NA must remain distinguishable from an empty string.


πŸ”—ExternalPtr Semantics

When using #[derive(ExternalPtr)]:

#[derive(ExternalPtr)]
pub struct MyData {
    values: Vec<f64>,
}

The Rust data is heap-allocated and owned by R:

  1. new() allocates Rust data on heap
  2. Pointer stored in R’s external pointer SEXP
  3. R’s GC tracks the SEXP
  4. When SEXP is collected, Rust Drop runs
  5. Heap memory freed

Thread safety: ExternalPtr<T> is Send when T: Send, allowing a sequential ownership handoff between worker and R threads. It is not Sync and must not be accessed concurrently. Operations that touch the SEXP or R’s protection machinery are routed to the R thread.


πŸ”—Complex Types

πŸ”—Lists

Lists convert to various Rust types:

// Named list β†’ HashMap
#[miniextendr]
pub fn process_map(x: HashMap<String, i32>) -> i32 {
    x.values().sum()
}

// List β†’ Vec of heterogeneous items (requires SEXP)
#[miniextendr]
pub fn list_length(x: List) -> i32 {
    x.len() as i32
}

A List argument takes any R list, whatever its names. It refuses a pairlist (formals(f), pairlist(...)) with a type error, 'x' must be a list: got pairlist, convert it with as.list(), though R’s is.list() is TRUE for one: List is a view with no root of its own, and a coerced copy would be an object nothing roots, which the function’s first allocation could free (#1866). The same holds for Option<List>, NamedList and ListMut. Call as.list() on the R side.

R lets a name appear more than once (list(a = 1, a = 2)), and so does List; get_named returns the first element with the name, as x[["a"]] does. A function that needs unique names checks for them itself, with its own message:

#[miniextendr]
pub fn settings_count(settings: List) -> i32 {
    if let Some(name) = settings.first_duplicate_name() {
        panic!("`settings` has the name {name:?} more than once");
    }
    settings.len() as i32
}

first_duplicate_name skips NA and empty names. The conversions that would have to drop a value refuse a repeated name themselves: HashMap / BTreeMap (from a list or via TryFromList), NamedVector, typed_list! specs, and a #[derive(TryFromList)] struct whose field name appears twice.

πŸ”—Data Frames

Accept a validated DataFrame view and use its typed column accessors:

#[miniextendr]
pub fn get_column(df: DataFrame, name: &str) -> Result<Vec<f64>, String> {
    df.column(name)
        .ok_or_else(|| format!("missing or non-numeric column {name:?}"))
}

Rust-side constructors return BuiltDataFrame, a rooted owner that dereferences to DataFrame and implements IntoR. See Data Frames for the view/handle split and conversion traits.

πŸ”—Dynamic R-native values (RValue)

RValue is an owned, Send value tree for dynamic base-R data. It covers NULL, logical/integer/double/complex/character/raw vectors, and recursive named or unnamed lists without carrying a live SEXP across threads:

use miniextendr_api::RValue;

let payload = RValue::List(vec![
    (Some("count".into()), RValue::from(3i32)),
    (Some("labels".into()), RValue::from(vec!["a", "b"])),
]);

A tuple whose fields convert to RValue becomes an unnamed RValue::List, the same shape as the tuple’s own IntoR: RValue::from((1i32, "a")) is list(1L, "a").

It implements TryFromSexp and IntoR, preserves NA in its typed vector variants, and is the representation used by structured condition data = payloads. It deliberately does not model closures, environments, language objects, S4 objects, external pointers, or ALTREP internals.

πŸ”—Matrices

With the ndarray feature:

use ndarray::Array2;

#[miniextendr]
pub fn matrix_sum(x: Array2<f64>) -> f64 {
    x.sum()
}

Character matrices/arrays are supported through explicit element-wise impls (R’s STRSXP is a vector of CHARSXP pointers, not contiguous memory, so the contiguous-copy path used for numeric arrays does not apply). Shape is preserved as R’s dim attribute with the same column-major contract as the numeric conversions, and Option<String>::None maps to NA_character_:

use ndarray::Array2;

#[miniextendr]
pub fn label_grid(x: Array2<Option<String>>) -> Array2<Option<String>> {
    x // NA-safe round-trip: None <-> NA_character_
}

Array<String, D> (without Option) mirrors Vec<String>: reading maps NA_character_ to "" (lossy); writing never produces NA.


πŸ”—Error Cases

A value that fails its Rust-side conversion raises an R error with e$kind == "conversion", e$param set to the parameter’s R name, e$rust_type set to the Rust type, and a message in R terms: '<p>' must be <expected>: <reason> (invalid '<p>' argument: <reason> for an opaque custom type without an R-facing expectation). A vector reports every failing element at once, by reason and 1-based position (NA is not allowed (elements 2, 4)). Most built-in types are checked by an R-side precondition first, which raises the same condition with its own message ('x' must have length 1); the examples below use no_preconditions (or, per argument, Unchecked<T>) so the value reaches Rust. An argument type whose error implements RConditionData contributes its own classes and fields; see ERROR_HANDLING.md for the wording table and classed conversion errors.

πŸ”—Type Mismatch

When R type doesn’t match expected Rust type:

#[miniextendr(no_preconditions)]
pub fn needs_integer(x: i32) -> i32 { x }
needs_integer(1.5)
# Error: 'x' must be a single integer: got numeric

πŸ”—NA in Non-Option

When NA is passed to non-Option parameter:

#[miniextendr(no_preconditions)]
pub fn needs_value(x: i32) -> i32 { x }
needs_value(NA_integer_)
# Error: 'x' must be a single integer: NA is not allowed

πŸ”—Coercion Failure

When coercion fails:

#[miniextendr(coerce)]
pub fn needs_int(x: i32) -> i32 { x }
needs_int(1e20)
# Error: 'x' must be a single whole number: value out of range

πŸ”—Feature-Gated Types

Many additional types are available via Cargo features:

FeatureTypes
num-bigintBigInt, BigUint
rust_decimalDecimal
uuidUuid
timeDate, Time, OffsetDateTime
ndarrayArray1, Array2, etc.
nalgebraMatrix, Vector, etc.
indexmapIndexMap, IndexSet
serdeNative R serialization
serde_jsonJSON string serialization (also enables serde)

Enable in Cargo.toml:

[dependencies]
miniextendr-api = { version = "0.1", features = ["uuid", "time"] }

πŸ”—Best Practices

  1. Use Option<T> for NA-safe parameters

    pub fn safe(x: Option<i32>) -> i32 { x.unwrap_or(0) }
  2. Use slices for read-only vector access (zero-copy)

    pub fn sum(x: &[f64]) -> f64 { x.iter().sum() }
  3. Use Vec<T> when you need to modify

    pub fn double(x: Vec<i32>) -> Vec<i32> { x.into_iter().map(|v| v*2).collect() }
  4. Enable coercion for flexible numeric APIs

    #[miniextendr(coerce)]
    pub fn flexible(x: f64) -> f64 { x }
  5. Return Option<T> to produce NA values

    pub fn maybe(x: i32) -> Option<i32> { if x > 0 { Some(x) } else { None } }

πŸ”—Named Lists

R lists with names can be accessed via NamedList, which builds a HashMap index for O(1) lookup:

use miniextendr_api::NamedList;

#[miniextendr]
pub fn get_option(config: NamedList) -> Option<String> {
    config.get::<String>("name")
}
MethodDescription
get::<T>(name)O(1) lookup by name, converting to type T
get_raw(name)O(1) lookup returning raw SEXP
contains(name)Check if a name exists
get_index::<T>(i)Positional access (no name lookup)
len() / is_empty()Size queries

When to use: List::get_named() is fine for a single lookup. Use NamedList when you need multiple lookups on the same list (O(n) build + O(1) per lookup vs O(n) per lookup).

NamedList implements TryFromSexp, so it can be used directly as a function parameter. NA and empty-string names are excluded from the index; a repeated name resolves to its first occurrence, as x[["name"]] does in R.


πŸ”—Mutable Input

There are two ways to mutate an R vector’s contents from Rust.

πŸ”—Copy-in / copy-out (Vec<T>)

Vec<T> copies the R vector on input (TryFromSexp), lets you mutate the owned copy, and copies out to a new R vector on return (IntoR). The caller’s original vector is untouched:

#[miniextendr]
pub fn double(mut x: Vec<f64>) -> Vec<f64> {
    for v in x.iter_mut() {
        *v *= 2.0;
    }
    x // copies out to a new R vector on return
}

πŸ”—In-place, zero-copy (&mut [T])

&mut [T] (and Option<&mut [T]>) borrow R’s data pointer directly and mutate the caller’s vector in place β€” no copy in, no copy out. This is faster for large vectors but only works for the contiguous atomic types (i32, f64, u8; not strings), and it mutates the object the caller passed:

#[miniextendr]
pub fn add_one_in_place(x: &mut [i32]) {
    for v in x.iter_mut() {
        *v += 1;
    }
}

Generated wrappers check native scalar and slice borrows before conversion, in both debug and release builds (#1104, #1252, #1502). Passing the same R vector to two borrowed parameters raises an error whenever either borrow is mutable. This includes &T / &mut T for i32, f64, u8, RLogical, and Rcomplex, and scalar/slice combinations. Scalar leaves require length one; slice leaves require a non-empty vector.

The check follows the selected conversion trait’s NATIVE_BORROW metadata (TryFromSexp or TryFromRoot), so type aliases retain the same checks. Vec and boxed-slice containers, nested lists, optional values, forwarding newtypes, Result<T, ()>, Missing<T>, maps, and borrowed Cow / RCow views forward the relevant metadata. Tuples describe each field by position (#2014): a borrowed field is checked against the tuple’s other fields, the fields of nested tuples, and every other parameter, while owned fields such as i32 in (i32, &mut i32) never take part. A list whose length differs from the tuple’s keeps its length error. Duplicate mutable leaves within one argument are rejected too. Every conflicting parameter pair appears in one diagnostic before any reference conversion or user mutation.

Shared/shared aliases are allowed. Optional NULLs and empty slice buffers do not overlap elements. NA scalar values still borrow their length-one buffer. Wrong native types and wrong scalar lengths retain normal conversion errors. Owned native containers such as Vec<i32> / Box<[i32]> and the independent storage used by match_arg with several_ok do not retain input borrows. Custom conversions default to no native borrow metadata; a converter retaining a native reference must describe it, and forwarding converters must preserve the delegated conversion’s metadata. Branch-dependent Either<L, R> conversions need separate handling, tracked in #1504.

The wrapper guard compares R object identities. Mutable list conversions (Vec<&mut T>, Vec<&mut [T]>, and their optional-element forms) also collect and check every raw buffer range before constructing references. This rejects distinct ALTREP objects exposing identical or partially overlapping storage. Type, length, and overlap failures are reported together with element positions. Direct callers must still keep the sources rooted and exclude borrows held outside that conversion; the list check does not supply a lifetime. Accept Vec<T> for copy-in/copy-out semantics when an input’s ownership is uncertain.


πŸ”—Borrowed vector roots

StrVec<'a>, RCow<'a, T>, Cow<'a, [T]>, Cow<'a, str> and the shared references &'a [T] and &'a str borrow R storage, so their lifetime has to come from whatever keeps that storage reachable. A SEXP is Copy and carries no lifetime, so a TryFromSexp impl lets safe code pick any lifetime, 'static included (#1427). These types convert through TryFromRoot<'a> instead, which takes a root:

  • T::try_from_root(root) with a Root<'a> from a ProtectScope;
  • ProtectedStrVec::as_strvec(), which borrows an owned character vector.

StrVec and RCow have no TryFromSexp impl, and raw TryFromSexp for the Cow types returns owned data. The shared references keep their raw impls: TryFromRoot for a reference runs that conversion, with its checks, errors and alias metadata, and shortens the result to the root.

A #[miniextendr] parameter of one of these types (bare or path-qualified; Vec<Cow<'_, str>> and Vec<Option<Cow<'_, str>>> too) is bound to the C wrapper’s argument slot through a ProtectScope that lasts until the result is converted, so it and every string or slice read from it cannot outlive the call. Functions with a view or Cow parameter run on R’s main thread. Shared references (&[T], &str, &i32, &f64, &u8, &RLogical, &Rcomplex), their Option forms and Vecs of them, nested Vecs included, are bound the same way when the function runs on the main thread. The trait-ABI shims bind all of them through a local scope, after the arity check. When the argument is an ALTREP list or character vector, the scope also keeps each element the conversion borrows (Borrowing transient ALTREP elements). A parameter that names 'static anywhere in its type is a compile error naming the fix: write '_ or elide the lifetime, or take an owned type (ProtectedStrVec, Vec, String) to keep the data.

The macro recognizes these types by name. An alias or a newtype of a view has to take a SEXP and call try_from_root in the body; one of a reference converts through TryFromSexp. On the worker thread, shared references copy before dispatch: the character forms into String storage, the native references and slices into owned values or a Vec; the worker borrows the copy. Mutable references (&mut [T], Option<&mut [T]>) and reference containers other than Vec still convert through TryFromSexp with an unconstrained lifetime (#1427). Missing, Either and Result<_, ()> of a view, and trait methods returning one, have no rooted conversion yet (#2038).


πŸ”—Known Limitations

  • Raw reference lifetimes remain unconstrained by TryFromSexp; callers must keep the source rooted and enforce borrowing rules. Generated parameters bind views, Cow and main-thread shared references to their root (Borrowed vector roots); the rest of the migration is tracked in #1427.

See GAPS.md for the full catalog of known limitations and workarounds.


πŸ”—See Also