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::ShapeView;
117//! use miniextendr_api::trait_abi::TraitView;
118//!
119//! fn calculate_area(obj: SEXP) -> f64 {
120//!     unsafe {
121//!         let view = ShapeView::try_from_sexp(obj)
122//!             .expect("object does not implement Shape");
123//!         // Call method through vtable
124//!         view.area()
125//!     }
126//! }
127//! ```
128//!
129//! [`ccall`]: crate::trait_abi::ccall
130//! [`conv`]: crate::trait_abi::conv
131//! [`mx_tag`]: crate::abi::mx_tag
132//! [`ExternalPtr`]: crate::externalptr::ExternalPtr
133//! [`TypedExternal`]: crate::externalptr::TypedExternal
134
135pub mod ccall;
136pub mod conv;
137#[doc(hidden)]
138pub mod signature;
139mod view_data;
140
141#[doc(hidden)]
142pub use view_data::TraitViewData;
143
144// Re-export commonly used items
145pub use conv::{check_arity, from_sexp, missing_to_sexp, nil, to_sexp, try_from_sexp};
146
147// Re-export for use in macro-generated View method wrappers (Approach 1, issue #345).
148// The View method wrapper calls this immediately after the vtable call to re-panic
149// with the reconstructed RCondition if the shim returned a tagged error SEXP.
150pub use crate::condition::repanic_if_rust_error;
151
152use crate::SEXP;
153use crate::abi::mx_tag;
154use std::os::raw::c_void;
155
156// region: TraitView - Trait for macro-generated View structs
157
158/// Trait for view types that can be created from SEXP via trait dispatch.
159///
160/// This trait is implemented by the macro-generated `<Trait>View` structs.
161/// It provides a common interface for:
162/// - Querying whether an object implements a trait
163/// - Creating a view from an SEXP
164///
165/// # Generated by `#[miniextendr]` on traits
166///
167/// When you write:
168/// ```ignore
169/// #[miniextendr]
170/// pub trait Counter {
171///     fn value(&self) -> i32;
172///     fn increment(&mut self);
173/// }
174/// ```
175///
176/// The generated view keeps its pointers in [`TraitViewData`]. Views made
177/// from an R external pointer root that owner until they are dropped;
178/// raw Rust-owned views instead require the caller to keep their data alive.
179///
180/// # Usage
181///
182/// ```ignore
183/// // Try to get a Counter view from an R object
184/// let view = CounterView::try_from_sexp(obj)?;
185///
186/// // Call methods through the view
187/// view.increment();
188/// let val = view.value();
189/// ```
190///
191/// # Safety
192///
193/// The `from_raw_parts` method is unsafe because:
194/// - `data` must be a valid pointer to the `RefCell` holding the concrete
195///   object (the vtable's shims borrow it for each call)
196/// - `vtable` must be a valid pointer to the trait's vtable
197/// - The vtable must remain valid for the lifetime of the view
198/// - The owner, when supplied, must keep `data` alive; otherwise the caller must
199///   keep `data` alive 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 `RefCell` holding the
211    ///   concrete object
212    /// - `vtable` must be a valid, non-null pointer to the trait's vtable
213    /// - `vtable` must remain valid for the lifetime of the view
214    /// - `owner`, when present, must be a live R object owning `data`; the
215    ///   implementation must root it for the lifetime of the view
216    /// - Without an owner, the caller must keep `data` alive for that lifetime
217    /// - A call that conflicts with one still running on the same object raises
218    ///   the `miniextendr_borrow_conflict` error (the shims borrow its `RefCell`)
219    /// - Construction, use and drop must all run on R's main thread
220    unsafe fn from_raw_parts(data: *mut c_void, vtable: *const c_void, owner: Option<SEXP>)
221    -> Self;
222
223    /// Try to create a view from an R SEXP.
224    ///
225    /// Queries the object for this trait's vtable using `mx_query`. If the
226    /// object implements the trait, returns `Some(view)`. Otherwise returns `None`.
227    ///
228    /// # Safety
229    ///
230    /// - `sexp` must be a valid R external pointer (EXTPTRSXP)
231    /// - The object must stay live through construction; the view then roots it
232    /// - A call that conflicts with one still running on the same object raises
233    ///   the `miniextendr_borrow_conflict` error (the shims borrow its `RefCell`)
234    /// - Construction, use and drop must all run on R's main thread
235    ///
236    /// # Returns
237    ///
238    /// - `Some(Self)` if the object implements the trait
239    /// - `None` if the object does not implement the trait
240    ///
241    /// # Example
242    ///
243    /// ```ignore
244    /// let counter = unsafe { CounterView::try_from_sexp(obj) }
245    ///     .expect("Object does not implement Counter");
246    /// ```
247    #[inline]
248    unsafe fn try_from_sexp(sexp: SEXP) -> Option<Self> {
249        // SAFETY: Caller guarantees sexp is valid and we're on main thread
250        unsafe {
251            // Get the vtable for this trait
252            let vtable = ccall::mx_query(sexp, Self::TAG);
253            if vtable.is_null() {
254                return None;
255            }
256
257            // Get the erased pointer (points to the wrapper struct header)
258            let raw_ptr = crate::sys::R_ExternalPtrAddr(sexp);
259            if raw_ptr.is_null() {
260                return None;
261            }
262            let erased_ptr = raw_ptr.cast::<crate::abi::mx_erased>();
263
264            // The wrapper struct layout is:
265            //   #[repr(C)]
266            //   struct ErasedObject<T> {
267            //       erased: mx_erased,  // offset 0
268            //       data: RefCell<T>,   // offset = size_of::<mx_erased>() rounded up to its alignment
269            //   }
270            // When T has stricter alignment than mx_erased, padding exists between
271            // erased and data. The correct offset is stored in the base vtable.
272            // The view never looks behind it: the producer's shims borrow the cell.
273            let base = (*erased_ptr).base;
274            let data_offset = (*base).data_offset;
275            let data = erased_ptr.cast::<u8>().add(data_offset).cast::<c_void>();
276
277            Some(Self::from_raw_parts(data, vtable, Some(sexp)))
278        }
279    }
280
281    /// Try to create a view, returning an error message on failure.
282    ///
283    /// Similar to `try_from_sexp` but returns an error string suitable for
284    /// use as an R error message if the object does not implement the trait.
285    ///
286    /// # Safety
287    ///
288    /// Same as `try_from_sexp`.
289    #[inline]
290    unsafe fn try_from_sexp_or_error(sexp: SEXP, trait_name: &str) -> Result<Self, String> {
291        // SAFETY: Delegated to try_from_sexp
292        unsafe {
293            Self::try_from_sexp(sexp)
294                .ok_or_else(|| ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("Object does not implement {0} trait",
                trait_name))
    })format!("Object does not implement {} trait", trait_name))
295        }
296    }
297}
298// endregion