Reference page
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 β panic |
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<i32> | TRUE=1, FALSE=0, NA=i32::MIN |
character | Vec<String> | NA β panic |
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)
| R Type | Rust Type | NA Handling |
|---|---|---|
integer | Option<i32> | NA β None |
numeric | Option<f64> | NA β None |
logical | Option<bool> | NA β None |
character | Option<String> | NA β None |
π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).
miniextendr converts string NA to panic by default. 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:
#[miniextendr]
pub fn maybe_value(x: i32) -> Option<i32> {
if x > 0 { Some(x) } else { None }
}maybe_value(5) # 5
maybe_value(-1) # NAπ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
π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
}π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
}
π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
R interns all strings (CHARSXP). When you get a &str from R:
#[miniextendr]
pub fn process_string(s: &str) -> String {
// s is valid for the entire R session (interned)
s.to_uppercase()
}
The &'static str lifetime is actually correct here because R never garbage collects interned strings.
π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: The pointer can be safely accessed from any thread, but R API calls must happen on the main 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
Data frames are lists with special attributes. Access columns:
#[miniextendr]
pub fn get_column(df: List, name: &str) -> Vec<f64> {
// df[name] returns the column
// Convert as needed
}πMatrices
With the ndarray feature:
use ndarray::Array2;
#[miniextendr]
pub fn matrix_sum(x: Array2<f64>) -> f64 {
x.sum()
}
π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 | JSON conversion |
serde_r | Native R serialization |
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.
πSafe Mutable Input
R vectors are copy-on-write, so &mut [T] is not supported in #[miniextendr] functions (rejected at compile time with a helpful error). Use Vec<T> for copy-in/copy-out mutation:
#[miniextendr]
pub fn double_in_place(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
}
Vec<T> copies the R vector on input (TryFromSexp), allows mutation, and copies out to a new R vector on return (IntoR).
πKnown Limitations
- Mutable slice parameters (
&mut [T]) are rejected at compile time. Accept&[T]and return a newVec<T>, or acceptVec<T>directly. - String matrices (
ndarray::Array<String, Ix2>) are not directly convertible because Rβs STRSXP is not contiguous memory. UseVec<Vec<String>>as an intermediary. - 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
- AS_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