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::gc_protect::OwnedProtect;
10use crate::sys::{
11    R_ClearExternalPtr, R_ExternalPtrAddr, R_ExternalPtrTag, R_MakeExternalPtr, R_PreserveObject,
12    R_RegisterCCallable, R_RegisterCFinalizerEx, Rf_install,
13};
14use crate::{Rboolean, SEXP, SEXPTYPE, SexpExt};
15use std::ffi::CStr;
16use std::sync::OnceLock;
17
18/// The tag symbol used to identify miniextendr external pointers.
19/// SEXP is Send+Sync so `OnceLock<SEXP>` works directly.
20static MX_TAG: OnceLock<SEXP> = OnceLock::new();
21
22/// Get the miniextendr tag symbol, initializing it if needed.
23///
24/// # Safety
25/// Must be called on R's main thread.
26#[inline]
27unsafe fn get_tag() -> SEXP {
28    *MX_TAG.get_or_init(|| unsafe {
29        let tag = Rf_install(c"miniextendr::mx_erased".as_ptr());
30        R_PreserveObject(tag);
31        tag
32    })
33}
34
35/// Finalizer callback for R's garbage collector.
36///
37/// Called when an external pointer wrapping `mx_erased` is collected.
38/// Invokes the object's drop function to clean up the Rust allocation.
39unsafe extern "C-unwind" fn mx_externalptr_finalizer(ptr: SEXP) {
40    unsafe {
41        debug_assert_eq!(ptr.type_of(), crate::SEXPTYPE::EXTPTRSXP,);
42        let erased = R_ExternalPtrAddr(ptr) as *mut mx_erased;
43        if !erased.is_null() {
44            let base = (*erased).base;
45            if !base.is_null() {
46                ((*base).drop)(erased);
47            }
48        }
49        R_ClearExternalPtr(ptr);
50    }
51}
52
53/// Wrap an erased object pointer in an R external pointer.
54///
55/// Registered as `"mx_wrap"` via `R_RegisterCCallable`.
56///
57/// # Safety
58///
59/// `ptr` must point to a valid `mx_erased` allocated by a miniextendr constructor.
60/// Must be called on R's main thread.
61#[unsafe(no_mangle)]
62pub unsafe extern "C-unwind" fn mx_wrap(ptr: *mut mx_erased) -> SEXP {
63    unsafe {
64        let tag = get_tag();
65        let guard = OwnedProtect::new(R_MakeExternalPtr(ptr.cast(), tag, SEXP::nil()));
66        R_RegisterCFinalizerEx(guard.get(), Some(mx_externalptr_finalizer), Rboolean::TRUE);
67        guard.get()
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}