Skip to main content

miniextendr_api/
mx_abi.rs

1//! Rust implementation of the mx_abi C-callable functions.
2//!
3//! These functions are registered with `R_RegisterCCallable` during package init
4//! and loaded by consumer packages via `R_GetCCallable`.
5//!
6//! This module replaces the C implementation that was previously in `mx_abi.c.in`.
7
8use crate::abi::{mx_erased, mx_tag};
9use crate::ffi::{
10    R_ClearExternalPtr, R_ExternalPtrAddr, R_ExternalPtrTag, R_MakeExternalPtr, R_PreserveObject,
11    R_RegisterCCallable, R_RegisterCFinalizerEx, Rboolean, Rf_install, Rf_protect, Rf_unprotect,
12    SEXP, SEXPTYPE, SexpExt,
13};
14use std::ffi::CStr;
15use std::sync::OnceLock;
16
17/// The tag symbol used to identify miniextendr external pointers.
18/// SEXP is Send+Sync so OnceLock<SEXP> works directly.
19static MX_TAG: OnceLock<SEXP> = OnceLock::new();
20
21/// Get the miniextendr tag symbol, initializing it if needed.
22///
23/// # Safety
24/// Must be called on R's main thread.
25#[inline]
26unsafe fn get_tag() -> SEXP {
27    *MX_TAG.get_or_init(|| unsafe {
28        let tag = Rf_install(c"miniextendr::mx_erased".as_ptr());
29        R_PreserveObject(tag);
30        tag
31    })
32}
33
34/// Finalizer callback for R's garbage collector.
35///
36/// Called when an external pointer wrapping `mx_erased` is collected.
37/// Invokes the object's drop function to clean up the Rust allocation.
38unsafe extern "C-unwind" fn mx_externalptr_finalizer(ptr: SEXP) {
39    unsafe {
40        debug_assert_eq!(ptr.type_of(), crate::ffi::SEXPTYPE::EXTPTRSXP,);
41        let erased = R_ExternalPtrAddr(ptr) as *mut mx_erased;
42        if !erased.is_null() {
43            let base = (*erased).base;
44            if !base.is_null() {
45                ((*base).drop)(erased);
46            }
47        }
48        R_ClearExternalPtr(ptr);
49    }
50}
51
52/// Wrap an erased object pointer in an R external pointer.
53///
54/// Registered as `"mx_wrap"` via `R_RegisterCCallable`.
55///
56/// # Safety
57///
58/// `ptr` must point to a valid `mx_erased` allocated by a miniextendr constructor.
59/// Must be called on R's main thread.
60#[unsafe(no_mangle)]
61pub unsafe extern "C-unwind" fn mx_wrap(ptr: *mut mx_erased) -> SEXP {
62    unsafe {
63        let tag = get_tag();
64        let sexp = Rf_protect(R_MakeExternalPtr(ptr.cast(), tag, SEXP::nil()));
65        R_RegisterCFinalizerEx(sexp, Some(mx_externalptr_finalizer), Rboolean::TRUE);
66        Rf_unprotect(1);
67        sexp
68    }
69}
70
71/// Extract an erased object pointer from an R external pointer.
72///
73/// Returns null if the SEXP is not an external pointer or doesn't carry
74/// the miniextendr tag.
75///
76/// Registered as `"mx_get"` via `R_RegisterCCallable`.
77///
78/// # Safety
79///
80/// `sexp` must be a valid SEXP. Must be called on R's main thread.
81#[unsafe(no_mangle)]
82pub unsafe extern "C-unwind" fn mx_get(sexp: SEXP) -> *mut mx_erased {
83    unsafe {
84        if sexp.type_of() != SEXPTYPE::EXTPTRSXP {
85            return std::ptr::null_mut();
86        }
87        if R_ExternalPtrTag(sexp) != get_tag() {
88            return std::ptr::null_mut();
89        }
90        R_ExternalPtrAddr(sexp) as *mut mx_erased
91    }
92}
93
94/// Query an object for an interface vtable by tag.
95///
96/// Returns the vtable pointer, or null if the type does not implement
97/// the requested trait.
98///
99/// Registered as `"mx_query"` via `R_RegisterCCallable`.
100///
101/// # Safety
102///
103/// `sexp` must be a valid SEXP. Must be called on R's main thread.
104#[unsafe(no_mangle)]
105pub unsafe extern "C-unwind" fn mx_query(sexp: SEXP, tag: mx_tag) -> *const std::ffi::c_void {
106    unsafe {
107        let erased = mx_get(sexp);
108        if erased.is_null() {
109            return std::ptr::null();
110        }
111        let base = (*erased).base;
112        if base.is_null() {
113            return std::ptr::null();
114        }
115        ((*base).query)(erased, tag)
116    }
117}
118
119/// Register the mx_* C-callables with R.
120///
121/// Called during package init (`R_init_*`) to make `mx_wrap`, `mx_get`,
122/// and `mx_query` available to consumer packages via `R_GetCCallable`.
123///
124/// # Safety
125///
126/// Must be called from R's main thread during package initialization.
127/// `pkg_name` must be a valid null-terminated C string.
128pub unsafe fn mx_abi_register(pkg_name: &CStr) {
129    unsafe {
130        // Initialize the tag symbol
131        get_tag();
132
133        // Register C-callables for cross-package access.
134        // Cast function pointers to DL_FUNC (Option<unsafe extern "C-unwind" fn() -> *mut c_void>).
135        R_RegisterCCallable(
136            pkg_name.as_ptr(),
137            c"mx_wrap".as_ptr(),
138            Some(std::mem::transmute::<
139                unsafe extern "C-unwind" fn(*mut mx_erased) -> SEXP,
140                unsafe extern "C-unwind" fn() -> *mut std::os::raw::c_void,
141            >(mx_wrap)),
142        );
143        R_RegisterCCallable(
144            pkg_name.as_ptr(),
145            c"mx_get".as_ptr(),
146            Some(std::mem::transmute::<
147                unsafe extern "C-unwind" fn(SEXP) -> *mut mx_erased,
148                unsafe extern "C-unwind" fn() -> *mut std::os::raw::c_void,
149            >(mx_get)),
150        );
151        R_RegisterCCallable(
152            pkg_name.as_ptr(),
153            c"mx_query".as_ptr(),
154            Some(std::mem::transmute::<
155                unsafe extern "C-unwind" fn(SEXP, mx_tag) -> *const std::ffi::c_void,
156                unsafe extern "C-unwind" fn() -> *mut std::os::raw::c_void,
157            >(mx_query)),
158        );
159    }
160}