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