Type Conversions in miniextendr
This guide documents how miniextendr converts between R and Rust types, including NA handling, coercion rules, and edge cases.
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 Type | Rust Type | Notes |
|---|---|---|
integer (length 1) | i32 | NA β panic |
numeric (length 1) | f64 | NA preserved as NA_REAL |
logical (length 1) | bool | NA β panic |
character (length 1) | String, &str | NA β "" (lossy); use Option to preserve NA |
raw (length 1) | u8 | No NA in raw |
complex (length 1) | Rcomplex | Has real/imag NA |
πVector Types
| R Type | Rust Type | Notes |
|---|---|---|
integer | Vec<i32>, &[i32] | NA = i32::MIN |
numeric | Vec<f64>, &[f64] | NA = special bit pattern |
logical | Vec<bool> | NA β error; use Vec<Option<bool>> to preserve it |
character | Vec<String> | NA β "" (lossy); use Vec<Option<String>> to preserve it |
raw | Vec<u8>, &[u8] | No NA |
list | Various | See 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 Type | R Type | Notes |
|---|---|---|
Vec<Vec<T>> | list of vectors | Any T with Vec<T>: IntoR; each element is what Vec<T> gives |
Vec<Option<Vec<T>>> / Vec<Option<Box<[T]>>> | list of vectors | As Vec<Vec<T>>; None β NULL |
Vec<&[T]> / Vec<Option<&[T]>> | list of vectors | Any T with &[T]: IntoR; slices are copied |
Vec<Box<[T]>> | list of vectors | Boxed slices β vectors (any T with Vec<T>: IntoR) |
Vec<[T; N]> | list of vectors | Fixed arrays β vectors (any T with Vec<T>: IntoR) |
Vec<HashSet<T>> | list of vectors | Sets β unordered vectors |
Vec<BTreeSet<T>> | list of vectors | Sets β 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 Type | Rust Type | Input NA | Input NULL | Output None |
|---|---|---|---|---|
integer | Option<i32> | None | None | NA_integer_ |
numeric | Option<f64> | None | None | NA_real_ |
logical | Option<bool> | None | None | NA (logical) |
character | Option<String> | None | None | NA_character_ |
character | Option<&str> | None | None | NA_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 Type | ALTREP Handling |
|---|---|
Vec<i32>, &[f64], etc. | Auto-materialized during conversion |
SEXP | Auto-materialized via ensure_materialized |
AltrepSexp | Accepted 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:
-2147483647to2147483647 i32::MINis 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= 1FALSE= 0NA=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:
Coerce<R>- Infallible (always succeeds)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)
| From | To | Notes |
|---|---|---|
i32 | f64 | Widening (no precision loss) |
i32 | i32 | Identity |
f64 | f64 | Identity |
Option<T> | T | None β NA value |
πFallible Coercions (TryCoerce)
| From | To | Fails When |
|---|---|---|
f64 | i32 | NaN, infinity, fractional, overflow |
i32 | u32 | Negative value |
i32 | NonZeroI32 | Zero value |
f64 | u64 | Negative, 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 Type | Rust Type | NA sentinel mapped to None |
|---|---|---|
| INTSXP | Vec<Option<i32>>, Box<[Option<i32>]> | i32::MIN (NA_integer_) |
| REALSXP | Vec<Option<f64>>, Box<[Option<f64>]> | NA_real_, including computed NAs (a NaN with low word 1954, as R_IsNA checks) |
| LGLSXP | Vec<Option<bool>>, Box<[Option<bool>]> | i32::MIN (NA_logical_) |
| STRSXP | Vec<Option<String>>, Box<[Option<String>]> | R_NaString (NA_character_) |
Coerced numeric types also support Vec<Option<T>> (accepts INTSXP, REALSXP, RAWSXP, LGLSXP):
| Rust Type | Accepted R Types | NA handling |
|---|---|---|
Vec<Option<i64>> | INTSXP, REALSXP, RAWSXP, LGLSXP | NA_integer_/NA_real_ β None |
Vec<Option<u64>> | INTSXP, REALSXP, RAWSXP, LGLSXP | NA_integer_/NA_real_ β None |
Vec<Option<i8>>, Vec<Option<i16>>, etc. | INTSXP, REALSXP, RAWSXP, LGLSXP | NA β None |
Output (Rust β R):
| Rust Type | R Output | None becomes |
|---|---|---|
Vec<Option<i32>> | INTSXP | NA_integer_ |
Vec<Option<f64>> | REALSXP | NA_real_ |
Vec<Option<bool>> | LGLSXP | NA (logical) |
Vec<Option<String>> | STRSXP | NA_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| Input | Result |
|---|---|
| double | as is; NA_real_ β None, NaN stays Some(NaN) |
| integer, logical | widened to f64; NA β None |
| character | Rβ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 |
| factor | each 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| Input | Result |
|---|---|
| character | as is; NA_character_ β None, "NA" and "" stay values |
| integer, double | Rβ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:
new()allocates Rust data on heap- Pointer stored in Rβs external pointer SEXP
- Rβs GC tracks the SEXP
- When SEXP is collected, Rust
Dropruns - 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:
| Feature | Types |
|---|---|
num-bigint | BigInt, BigUint |
rust_decimal | Decimal |
uuid | Uuid |
time | Date, Time, OffsetDateTime |
ndarray | Array1, Array2, etc. |
nalgebra | Matrix, Vector, etc. |
indexmap | IndexMap, IndexSet |
serde | Native R serialization |
serde_json | JSON string serialization (also enables serde) |
Enable in Cargo.toml:
[dependencies]
miniextendr-api = { version = "0.1", features = ["uuid", "time"] }
πBest Practices
-
Use
Option<T>for NA-safe parameterspub fn safe(x: Option<i32>) -> i32 { x.unwrap_or(0) } -
Use slices for read-only vector access (zero-copy)
pub fn sum(x: &[f64]) -> f64 { x.iter().sum() } -
Use
Vec<T>when you need to modifypub fn double(x: Vec<i32>) -> Vec<i32> { x.into_iter().map(|v| v*2).collect() } -
Enable coercion for flexible numeric APIs
#[miniextendr(coerce)] pub fn flexible(x: f64) -> f64 { x } -
Return
Option<T>to produce NA valuespub 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")
}| Method | Description |
|---|---|
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 aRoot<'a>from aProtectScope;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,Cowand 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
- COERCE.md β Type coercion trait design
- R_COERCE.md β
as.<class>()coercion methods - CONVERSION_MATRIX.md β R type x Rust type behavior reference
- FEATURES.md β Feature-gated types (ndarray, nalgebra, uuid, time, etc.)
- GC_PROTECT.md β RAII-based GC protection for SEXP lifetimes
- ERROR_HANDLING.md β Type conversion error messages