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 |
πNested Collection Types
miniextendr supports converting nested collections to R lists:
| Rust Type | R Type | Notes |
|---|---|---|
Vec<Vec<T>> | list of vectors | For T: RNativeType or T = String |
Vec<Box<[T]>> | list of vectors | Boxed slices β vectors |
Vec<[T; N]> | list of vectors | Fixed arrays β vectors |
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.
π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 a specific IEEE 754 NaN with a particular bit pattern.
Critical: This is different from regular f64::NAN:
// These are DIFFERENT values
let na = NA_REAL; // R's NA
let nan = f64::NAN; // Regular IEEE NaN
// Detection requires bit comparison
fn is_na_real(value: f64) -> bool {
value.to_bits() == NA_REAL.to_bits()
}
// Regular NaN check does NOT detect NA
value.is_nan() // Returns true for both NA and NaN
Implication: When working with f64 vectors, regular NaN values pass through unchanged. Only NA_REAL is treated as NA.
π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>]> | specific NaN bit pattern (NA_real_) |
| 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()
}
π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()
}
The slice has a 'static lifetime annotation, but this is a lie for API convenience. The actual lifetime is tied to Rβs GC protection of the SEXP.
Safe patterns:
- Use slice within the function
- Copy data if you need to store it
Unsafe patterns:
- Storing the slice in a struct that outlives the function
- Returning the slice (wonβt compile anyway)
π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()
}
The Rust type currently carries a 'static lifetime for API convenience, but
that lifetime is no more real than it is for borrowed numeric slices. R can
garbage-collect a CHARSXP once it is unreachable. Use the borrow only within
the .Call; copy to String before storing it. NA_character_ becomes ""
on the plain borrowed/owned string paths, so use Option<&str> or
Option<String> 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
}π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"])),
]);
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
πType Mismatch
When R type doesnβt match expected Rust type:
#[miniextendr]
pub fn needs_integer(x: i32) -> i32 { x }needs_integer(1.5)
# Error: failed to convert parameter 'x' to i32: wrong typeπNA in Non-Option
When NA is passed to non-Option parameter:
#[miniextendr]
pub fn needs_value(x: i32) -> i32 { x }needs_value(NA_integer_)
# Error: failed to convert parameter 'x' to i32: contains NAπCoercion Failure
When coercion fails:
#[miniextendr(coerce)]
pub fn needs_int(x: i32) -> i32 { x }needs_int(1.5)
# Error: failed to coerce parameter 'x' to i32: fractional value
π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; duplicate names resolve to the last occurrence.
π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;
}
}
Aliasing foot-gun (#1104). Because
&mut [T]borrows Rβs buffer without copying, binding the same R vector to two slice parameters β e.g.f(x, x)from R β hands out two views over one buffer. That is undefined behavior in Rust whenever at least one of the two borrows is mutable: two&mut [T], or a&mut [T]paired with a&[T](a&[T]is also a zero-copy view over Rβs buffer, so a shared borrow aliasing a mutable one is UB too). When a#[miniextendr]function has two or more slice-family parameters and a pair of them could alias with one being mutable, the generated wrapper emits adebug_assert!that compares the underlying SEXP identities before conversion and raises an error naming both parameters if they share one object. (Two shared&[T]reads over one vector are sound and are not flagged. SEXP identity, not the raw data pointer, is compared, so two distinct empty vectors β which share Rβs0x1sentinel data pointer β are not a false positive.) This check is debug-build only (zero cost in release), so in a release build the aliasing call is not rejected β donβt rely on the guard as a correctness boundary; pass distinct vectors. If two arguments might reference the same vector and either mutates it, takeVec<T>(copy-in/copy-out) for at least one of them.The wrapper-level identity check compares only top-level parameter SEXPs. It does not catch a vector reached through a list element aliasing a direct slice parameter, such as
f(list(v), v)when the first parameter isVec<&mut [T]>. The list conversion separately rejects duplicates within the list, but there is no cross-site registry between nested and direct borrows. Treat those arguments as potentially aliasing and copy at least one side; #1252 tracks the residual risk.
πKnown Limitations
- SEXP slice lifetimes use
'staticfor convenience, but actual lifetime is tied to GC protection scope.
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