Skip to main content

miniextendr_api/
trait_abi.rs

1//! # Trait ABI Runtime Support
2//!
3//! This module provides runtime support for cross-package trait dispatch.
4//! It bridges between R's external pointer system and Rust's trait objects
5//! using a stable C ABI.
6//!
7//! ## Overview
8//!
9//! The trait ABI system enables:
10//!
11//! 1. **Cross-package dispatch**: Package A can call trait methods on objects
12//!    created by Package B, without compile-time knowledge of the concrete type.
13//!
14//! 2. **Type safety**: Runtime type checking via [`mx_tag`] ensures safe downcasts.
15//!
16//! 3. **Memory safety**: R's garbage collector manages object lifetime via
17//!    external pointer finalizers.
18//!
19//! ## Architecture
20//!
21//! ```text
22//! ┌─────────────────┐     ┌──────────────────┐     ┌─────────────────┐
23//! │ R Code          │     │ C-callables      │     │ Rust Runtime    │
24//! │                 │     │ (rpkg)           │     │ (miniextendr)   │
25//! │ .Call("method", │────►│ mx_query()       │────►│ vtable lookup   │
26//! │       obj, ...) │     │ mx_wrap()        │     │ method shim     │
27//! │                 │◄────│ mx_get()         │◄────│ type conversion │
28//! └─────────────────┘     └──────────────────┘     └─────────────────┘
29//! ```
30//!
31//! ## Submodules
32//!
33//! - [`ccall`]: Direct FFI wrappers for `mx_abi.rs` functions
34//! - [`conv`]: Type conversion helpers for method shims
35//!
36//! ## Integration with ExternalPtr / TypedExternal
37//!
38//! Trait ABI support is integrated with the [`ExternalPtr`] and [`TypedExternal`]
39//! system. `ExternalPtr<T>` serves as the "traitless" case (equivalent to `Any`
40//! in dynamic typing). Trait dispatch wrappers are automatically registered
41//! when you annotate `impl Trait for Type` with `#[miniextendr]`:
42//!
43//! ```ignore
44//! #[derive(ExternalPtr)]
45//! struct Circle { radius: f64 }
46//!
47//! #[miniextendr]
48//! impl Shape for Circle { /* ... */ }
49//! ```
50//!
51//! This generates the wrapper structures, vtable references, and query
52//! implementations for cross-package trait dispatch.
53//!
54//! ## Exporting traits you don't own
55//!
56//! You cannot apply `#[miniextendr]` to external traits. Instead, define a
57//! **local adapter trait** that exposes the subset you want in R, then provide
58//! a blanket impl for any type that implements the external trait:
59//!
60//! ```ignore
61//! use num_traits::Num;
62//!
63//! #[miniextendr]
64//! pub trait RNum {
65//!     fn add(&self, other: &Self) -> Self;
66//!     fn to_string(&self) -> String;
67//! }
68//!
69//! impl<T: Num + Clone + ToString> RNum for T {
70//!     fn add(&self, other: &Self) -> Self { self.clone() + other.clone() }
71//!     fn to_string(&self) -> String { ToString::to_string(self) }
72//! }
73//! ```
74//!
75//! This keeps the ABI stable while avoiding generics in the trait itself.
76//!
77//! ## Initialization
78//!
79//! Each package includes `mx_abi.rs` (from miniextendr-api) which provides the
80//! `mx_wrap`/`mx_get`/`mx_query` functions. `package_init()` (via `miniextendr_init!`)
81//! calls `mx_abi_register()` to initialize the tag symbol and register C-callables.
82//!
83//! ## Thread Safety
84//!
85//! All trait ABI operations are **main-thread only**:
86//!
87//! - R invokes `.Call` on the main thread
88//! - Method shims do not route through `with_r_thread`
89//!
90//! ## Example Usage
91//!
92//! ### Defining a trait (provider package)
93//!
94//! ```ignore
95//! // In package "shapes"
96//! #[miniextendr]
97//! pub trait Shape {
98//!     fn area(&self) -> f64;
99//!     fn perimeter(&self) -> f64;
100//! }
101//!
102//! #[derive(ExternalPtr)]
103//! pub struct Circle { radius: f64 }
104//!
105//! #[miniextendr]
106//! impl Shape for Circle {
107//!     fn area(&self) -> f64 { std::f64::consts::PI * self.radius * self.radius }
108//!     fn perimeter(&self) -> f64 { 2.0 * std::f64::consts::PI * self.radius }
109//! }
110//! ```
111//!
112//! ### Using across packages (consumer package)
113//!
114//! ```ignore
115//! // In package "geometry" (depends on "shapes")
116//! use shapes::{TAG_SHAPE, ShapeView};
117//!
118//! fn calculate_area(obj: SEXP) -> f64 {
119//!     unsafe {
120//!         let view = mx_query_as::<ShapeView>(obj, TAG_SHAPE)
121//!             .expect("object does not implement Shape");
122//!         // Call method through vtable
123//!         view.area()
124//!     }
125//! }
126//! ```
127//!
128//! [`ccall`]: crate::trait_abi::ccall
129//! [`conv`]: crate::trait_abi::conv
130//! [`mx_tag`]: crate::abi::mx_tag
131//! [`ExternalPtr`]: crate::externalptr::ExternalPtr
132//! [`TypedExternal`]: crate::externalptr::TypedExternal
133
134pub mod ccall;
135pub mod conv;
136
137// Re-export commonly used items
138pub use conv::{check_arity, extract_arg, from_sexp, nil, rf_error, to_sexp, try_from_sexp};
139
140// Re-export for use in macro-generated View method wrappers (Approach 1, issue #345).
141// The View method wrapper calls this immediately after the vtable call to re-panic
142// with the reconstructed RCondition if the shim returned a tagged error SEXP.
143pub use crate::condition::repanic_if_rust_error;
144
145use crate::abi::mx_tag;
146use crate::ffi::SEXP;
147use std::os::raw::c_void;
148
149// region: TraitView - Trait for macro-generated View structs
150
151/// Trait for view types that can be created from SEXP via trait dispatch.
152///
153/// This trait is implemented by the macro-generated `<Trait>View` structs.
154/// It provides a common interface for:
155/// - Querying whether an object implements a trait
156/// - Creating a view from an SEXP
157///
158/// # Generated by `#[miniextendr]` on traits
159///
160/// When you write:
161/// ```ignore
162/// #[miniextendr]
163/// pub trait Counter {
164///     fn value(&self) -> i32;
165///     fn increment(&mut self);
166/// }
167/// ```
168///
169/// The macro generates `CounterView` that implements `TraitView`:
170/// ```ignore
171/// impl TraitView for CounterView {
172///     const TAG: mx_tag = TAG_COUNTER;
173///
174///     unsafe fn from_raw_parts(data: *mut c_void, vtable: *const c_void) -> Self {
175///         Self {
176///             data,
177///             vtable: vtable.cast::<CounterVTable>(),
178///         }
179///     }
180/// }
181/// ```
182///
183/// # Usage
184///
185/// ```ignore
186/// // Try to get a Counter view from an R object
187/// let view = CounterView::try_from_sexp(obj)?;
188///
189/// // Call methods through the view
190/// view.increment();
191/// let val = view.value();
192/// ```
193///
194/// # Safety
195///
196/// The `from_raw_parts` method is unsafe because:
197/// - `data` must be a valid pointer to the concrete object
198/// - `vtable` must be a valid pointer to the trait's vtable
199/// - The pointers must remain valid for the lifetime of the view
200pub trait TraitView: Sized {
201    /// The type tag for this trait.
202    ///
203    /// This is a compile-time constant generated by `#[miniextendr]` on the trait.
204    const TAG: mx_tag;
205
206    /// Create a view from raw data and vtable pointers.
207    ///
208    /// # Safety
209    ///
210    /// - `data` must be a valid, non-null pointer to the concrete object
211    /// - `vtable` must be a valid, non-null pointer to the trait's vtable
212    /// - Both pointers must remain valid for the lifetime of the view
213    unsafe fn from_raw_parts(data: *mut c_void, vtable: *const c_void) -> Self;
214
215    /// Try to create a view from an R SEXP.
216    ///
217    /// Queries the object for this trait's vtable using `mx_query`. If the
218    /// object implements the trait, returns `Some(view)`. Otherwise returns `None`.
219    ///
220    /// # Safety
221    ///
222    /// - `sexp` must be a valid R external pointer (EXTPTRSXP)
223    /// - Must be called on R's main thread
224    ///
225    /// # Returns
226    ///
227    /// - `Some(Self)` if the object implements the trait
228    /// - `None` if the object does not implement the trait
229    ///
230    /// # Example
231    ///
232    /// ```ignore
233    /// let counter = unsafe { CounterView::try_from_sexp(obj) }
234    ///     .expect("Object does not implement Counter");
235    /// ```
236    #[inline]
237    unsafe fn try_from_sexp(sexp: SEXP) -> Option<Self> {
238        // SAFETY: Caller guarantees sexp is valid and we're on main thread
239        unsafe {
240            // Get the vtable for this trait
241            let vtable = ccall::mx_query(sexp, Self::TAG);
242            if vtable.is_null() {
243                return None;
244            }
245
246            // Get the erased pointer (points to the wrapper struct header)
247            let raw_ptr = crate::ffi::R_ExternalPtrAddr(sexp);
248            if raw_ptr.is_null() {
249                return None;
250            }
251            let erased_ptr = raw_ptr.cast::<crate::abi::mx_erased>();
252
253            // The wrapper struct layout is:
254            //   #[repr(C)]
255            //   struct __MxWrapper<T> {
256            //       erased: mx_erased,  // offset 0
257            //       data: T,            // offset = size_of::<mx_erased>() rounded up to align_of::<T>()
258            //   }
259            // When T has stricter alignment than mx_erased, padding exists between
260            // erased and data. The correct offset is stored in the base vtable.
261            let base = (*erased_ptr).base;
262            let data_offset = (*base).data_offset;
263            let data = erased_ptr.cast::<u8>().add(data_offset).cast::<c_void>();
264
265            Some(Self::from_raw_parts(data, vtable))
266        }
267    }
268
269    /// Try to create a view, returning an error message on failure.
270    ///
271    /// Similar to `try_from_sexp` but returns an error string suitable for
272    /// use as an R error message if the object does not implement the trait.
273    ///
274    /// # Safety
275    ///
276    /// Same as `try_from_sexp`.
277    #[inline]
278    unsafe fn try_from_sexp_or_error(sexp: SEXP, trait_name: &str) -> Result<Self, String> {
279        // SAFETY: Delegated to try_from_sexp
280        unsafe {
281            Self::try_from_sexp(sexp)
282                .ok_or_else(|| format!("Object does not implement {} trait", trait_name))
283        }
284    }
285}
286// endregion