Skip to main content

miniextendr_api/
abi.rs

1//! # ABI Types for Cross-Package Trait Dispatch
2//!
3//! This module defines the stable, C-compatible ABI types used for cross-package
4//! trait dispatch in miniextendr. These types enable R packages written with
5//! miniextendr to share trait-based interfaces across package boundaries.
6//!
7//! ## Design Principles
8//!
9//! 1. **Stability**: All types are `#[repr(C)]` and append-only. Fields are never
10//!    removed or reordered, only new fields may be added at the end.
11//!
12//! 2. **C Compatibility**: Types use only C-compatible primitives to ensure
13//!    consistent layout across different Rust versions and compilation units.
14//!
15//! 3. **Type Safety**: Runtime type checking via [`mx_tag`] ensures type-safe
16//!    downcasts even across package boundaries.
17//!
18//! ## Type Hierarchy
19//!
20//! ```text
21//! mx_erased (type-erased object)
22//!     │
23//!     └── points to → mx_base_vtable (base vtable)
24//!                         │
25//!                         ├── drop: fn pointer
26//!                         ├── concrete_tag: mx_tag
27//!                         └── query: fn pointer → interface vtables
28//! ```
29//!
30//! ## Usage Flow
31//!
32//! 1. Trait is declared with `#[miniextendr]`, impl uses `#[miniextendr] impl Trait for Type`
33//! 2. Type derives `ExternalPtr` (generates MxWrapper, base vtable, wrap fn)
34//! 3. Trait dispatch entries self-register via `distributed_slice`
35//! 4. Constructor returns `*mut mx_erased`
36//! 5. `mx_wrap()` wraps pointer in R's EXTPTRSXP
37//! 6. Method calls use `universal_query()` to get interface vtable
38//! 7. Vtable entry receives `(data, argc, argv)` and returns SEXP
39//!
40//! ## Example
41//!
42//! ```ignore
43//! // Define trait (generates TAG_Foo, FooVTable, FooView)
44//! #[miniextendr]
45//! trait Foo {
46//!     fn len(&self) -> usize;
47//! }
48//!
49//! // Implement for concrete type (self-registers via distributed_slice)
50//! #[miniextendr]
51//! impl Foo for MyType { /* ... */ }
52//! ```
53//!
54//! ## Thread Safety
55//!
56//! All ABI operations are main-thread only. R invokes `.Call` on the main thread,
57//! and method shims do not route through `with_r_thread`.
58
59use crate::SEXP;
60use std::cell::RefCell;
61use std::os::raw::c_void;
62
63/// Type tag for runtime type identification.
64///
65/// A 128-bit identifier split into two 64-bit halves for C compatibility.
66/// Used to identify concrete types and trait interfaces at runtime.
67///
68/// ## Generation
69///
70/// Tags should be generated as compile-time constants, typically using
71/// a hash of the fully-qualified type/trait path. The `#[miniextendr]`
72/// attribute macro handles this automatically.
73///
74/// ## Comparison
75///
76/// Tags are compared by value equality of both `lo` and `hi` fields.
77///
78/// ## Layout Guarantee
79///
80/// This type is `#[repr(C)]` and its layout is frozen. Fields will never
81/// be reordered, and new fields will only be appended.
82#[repr(C)]
83#[derive(#[automatically_derived]
impl ::core::fmt::Debug for mx_tag {
    #[inline]
    fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
        ::core::fmt::Formatter::debug_struct_field2_finish(f, "mx_tag", "lo",
            &self.lo, "hi", &&self.hi)
    }
}Debug, #[automatically_derived]
#[doc(hidden)]
unsafe impl ::core::clone::TrivialClone for mx_tag { }
#[automatically_derived]
impl ::core::clone::Clone for mx_tag {
    #[inline]
    fn clone(&self) -> Self {
        let _: ::core::clone::AssertParamIsClone<u64>;
        *self
    }
}Clone, #[automatically_derived]
impl ::core::marker::Copy for mx_tag { }Copy, #[automatically_derived]
impl ::core::marker::StructuralPartialEq for mx_tag { }
#[automatically_derived]
impl ::core::cmp::PartialEq for mx_tag {
    #[inline]
    fn eq(&self, other: &Self) -> bool {
        self.lo == other.lo && self.hi == other.hi
    }
}PartialEq, #[automatically_derived]
impl ::core::cmp::Eq for mx_tag {
    #[inline]
    #[doc(hidden)]
    #[coverage(off)]
    fn assert_fields_are_eq(&self) {
        let _: ::core::cmp::AssertParamIsEq<u64>;
    }
}Eq, #[automatically_derived]
impl ::core::hash::Hash for mx_tag {
    #[inline]
    fn hash<__H: ::core::hash::Hasher>(&self, state: &mut __H) {
        ::core::hash::Hash::hash(&self.lo, state);
        ::core::hash::Hash::hash(&self.hi, state)
    }
}Hash)]
84pub struct mx_tag {
85    /// Lower 64 bits of the type tag.
86    pub lo: u64,
87    /// Upper 64 bits of the type tag.
88    pub hi: u64,
89}
90
91impl mx_tag {
92    /// Create a new type tag from two 64-bit values.
93    ///
94    /// # Arguments
95    ///
96    /// * `lo` - Lower 64 bits
97    /// * `hi` - Upper 64 bits
98    ///
99    /// # Example
100    ///
101    /// ```ignore
102    /// const MY_TAG: mx_tag = mx_tag::new(0x1234_5678_9abc_def0, 0xfed_cba9_8765_4321);
103    /// ```
104    #[inline]
105    pub const fn new(lo: u64, hi: u64) -> Self {
106        Self { lo, hi }
107    }
108}
109
110/// Method signature for trait vtable entries.
111///
112/// All trait methods are erased to this uniform signature:
113/// - `data`: Pointer to the concrete object data
114/// - `argc`: Number of arguments in `argv`
115/// - `argv`: Array of SEXP arguments from R
116/// - Returns: SEXP result to R
117///
118/// ## Argument Handling
119///
120/// The shim generated by `#[miniextendr]` on a trait is responsible for:
121/// 1. Checking `argc` matches expected arity
122/// 2. Converting each `argv[i]` via [`TryFromSexp`]
123/// 3. Calling the actual method
124/// 4. Converting the result via [`IntoR`]
125/// 5. Catching panics and converting to R errors
126///
127/// ## Safety
128///
129/// This function pointer is `unsafe` because:
130/// - `data` must point to valid, properly-aligned data of the expected type
131/// - `argv` must point to `argc` valid SEXP values, each rooted by the caller
132///   for the whole call (the shim's conversions and the method body allocate);
133///   the generated View protects each argument before converting the next
134/// - The returned SEXP is unprotected; the caller roots it before allocating
135/// - Must be called on R's main thread
136///
137/// [`TryFromSexp`]: crate::TryFromSexp
138/// [`IntoR`]: crate::IntoR
139#[allow(non_camel_case_types)]
140pub type mx_meth = unsafe extern "C" fn(data: *mut c_void, argc: i32, argv: *const SEXP) -> SEXP;
141
142/// A trait method whose explicit borrowed receiver needs its R owner.
143///
144/// In addition to [`mx_meth`]'s contract, `owner` must be the caller-rooted
145/// external pointer holding `data`, with initialized sidecar slots. Only
146/// owner-receiver methods use this signature; ordinary receivers keep `mx_meth`.
147#[allow(non_camel_case_types)]
148pub type mx_owner_meth =
149    unsafe extern "C" fn(data: *mut c_void, owner: SEXP, argc: i32, argv: *const SEXP) -> SEXP;
150
151/// Base vtable present in all erased objects.
152///
153/// This vtable provides the minimal operations needed for any erased object:
154/// - Destructor for cleanup when R garbage collects the wrapper
155/// - Concrete type tag for type-safe downcasts
156/// - Query function to retrieve interface vtables
157///
158/// ## Layout Guarantee
159///
160/// This type is `#[repr(C)]` and its layout is frozen. Fields will never
161/// be reordered, and new fields will only be appended at the end.
162///
163/// ## Generated By
164///
165/// `#[derive(ExternalPtr)]` emits a static instance of this vtable for each
166/// wrapped type.
167#[repr(C)]
168pub struct mx_base_vtable {
169    /// Destructor called when the R external pointer is garbage collected.
170    ///
171    /// Receives a pointer to the erased object (not the data pointer).
172    /// Must deallocate the entire erased wrapper structure.
173    ///
174    /// # Safety
175    ///
176    /// - `ptr` must be a valid pointer to `mx_erased` allocated by this type's constructor
177    /// - Must only be called once per object
178    /// - Must be called on R's main thread (during GC finalization)
179    pub drop: unsafe extern "C" fn(ptr: *mut mx_erased),
180
181    /// Tag identifying the concrete type wrapped by this object.
182    ///
183    /// Used for type-safe downcasts: if `concrete_tag` matches the expected
184    /// type's tag, the data pointer can be cast to that type.
185    pub concrete_tag: mx_tag,
186
187    /// Query function to retrieve interface vtables.
188    ///
189    /// Given a trait tag, returns a pointer to the vtable for that interface,
190    /// or null if the type does not implement that trait.
191    ///
192    /// # Arguments
193    ///
194    /// * `ptr` - Pointer to the erased object
195    /// * `trait_tag` - Tag identifying the requested trait interface
196    ///
197    /// # Returns
198    ///
199    /// - Non-null pointer to the trait's vtable if implemented
200    /// - Null pointer if the trait is not implemented
201    ///
202    /// # Safety
203    ///
204    /// - `ptr` must be a valid pointer to `mx_erased`
205    /// - The returned pointer (if non-null) must be cast to the correct vtable type
206    pub query: unsafe extern "C" fn(ptr: *mut mx_erased, trait_tag: mx_tag) -> *const c_void,
207
208    /// Byte offset from the start of the wrapper struct to the `data` field.
209    ///
210    /// The generated wrapper struct is
211    /// `#[repr(C)] struct { erased: mx_erased, data: RefCell<T> }`. Only the
212    /// producer's method shims, compiled with `T` known, look behind this
213    /// offset: they borrow the `RefCell` for the call, so a view in another
214    /// package passes the pointer on and never sees the borrow flag.
215    /// When `T` has stricter alignment than `mx_erased`, padding exists between
216    /// `erased` and `data`. This field records the correct offset so that
217    /// `TraitView::try_from_sexp` can compute the data pointer without
218    /// assuming `offset == size_of::<mx_erased>()`.
219    pub data_offset: usize,
220}
221
222/// Type-erased object header.
223///
224/// This is the common prefix of all erased objects, providing access to
225/// the base vtable. The actual data follows this header in memory.
226///
227/// ## Memory Layout
228///
229/// ```text
230/// ┌─────────────────────────────────────┐
231/// │ mx_erased                           │
232/// │   base: *const mx_base_vtable ──────┼──► static vtable
233/// ├─────────────────────────────────────┤
234/// │ (type-specific data follows...)     │
235/// │   data: RefCell<T>                  │
236/// └─────────────────────────────────────┘
237/// ```
238///
239/// ## Layout Guarantee
240///
241/// This type is `#[repr(C)]` and its layout is frozen. The `base` field
242/// will always be at offset 0, and new fields will only be appended.
243///
244/// ## Generated By
245///
246/// `#[derive(ExternalPtr)]` uses [`ErasedObject<T>`], which places `mx_erased`
247/// as the first field for proper layout.
248#[repr(C)]
249pub struct mx_erased {
250    /// Pointer to the base vtable.
251    ///
252    /// This must point to a valid, static vtable for the lifetime of the object.
253    /// The vtable is typically a `static` generated by `#[derive(ExternalPtr)]`.
254    pub base: *const mx_base_vtable,
255}
256
257// region: Shared erased storage
258
259/// Optional interface queried by `mx_wrap` after it roots the new owner.
260/// Keeping initialization behind `query` leaves the base-vtable layout alone.
261/// This tag identifies this exact interface layout and calling convention.
262pub(crate) const SIDECAR_INIT_TAG: mx_tag = mx_tag_from_path("miniextendr::sidecar_init/v1");
263
264#[repr(C)]
265pub(crate) struct SidecarInitVTable {
266    /// `ptr` belongs to the caller-rooted `owner`; attach its sidecar slots.
267    pub init: unsafe extern "C-unwind" fn(ptr: *mut mx_erased, owner: SEXP),
268}
269
270/// Storage used by generated trait-ABI constructors.
271///
272/// The C header and concrete value retain their repr(C) layout. Keeping the
273/// allocation and destructor here avoids emitting identical helper items for
274/// every ExternalPtr derive. This is not ordinary ExternalPtr/Any storage, but
275/// it keeps the value in a `RefCell` the same way: the method shims borrow it
276/// for each call.
277#[doc(hidden)]
278#[repr(C)]
279pub struct ErasedObject<T> {
280    erased: mx_erased,
281    data: RefCell<T>,
282}
283
284impl<T> ErasedObject<T> {
285    /// Offset of the value's `RefCell`, used by trait views, including
286    /// over-aligned concrete types.
287    pub const DATA_OFFSET: usize = const { builtin # offset_of(Self, data) }core::mem::offset_of!(Self, data);
288
289    /// Allocate an erased object with the generated type's base vtable.
290    ///
291    /// # Safety
292    /// `base` must describe `T` in this storage: its destructor must be
293    /// `Self::drop_raw`, its offset `Self::DATA_OFFSET`, and its query/tag
294    /// must identify the trait implementations of this concrete `T`.
295    pub unsafe fn into_raw(data: T, base: &'static mx_base_vtable) -> *mut mx_erased {
296        Box::into_raw(Box::new(Self {
297            erased: mx_erased { base },
298            data: RefCell::new(data),
299        }))
300        .cast()
301    }
302
303    /// Destructor installed in generated base vtables.
304    ///
305    /// # Safety
306    /// A non-null `ptr` must be a live allocation returned by
307    /// `ErasedObject::<T>::into_raw`, and must be released exactly once.
308    pub unsafe extern "C" fn drop_raw(ptr: *mut mx_erased) {
309        if ptr.is_null() {
310            return;
311        }
312        crate::externalptr::drop_catching_panic(|| {
313            // A panicking user Drop must not cross this C-ABI boundary.
314            unsafe { drop(Box::from_raw(ptr.cast::<Self>())) };
315        });
316    }
317}
318
319impl<T: crate::externalptr::TypedExternal> ErasedObject<T> {
320    const SIDECAR_INIT: SidecarInitVTable = SidecarInitVTable {
321        init: Self::init_sidecars,
322    };
323
324    /// Query the concrete type's traits and its optional sidecar initializer.
325    ///
326    /// # Safety
327    /// `ptr` must be a live `ErasedObject<T>` with its matching base vtable.
328    /// The returned pointer is valid for the program's lifetime and must be
329    /// cast to the interface identified by `tag`.
330    pub unsafe extern "C" fn query(ptr: *mut mx_erased, tag: mx_tag) -> *const c_void {
331        if tag == SIDECAR_INIT_TAG {
332            return if T::R_SLOT_COUNT == 0 {
333                core::ptr::null()
334            } else {
335                core::ptr::from_ref(&Self::SIDECAR_INIT).cast()
336            };
337        }
338        unsafe { crate::registry::universal_query(ptr, tag) }
339    }
340
341    unsafe extern "C-unwind" fn init_sidecars(ptr: *mut mx_erased, owner: SEXP) {
342        // SAFETY: mx_wrap calls the interface for this concrete allocation
343        // after registering its finalizer and while the owner is rooted.
344        unsafe {
345            let value = (*ptr.cast::<Self>()).data.borrow();
346            crate::externalptr::init_erased_sidecars(&*value, owner);
347        }
348    }
349}
350// endregion
351
352// region: Tag generation
353
354/// FNV-1a 64-bit offset basis.
355pub(crate) const FNV1A_64_OFFSET: u64 = 0xcbf29ce484222325;
356
357/// FNV-1a 64-bit prime.
358const FNV1A_64_PRIME: u64 = 0x00000100000001b3;
359
360/// Compute FNV-1a 64-bit hash of a byte slice (const-compatible).
361///
362/// Also fingerprints the generated wrappers file (`registry.rs`), so the
363/// header there and the type tags share one dependency-free digest.
364pub(crate) const fn fnv1a_64(bytes: &[u8], seed: u64) -> u64 {
365    let mut hash = seed;
366    let mut i = 0;
367    while i < bytes.len() {
368        hash ^= bytes[i] as u64;
369        hash = hash.wrapping_mul(FNV1A_64_PRIME);
370        i += 1;
371    }
372    hash
373}
374
375/// Create a new type tag from a string path.
376///
377/// This is a helper for generating deterministic tags from type/trait paths.
378/// Uses FNV-1a hash to produce a 128-bit tag (two independent 64-bit hashes).
379///
380/// # Arguments
381///
382/// * `path` - Fully-qualified path like `"mypackage::MyType"` or `"mypackage::Foo"`
383///
384/// # Returns
385///
386/// A deterministic [`mx_tag`] for the given path.
387///
388/// # Example
389///
390/// ```
391/// use miniextendr_api::abi::mx_tag_from_path;
392///
393/// const TAG_FOO: miniextendr_api::abi::mx_tag = mx_tag_from_path("mypackage::Foo");
394/// const TAG_BAR: miniextendr_api::abi::mx_tag = mx_tag_from_path("mypackage::Bar");
395///
396/// // Same path produces same tag
397/// assert_eq!(TAG_FOO, mx_tag_from_path("mypackage::Foo"));
398///
399/// // Different paths produce different tags
400/// assert_ne!(TAG_FOO, TAG_BAR);
401/// ```
402///
403/// # Note
404///
405/// This function is `const` to enable compile-time tag generation.
406/// The hash is deterministic across compilations.
407#[inline]
408pub const fn mx_tag_from_path(path: &str) -> mx_tag {
409    let bytes = path.as_bytes();
410    // Use different seeds for lo and hi to get independent hash values
411    let lo = fnv1a_64(bytes, FNV1A_64_OFFSET);
412    // XOR with a different constant for the second hash to avoid correlation
413    let hi = fnv1a_64(bytes, FNV1A_64_OFFSET ^ 0x5555555555555555);
414    mx_tag::new(lo, hi)
415}
416
417#[cfg(test)]
418mod tests;
419// endregion