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(Debug, Clone, Copy, PartialEq, Eq, 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 = 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