Reference page
Trait ABI
The trait ABI lets R (and other packages) call Rust trait methods without knowing the concrete Rust type at compile time. It does this by storing a tiny "header + vtable" next to the object and using R external pointers to carry it around.
The trait ABI lets R (and other packages) call Rust trait methods without knowing the concrete Rust type at compile time. It does this by storing a tiny βheader + vtableβ next to the object and using R external pointers to carry it around.
πKey Concepts
πType Tags (mx_tag)
A 128-bit identifier for a trait or concrete type, generated from the fully-qualified Rust path via hashing.
// Generated at compile time from "crate::Counter"
const TAG_COUNTER: mx_tag = mx_tag_from_path("crate::Counter");πErased Header (mx_erased)
A tiny header that points to a base vtable. This is the common prefix of all type-erased objects:
typedef struct mx_erased {
const mx_base_vtable *base;
} mx_erased;πBase Vtable (mx_base_vtable)
Every erased object has a base vtable with:
drop: Destructor for cleanup when R garbage collects the objectconcrete_tag: Type tag for downcasting to the concrete typequery: Function that returns trait vtables by tag
typedef struct mx_base_vtable {
void (*drop)(mx_erased *ptr);
mx_tag concrete_tag;
const void *(*query)(mx_erased *ptr, mx_tag trait_tag);
} mx_base_vtable;πTrait Vtables
Each trait gets its own vtable with function pointers for each method. All methods use a uniform ABI:
typedef SEXP (*mx_meth)(void *data, int argc, const SEXP *argv);πHow It Works
π1. Compile Time
The #[miniextendr] macro on a trait generates:
- Tag constant (
TAG_COUNTER) - Vtable struct (
CounterVTable) - View struct (
CounterView) for calling methods - Method shims that convert between R and Rust types
#[miniextendr]
pub trait Counter {
fn value(&self) -> i32;
fn increment(&mut self);
}π2. Object Creation
When you create an object for trait dispatch, a wrapper struct is allocated with mx_erased at the front and the real Rust data after it:
#[repr(C)]
struct __MxWrapperMyCounter {
erased: mx_erased, // Must be first
data: MyCounter,
}π3. Packaging with mx_wrap
mx_wrap turns *mut mx_erased into an R external pointer (EXTPTRSXP). The finalizer uses the base vtableβs drop function to clean up when garbage collected.
// In a constructor
let wrapper = Box::new(__MxWrapperMyCounter {
erased: mx_erased { base: &__MX_BASE_VTABLE_MYCOUNTER },
data: my_counter,
});
let sexp = unsafe { mx_wrap(Box::into_raw(wrapper) as *mut mx_erased) };π4. Dispatch with mx_query
When you want to call a trait method, mx_query(sexp, TAG_TRAIT) asks the object for the trait vtable:
// Get the Counter vtable for this object
let vtable = mx_query(sexp, TAG_COUNTER);
if vtable.is_null() {
// Object doesn't implement Counter
}π5. Method Call
If the vtable exists, the generated shim converts R args, calls the Rust method, and converts the result back to R:
// CounterView wraps the sexp and vtable for ergonomic calls
let view = CounterView::try_from_sexp(sexp)?;
let value = view.value(); // Calls through vtable
view.increment();πMultiple Traits Per Type
A single type can implement multiple traits. All are automatically registered via #[miniextendr]:
#[miniextendr]
pub trait Counter {
fn value(&self) -> i32;
fn increment(&mut self);
}
#[miniextendr]
pub trait Resettable {
fn reset(&mut self);
}
pub struct MyCounter { value: i32 }
#[miniextendr]
impl Counter for MyCounter {
fn value(&self) -> i32 { self.value }
fn increment(&mut self) { self.value += 1; }
}
#[miniextendr]
impl Resettable for MyCounter {
fn reset(&mut self) { self.value = 0; }
}
// Registration is automatic via #[miniextendr].
The framework groups trait impls by concrete type. MyCounter gets a single wrapper with a query function that handles both traits:
unsafe extern "C" fn __mx_query_mycounter(
_ptr: *mut mx_erased,
trait_tag: mx_tag,
) -> *const c_void {
if trait_tag == TAG_COUNTER {
return &__VTABLE_COUNTER_FOR_MYCOUNTER as *const _;
}
if trait_tag == TAG_RESETTABLE {
return &__VTABLE_RESETTABLE_FOR_MYCOUNTER as *const _;
}
std::ptr::null()
}
From R or consumer packages, both views work on the same object:
// Get Counter view
let counter_view = CounterView::try_from_sexp(sexp)?;
counter_view.increment();
// Get Resettable view from same object
let reset_view = ResettableView::try_from_sexp(sexp)?;
reset_view.reset();πCross-Package Usage
The trait ABI enables cross-package dispatch where:
- Producer package: Defines traits and concrete types
- Consumer package: Uses trait views without knowing concrete types
πArchitecture
Every package (rpkg, producer.pkg, consumer.pkg, ...)
ββββββββββββββββββββββββββββββββββββββββββββββββββββ
mx_abi.rs (compiled into each package's staticlib via miniextendr-api)
mx_abi_register() β called by package_init() via miniextendr_init!
init_tag() β Rf_install("miniextendr::mx_erased")
R_RegisterCCallable(...) β registers for cross-package use
mx_wrap() / mx_get() / mx_query() β linked directly via extern "C"
- Each package includes
mx_abi.rsfrom miniextendr-api (compiled into the Rust staticlib) - Rust calls
mx_wrap/mx_get/mx_querydirectly (noR_GetCCallableindirection) - Cross-package dispatch works because all packages share the same
Rf_install("miniextendr::mx_erased")tag symbol
πRequirements
- Use
miniextendr_init!in lib.rs: This callspackage_init()which includesmx_abi_register()automatically.
// lib.rs
miniextendr_api::miniextendr_init!(mypkg);
-
Main thread only: All trait ABI operations must happen on Rβs main thread (which is where
.Callruns). -
Null checks: If a type doesnβt implement a trait,
mx_queryreturns null. The generatedTraitView::try_from_sexphandles this gracefully.
πSource Files
miniextendr-api/src/trait_abi/mod.rs- Core types and traitsminiextendr-api/src/trait_abi/ccall.rs- C-callable wrappers and initminiextendr-api/src/abi.rs- FFI-compatible struct definitionsminiextendr-api/src/mx_abi.rs- Rust implementation of mx_wrap/mx_get/mx_queryrpkg/inst/include/mx_abi.h- C header for consumer packages