Adapter Traits: Exporting External Traits to R
This guide explains how to expose Rust traits you don't own (from external crates) to R via miniextendr's trait ABI.
This guide explains how to expose Rust traits you don’t own (from external crates) to R via miniextendr’s trait ABI.
🔗The Problem
miniextendr’s #[miniextendr] attribute must be applied to trait definitions
to generate the ABI metadata. You cannot retroactively annotate traits from
external crates:
// This WON'T work - can't add attributes to external traits
#[miniextendr] // ERROR: can't modify external crate
use num_traits::Num;🔗Solution: Adapter Traits
Create a local wrapper trait that mirrors the methods you want to expose, then implement it via blanket impl for types implementing the external trait.
🔗Basic Pattern
use miniextendr_api::prelude::*;
use num_traits::Num;
// 1. Define your local adapter trait
#[miniextendr]
pub trait RNum {
fn add(&self, other: &Self) -> Self;
fn mul(&self, other: &Self) -> Self;
fn is_zero(&self) -> bool;
}
// 2. Blanket impl for all types implementing the external trait
impl<T> RNum for T
where
T: Num + Clone,
{
fn add(&self, other: &Self) -> Self {
self.clone() + other.clone()
}
fn mul(&self, other: &Self) -> Self {
self.clone() * other.clone()
}
fn is_zero(&self) -> bool {
Num::is_zero(self)
}
}
// 3. Implement the trait for your concrete type
#[derive(ExternalPtr)]
struct MyNumber {
value: i64,
}
#[miniextendr]
impl RNum for MyNumber {
// Uses the blanket impl above
}
// 4. Registration is automatic - #[miniextendr] items are registered via
// linkme distributed slices. No manual module declaration needed.🔗Why This Works
- You own the adapter trait -
#[miniextendr]can generate ABI metadata - Blanket impl provides functionality - Any type implementing
NumgetsRNum - Concrete impl triggers codegen -
impl RNum for MyNumbergenerates the vtable
🔗Built-in Adapter Traits
miniextendr-api provides ready-to-use adapter traits for common std library traits.
These have blanket implementations so you just need to export them for your types:
| Trait | Wraps | R methods | Rust methods |
|---|---|---|---|
RDebug | Debug | debug_str(), debug_str_pretty() | same |
RDisplay | Display | as_r_string() | same |
RFromStr | FromStr | from_str(s) | r_from_str(s) -> Option<Self> |
RHash | Hash | hash() | r_hash() -> String |
ROrd | Ord | cmp(other) | r_cmp(&self, other) -> i32 |
RPartialOrd | PartialOrd | partial_cmp(other) | r_partial_cmp(&self, other) -> Option<i32> |
RError | Error | error_message(), error_chain(), error_chain_length() | same |
RClone | Clone | clone() | r_clone() -> Self |
RCopy | Copy | copy(), is_copy() | same |
RDefault | Default | default() | r_default() -> Self |
🔗Rust names differ from the R names
R calls these methods by the std names, but in Rust a method whose name std
already uses carries an r_ prefix (r_clone, r_cmp, r_len, …) and
keeps the std name for R through #[miniextendr(r_name = "...")]. The blanket
impls cover every Clone, Default, Hash, Ord, PartialOrd and FromStr
type, and RToVec covers arrays and boxed slices. With std’s names, importing
one of these traits (by name or through use miniextendr_api::*;) would make
the module’s own v.clone(), String::default() or a.cmp(&b) ambiguous
(E0034), and [T; N]::len() would return i64. With the prefix, an import
never shadows a std method.
Both empty and populated #[miniextendr] impl blocks inherit R names and
skipped methods from the trait definition. An explicit impl-level r_name
overrides the inherited name. The trait can be named by its defining module,
its crate-root re-export, or an imported name, including a renamed import.
🔗Generic / Associated-Type Adapter Traits
These traits have generic parameters or associated types, and are supported via concrete vtable shim generation at the impl site:
| Trait | Wraps | R methods | Notes |
|---|---|---|---|
RIterator | Iterator (associated type Item) | next_item(), size_hint(), count(), collect_n(), skip(), nth() | Rust next is next_item in R via r_name; next and nth return Nullable<Item>, NULL in R at the end; size_hint returns list(lower, upper) (NA upper bound when unknown) |
RExtend<T> | Extend | extend_from_vec(), len(), is_empty() | Rust r_len / r_is_empty; extend_from_slice skipped |
RFromIter<T> | FromIterator | from_vec(items) | |
RToVec<T> | (iterable collections) | to_vec(), len(), is_empty() | Rust r_to_vec / r_len / r_is_empty |
RMakeIter<T, I> | (iterator factory) | make_iter() | I must implement RIterator |
RIterator keeps the names size_hint, count, skip and nth: its only blanket impl is
on RefCell<I>, which has no such methods and is not an iterator, so an import
leaves Iterator’s methods alone.
Usage:
use miniextendr_api::prelude::*;
use miniextendr_api::{RClone, RDebug};
#[derive(Debug, Clone, ExternalPtr)]
struct MyData { value: i32 }
// Expose Debug and Clone to R: `x$RDebug$debug_str()`, `x$RClone$clone()`.
#[miniextendr]
impl RDebug for MyData {}
#[miniextendr]
impl RClone for MyData {}
// With the traits imported, `.clone()` is still `Clone::clone`; the adapter
// method is `r_clone()`.
fn copies(data: &MyData) -> (MyData, MyData) {
(data.clone(), data.r_clone())
}
// Registration is automatic via #[miniextendr].🔗Trait ABI Constraints
When designing adapter traits, keep these limitations in mind:
| Feature | Supported? | Notes |
|---|---|---|
| Generic parameters on trait | Yes | trait Foo<T> - concrete shims generated at impl site |
| Generic methods | No | fn bar<T>() not allowed |
| Async methods | No | async fn not allowed |
| Associated types | Yes | type Item - resolved to concrete type at impl site |
| Self by value | Yes | fn consume(self) works |
| &self / &mut self | Yes | Standard receivers |
| Static methods | Yes | But don’t go through vtable |
#[miniextendr(skip)] | Yes | Exclude specific methods from R wrappers |
#[miniextendr(r_name = "...")] | Yes | Rename a method in R (e.g., next → next_item) |
Method arguments and return types must implement:
TryFromSexpfor parameters (R → Rust conversion)IntoRfor return values (Rust → R conversion)
🔗Example: Using the Built-in RIterator Trait
The built-in RIterator trait has an associated type Item and uses
#[miniextendr(skip)] and #[miniextendr(r_name = "...")] to customize
its R interface:
use miniextendr_api::prelude::*;
#[derive(ExternalPtr)]
struct CountUp {
current: i32,
max: i32,
}
// RIterator has an associated type Item, resolved to i32 here.
// Its declaration supplies the R name `next_item` for Rust's `next`.
#[miniextendr]
impl miniextendr_api::adapter_traits::RIterator for CountUp {
type Item = i32;
fn next(&self) -> Nullable<i32> { /* ... */ }
fn size_hint(&self) -> (i64, Option<i64>) { /* ... */ }
fn count(&self) -> i64 { /* ... */ }
fn collect_n(&self, n: i32) -> Vec<i32> { /* ... */ }
fn skip(&self, n: i32) -> i32 { /* ... */ }
fn nth(&self, n: i32) -> Nullable<i32> { /* ... */ }
}
In R, the method is called next_item() (not next(), which would shadow
R’s built-in), and size_hint() returns list(lower, upper). next_item()
and nth() return NULL once the iterator is exhausted, whatever the item
type. An Option<i32> would give NA_integer_, which a real item could also
be; Nullable<T> gives NULL for None.
🔗Alternative: Newtype Wrapper
When the external trait has complex signatures or you need explicit conversions, use a newtype wrapper instead of blanket impls:
use rust_decimal::Decimal;
// Newtype wrapper
#[derive(ExternalPtr)]
pub struct RDecimal(Decimal);
impl RDecimal {
pub fn new(s: &str) -> Result<Self, String> {
Decimal::from_str(s)
.map(RDecimal)
.map_err(|e| e.to_string())
}
pub fn inner(&self) -> &Decimal {
&self.0
}
}
// Define trait on the newtype
#[miniextendr]
pub trait DecimalOps {
fn add(&self, other: &RDecimal) -> RDecimal;
fn to_string(&self) -> String;
}
#[miniextendr]
impl DecimalOps for RDecimal {
fn add(&self, other: &RDecimal) -> RDecimal {
RDecimal(self.0 + other.0)
}
fn to_string(&self) -> String {
self.0.to_string()
}
}🔗When to Use Newtype vs Blanket Impl
| Approach | Use When |
|---|---|
| Blanket impl | External trait is simple, no associated types |
| Newtype | Need explicit conversions, complex signatures, or want isolation |
🔗Cross-Package Trait Dispatch
Adapter traits work with miniextendr’s cross-package trait ABI:
Producer package (defines trait + impl):
// producer/src/lib.rs
#[miniextendr]
pub trait RNum { ... }
#[derive(ExternalPtr)]
pub struct BigInt { ... }
#[miniextendr]
impl RNum for BigInt { ... }
// Registration is automatic via #[miniextendr].
Consumer package (uses trait):
// consumer/src/lib.rs
use producer::RNum;
#[miniextendr]
fn double_it(x: &dyn RNum) -> impl RNum {
x.add(x) // Uses trait method via vtable
}
The consumer calls RNum::add through the vtable, allowing new implementations
to be added in other packages without recompiling the consumer.
🔗Complete Example
See tests/cross-package/ for a working example of:
producer.pkg: DefinesCountertrait andSimpleCounterimplconsumer.pkg: UsesCountertrait objects from producer
🔗Tips
- Keep adapter traits small - Only expose methods you actually need in R
- Use concrete types - Avoid generics; use specific types like
i32,f64 - Document the mapping - Explain how R values map to Rust types
- Handle errors explicitly - Return
Result<T, String>for fallible operations - Consider serialization - For complex external types,
character(JSON/string) often works
🔗Trait-Provided Impl Expansion (TPIE)
When an adapter trait has a blanket impl, you can write an empty #[miniextendr] impl
block and the trait’s default implementations are automatically used:
use miniextendr_api::prelude::*;
#[derive(Debug, Clone, ExternalPtr)]
struct MyData { value: i32 }
// Empty body - uses blanket impl from RDebug
#[miniextendr]
impl miniextendr_api::adapter_traits::RDebug for MyData {}
// Empty body - uses blanket impl from RClone
#[miniextendr]
impl miniextendr_api::adapter_traits::RClone for MyData {}
The empty body is expanded using metadata exported with the trait. A bare
imported name such as RClone and the crate-root path
miniextendr_api::RClone work too; companion vtable items need no imports.
User-defined traits can be re-exported or renamed in the same way. The
original declaration’s name still determines the R trait namespace.
This generates all the R wrappers, vtable registration, and C-callable shims without requiring you to re-implement the methods.
🔗Method Attributes
🔗#[miniextendr(skip)]
Exclude a trait method from the R interface. The method exists in Rust but gets no R wrapper:
#[miniextendr]
pub trait MyTrait {
fn useful_method(&self) -> i32;
#[miniextendr(skip)]
fn internal_only(&self) -> usize; // Not exposed to R
}🔗#[miniextendr(r_name = "...")]
Rename a method in R to avoid conflicts with R keywords or built-ins, or give a Rust method that avoids a std name its std name in R:
#[miniextendr]
pub trait RIterator {
#[miniextendr(r_name = "next_item")]
fn next(&self) -> Nullable<Self::Item>; // Called next_item() in R
}🔗See Also
- SAFETY.md - Thread safety for trait dispatch
- ENTRYPOINT.md - Trait ABI initialization requirements
miniextendr-api/src/trait_abi/- Trait ABI implementation