Skip to main content

miniextendr_api/
allocator.rs

1//! R-backed global allocator for Rust.
2//!
3//! Allocations are backed by R RAWSXP objects and protected from GC via
4//! `R_PreserveObject`/`R_ReleaseObject` (R's precious list).
5//!
6//! # Protection Strategy
7//!
8//! This allocator uses `R_PreserveObject` directly because:
9//! - Allocations may need to survive across multiple `.Call` invocations
10//! - The SEXP (RAWSXP) is its own handle — zero Rust-side bookkeeping
11//! - LIFO release pattern (recently allocated = first freed) means O(1) release
12//!   in practice (R's precious list scans from head)
13//!
14//! See the [crate-level documentation](crate#gc-protection-strategies) for an
15//! overview of miniextendr's protection mechanisms.
16//!
17//! # Layout
18//!
19//! Layout inside the RAWSXP (bytes):
20//!   \[optional leading pad\]\[Header\]\[user bytes...\]
21//!
22//! We always return a pointer aligned to at least:
23//!   `max(requested_align, align_of::<Header>())`
24//! so the `Header` placed immediately before the user pointer is always aligned.
25//!
26//! # ⚠️ Warning: longjmp Risk
27//!
28//! R's `Rf_allocVector` can longjmp on allocation failure instead of returning
29//! NULL. If this happens, Rust destructors will NOT run, potentially causing:
30//! - Resource leaks (files, locks, etc.)
31//! - Corrupted state if allocation happens mid-operation
32//!
33//! This allocator is best suited for:
34//! - Short-lived operations within a single R API call
35//! - Contexts where `R_UnwindProtect` is active (e.g., inside `run_on_worker`)
36//!
37//! For long-lived allocations or critical cleanup requirements, consider using
38//! Rust's standard allocator instead.
39
40use crate::ffi::{R_PreserveObject_unchecked, R_ReleaseObject_unchecked, SEXP, SEXPTYPE};
41use crate::worker::{has_worker_context, is_r_main_thread, with_r_thread};
42use core::{
43    alloc::{GlobalAlloc, Layout},
44    mem, ptr,
45};
46
47// region: SendableDataPtr - Thread-safe wrapper for allocator pointers
48
49/// Wrapper to make `*mut u8` pointers `Send` for cross-thread routing.
50///
51/// Unlike `SendablePtr<T>` in externalptr, this allows null pointers
52/// since allocator operations can fail and return null.
53///
54/// # Safety
55///
56/// Same safety model as `Sendable<T>` and `SendablePtr`:
57/// - The pointer value (memory address) is safely transmitted between threads
58/// - The pointer is only dereferenced on R's main thread
59/// - This is guaranteed by the `with_r_thread_or_inline` routing mechanism
60type SendableDataPtr = crate::worker::Sendable<*mut u8>;
61
62#[inline]
63const fn sendable_data_ptr_new(ptr: *mut u8) -> SendableDataPtr {
64    crate::worker::Sendable(ptr)
65}
66
67#[inline]
68const fn sendable_data_ptr_get(ptr: SendableDataPtr) -> *mut u8 {
69    ptr.0
70}
71
72#[inline]
73const fn sendable_data_ptr_is_null(ptr: SendableDataPtr) -> bool {
74    ptr.0.is_null()
75}
76
77#[inline]
78const fn sendable_data_ptr_null() -> SendableDataPtr {
79    crate::worker::Sendable(ptr::null_mut())
80}
81// endregion
82
83// region: Thread routing helper
84
85/// Routes a closure to the R main thread if not already there.
86///
87/// - If on main thread: executes directly
88/// - If in worker context: routes via `with_r_thread`
89/// - Otherwise: panics (R API calls from arbitrary threads are unsafe)
90///
91/// # Panics
92///
93/// Panics if called from a non-main thread without worker context.
94/// This prevents unsafe R API calls from arbitrary threads (e.g., Rayon).
95#[inline]
96fn with_r_thread_or_inline<R: Send + 'static, F: FnOnce() -> R + Send + 'static>(f: F) -> R {
97    if is_r_main_thread() {
98        f()
99    } else if has_worker_context() {
100        with_r_thread(f)
101    } else {
102        panic!(
103            "RAllocator: cannot allocate from non-main thread without worker context. \
104             Ensure miniextendr_runtime_init() was called and you're within run_on_worker()."
105        )
106    }
107}
108// endregion
109
110// region: Header and constants
111
112/// Metadata stored immediately before the returned user pointer.
113#[repr(C)]
114#[derive(Copy, Clone)]
115struct Header {
116    /// The RAWSXP itself, preserved via R_PreserveObject.
117    sexp: SEXP,
118}
119
120const HEADER_SIZE: usize = mem::size_of::<Header>();
121const HEADER_ALIGN: usize = mem::align_of::<Header>();
122// endregion
123
124// region: RAllocator
125
126/// R-backed global allocator.
127///
128/// All allocations are backed by R RAWSXP objects and protected from
129/// garbage collection. The allocator stores metadata before the returned
130/// pointer to enable proper deallocation.
131///
132/// **Note:** This should NOT be used as `#[global_allocator]` in R package
133/// library crates, as it would be invoked during compilation/build time when
134/// R isn't available. Instead, use it explicitly in standalone binaries that
135/// embed R, or use arena-style allocation APIs.
136///
137/// # Thread Safety
138///
139/// This allocator is safe to use from any thread. R API calls are automatically
140/// routed to the main thread via `with_r_thread_or_inline`.
141#[derive(Debug)]
142pub struct RAllocator;
143
144unsafe impl GlobalAlloc for RAllocator {
145    unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
146        sendable_data_ptr_get(with_r_thread_or_inline(move || unsafe {
147            alloc_main_thread(layout)
148        }))
149    }
150
151    unsafe fn dealloc(&self, data: *mut u8, _layout: Layout) {
152        if data.is_null() {
153            return;
154        }
155        let ptr = sendable_data_ptr_new(data);
156        with_r_thread_or_inline(move || unsafe {
157            dealloc_main_thread(ptr);
158        });
159    }
160
161    unsafe fn realloc(&self, old: *mut u8, layout: Layout, new_size: usize) -> *mut u8 {
162        // Handle null input (acts like alloc)
163        if old.is_null() {
164            let Ok(new_layout) = Layout::from_size_align(new_size, layout.align()) else {
165                return ptr::null_mut();
166            };
167            return unsafe { self.alloc(new_layout) };
168        }
169
170        // Handle zero size (acts like dealloc)
171        if new_size == 0 {
172            unsafe { self.dealloc(old, layout) };
173            return ptr::null_mut();
174        }
175
176        let old_ptr = sendable_data_ptr_new(old);
177        let old_size = layout.size();
178        let align = layout.align();
179
180        let new_ptr = with_r_thread_or_inline(move || unsafe {
181            realloc_main_thread(old_ptr, old_size, align, new_size)
182        });
183        sendable_data_ptr_get(new_ptr)
184    }
185
186    unsafe fn alloc_zeroed(&self, layout: Layout) -> *mut u8 {
187        let p = unsafe { self.alloc(layout) };
188        if !p.is_null() {
189            unsafe { ptr::write_bytes(p, 0, layout.size()) };
190        }
191        p
192    }
193}
194// endregion
195
196// region: Main-thread helpers
197
198/// Allocate memory on the R main thread.
199///
200/// # Safety
201///
202/// Must be called from R's main thread (or routed via `with_r_thread`).
203unsafe fn alloc_main_thread(layout: Layout) -> SendableDataPtr {
204    // ZST allocations: return null since we can't meaningfully track them
205    // (dangling pointer would crash in dealloc when we try to read the header)
206    if layout.size() == 0 {
207        return sendable_data_ptr_null();
208    }
209
210    let align = layout.align().max(HEADER_ALIGN);
211
212    // Calculate total size needed with overflow checking
213    let total = {
214        let Some(align_minus_1) = align.checked_sub(1) else {
215            return sendable_data_ptr_null();
216        };
217        let Some(temp) = HEADER_SIZE.checked_add(align_minus_1) else {
218            return sendable_data_ptr_null();
219        };
220        let Some(total) = temp.checked_add(layout.size()) else {
221            return sendable_data_ptr_null();
222        };
223        total
224    };
225
226    let total_isize: isize = match total.try_into() {
227        Ok(n) => n,
228        Err(_) => return sendable_data_ptr_null(),
229    };
230
231    // NOTE: Rf_allocVector can longjmp on failure instead of returning NULL.
232    // If this happens inside run_on_worker, R_UnwindProtect will catch it.
233    // Outside of that context, Rust destructors may be skipped.
234    // Use _unchecked since we're guaranteed to be on R main thread via with_r_thread_or_inline.
235    let sexp = unsafe { crate::ffi::Rf_allocVector_unchecked(SEXPTYPE::RAWSXP, total_isize) };
236    if sexp.is_null() {
237        return sendable_data_ptr_null();
238    }
239
240    // Protect from GC (must stay valid until dealloc()).
241    // Uses R_PreserveObject — LIFO means recently allocated objects are found fast on release.
242    unsafe { R_PreserveObject_unchecked(sexp) };
243
244    // Use _unchecked since we're guaranteed to be on R main thread.
245    let raw_base = unsafe { crate::ffi::RAW_unchecked(sexp) }.cast::<u8>();
246
247    // Calculate header and data pointers with alignment
248    let after_header = unsafe { raw_base.add(HEADER_SIZE) };
249    let pad = after_header.align_offset(align);
250    if pad == usize::MAX {
251        // Alignment failed (extremely unlikely)
252        unsafe { R_ReleaseObject_unchecked(sexp) };
253        return sendable_data_ptr_null();
254    }
255
256    let data = unsafe { after_header.add(pad) };
257    let header = unsafe { data.sub(HEADER_SIZE) }.cast::<Header>();
258
259    unsafe { header.write(Header { sexp }) };
260
261    debug_assert_eq!(data.align_offset(layout.align()), 0);
262    sendable_data_ptr_new(data)
263}
264
265/// Deallocate memory on the R main thread.
266///
267/// # Safety
268///
269/// Must be called from R's main thread (or routed via `with_r_thread`).
270/// The pointer must have been allocated by this allocator.
271unsafe fn dealloc_main_thread(ptr: SendableDataPtr) {
272    let data = sendable_data_ptr_get(ptr);
273    let header = unsafe { data.sub(HEADER_SIZE) }.cast::<Header>();
274    let sexp = unsafe { (*header).sexp };
275    unsafe { R_ReleaseObject_unchecked(sexp) };
276}
277
278/// Reallocate memory on the R main thread.
279///
280/// # Safety
281///
282/// Must be called from R's main thread (or routed via `with_r_thread`).
283/// The old pointer must have been allocated by this allocator.
284unsafe fn realloc_main_thread(
285    old_ptr: SendableDataPtr,
286    old_size: usize,
287    align: usize,
288    new_size: usize,
289) -> SendableDataPtr {
290    let old = sendable_data_ptr_get(old_ptr);
291
292    // Recover RAWSXP from header (stored directly, no DLL cell indirection).
293    // Use _unchecked since we're guaranteed to be on R main thread via with_r_thread_or_inline.
294    let header = unsafe { old.sub(HEADER_SIZE) }.cast::<Header>();
295    let sexp = unsafe { (*header).sexp };
296
297    // Check if existing allocation has capacity
298    let raw_base = unsafe { crate::ffi::RAW_unchecked(sexp) }.cast::<u8>();
299    let cap: usize = match unsafe { crate::ffi::Rf_xlength_unchecked(sexp) }.try_into() {
300        Ok(n) => n,
301        Err(_) => return sendable_data_ptr_null(),
302    };
303
304    let used = unsafe { old.cast_const().offset_from(raw_base.cast_const()) };
305    let Ok(used_usize) = usize::try_from(used) else {
306        // Should be impossible if `old` came from this allocator, but don't UB.
307        return sendable_data_ptr_null();
308    };
309    let available = cap.saturating_sub(used_usize);
310
311    if new_size <= available {
312        return old_ptr; // Reuse existing allocation
313    }
314
315    // Need new allocation
316    let Ok(new_layout) = Layout::from_size_align(new_size, align) else {
317        return sendable_data_ptr_null();
318    };
319
320    let new_ptr = unsafe { alloc_main_thread(new_layout) };
321    if sendable_data_ptr_is_null(new_ptr) {
322        // On realloc failure, the old allocation must remain valid.
323        return sendable_data_ptr_null();
324    }
325
326    unsafe {
327        ptr::copy_nonoverlapping(old, sendable_data_ptr_get(new_ptr), old_size.min(new_size))
328    };
329    unsafe { R_ReleaseObject_unchecked(sexp) }; // Free old allocation
330
331    new_ptr
332}
333// endregion