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 safe to use from any thread. R API calls are automatically
141/// routed to the main thread via `with_r_thread_or_inline`.
142#[derive(Debug)]
143pub struct RAllocator;
144
145unsafe impl GlobalAlloc for RAllocator {
146    unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
147        sendable_data_ptr_get(with_r_thread_or_inline(move || unsafe {
148            alloc_main_thread(layout)
149        }))
150    }
151
152    unsafe fn dealloc(&self, data: *mut u8, _layout: Layout) {
153        if data.is_null() {
154            return;
155        }
156        let ptr = sendable_data_ptr_new(data);
157        with_r_thread_or_inline(move || unsafe {
158            dealloc_main_thread(ptr);
159        });
160    }
161
162    unsafe fn realloc(&self, old: *mut u8, layout: Layout, new_size: usize) -> *mut u8 {
163        // Handle null input (acts like alloc)
164        if old.is_null() {
165            let Ok(new_layout) = Layout::from_size_align(new_size, layout.align()) else {
166                return ptr::null_mut();
167            };
168            return unsafe { self.alloc(new_layout) };
169        }
170
171        // Handle zero size (acts like dealloc)
172        if new_size == 0 {
173            unsafe { self.dealloc(old, layout) };
174            return ptr::null_mut();
175        }
176
177        let old_ptr = sendable_data_ptr_new(old);
178        let old_size = layout.size();
179        let align = layout.align();
180
181        let new_ptr = with_r_thread_or_inline(move || unsafe {
182            realloc_main_thread(old_ptr, old_size, align, new_size)
183        });
184        sendable_data_ptr_get(new_ptr)
185    }
186
187    unsafe fn alloc_zeroed(&self, layout: Layout) -> *mut u8 {
188        let p = unsafe { self.alloc(layout) };
189        if !p.is_null() {
190            unsafe { ptr::write_bytes(p, 0, layout.size()) };
191        }
192        p
193    }
194}
195// endregion
196
197// region: Main-thread helpers
198
199/// Allocate memory on the R main thread.
200///
201/// # Safety
202///
203/// Must be called from R's main thread (or routed via `with_r_thread`).
204unsafe fn alloc_main_thread(layout: Layout) -> SendableDataPtr {
205    // ZST allocations: return null since we can't meaningfully track them
206    // (dangling pointer would crash in dealloc when we try to read the header)
207    if layout.size() == 0 {
208        return sendable_data_ptr_null();
209    }
210
211    let align = layout.align().max(HEADER_ALIGN);
212
213    // Calculate total size needed with overflow checking
214    let total = {
215        let Some(align_minus_1) = align.checked_sub(1) else {
216            return sendable_data_ptr_null();
217        };
218        let Some(temp) = HEADER_SIZE.checked_add(align_minus_1) else {
219            return sendable_data_ptr_null();
220        };
221        let Some(total) = temp.checked_add(layout.size()) else {
222            return sendable_data_ptr_null();
223        };
224        total
225    };
226
227    let total_isize: isize = match total.try_into() {
228        Ok(n) => n,
229        Err(_) => return sendable_data_ptr_null(),
230    };
231
232    // NOTE: Rf_allocVector can longjmp on failure instead of returning NULL.
233    // If this happens inside run_on_worker, R_UnwindProtect will catch it.
234    // Outside of that context, Rust destructors may be skipped.
235    // Use _unchecked since we're guaranteed to be on R main thread via with_r_thread_or_inline.
236    // RAWSXP literal is the source of truth (#882): this is an opaque R-managed
237    // byte arena used as a `*mut u8` allocation, not a typed vector — there is no
238    // `T: RNativeType` element to derive the tag from.
239    let sexp = unsafe { crate::sys::Rf_allocVector_unchecked(SEXPTYPE::RAWSXP, total_isize) };
240    if sexp.is_null() {
241        return sendable_data_ptr_null();
242    }
243
244    // Protect from GC (must stay valid until dealloc()).
245    // Uses R_PreserveObject — LIFO means recently allocated objects are found fast on release.
246    unsafe { R_PreserveObject_unchecked(sexp) };
247
248    // Use _unchecked since we're guaranteed to be on R main thread.
249    let raw_base = unsafe { crate::sys::RAW_unchecked(sexp) }.cast::<u8>();
250
251    // Calculate header and data pointers with alignment
252    let after_header = unsafe { raw_base.add(HEADER_SIZE) };
253    let pad = after_header.align_offset(align);
254    if pad == usize::MAX {
255        // Alignment failed (extremely unlikely)
256        unsafe { R_ReleaseObject_unchecked(sexp) };
257        return sendable_data_ptr_null();
258    }
259
260    let data = unsafe { after_header.add(pad) };
261    let header = unsafe { data.sub(HEADER_SIZE) }.cast::<Header>();
262
263    unsafe { header.write(Header { sexp }) };
264
265    debug_assert_eq!(data.align_offset(layout.align()), 0);
266    sendable_data_ptr_new(data)
267}
268
269/// Deallocate memory on the R main thread.
270///
271/// # Safety
272///
273/// Must be called from R's main thread (or routed via `with_r_thread`).
274/// The pointer must have been allocated by this allocator.
275unsafe fn dealloc_main_thread(ptr: SendableDataPtr) {
276    let data = sendable_data_ptr_get(ptr);
277    let header = unsafe { data.sub(HEADER_SIZE) }.cast::<Header>();
278    let sexp = unsafe { (*header).sexp };
279    unsafe { R_ReleaseObject_unchecked(sexp) };
280}
281
282/// Reallocate memory on the R main thread.
283///
284/// # Safety
285///
286/// Must be called from R's main thread (or routed via `with_r_thread`).
287/// The old pointer must have been allocated by this allocator.
288unsafe fn realloc_main_thread(
289    old_ptr: SendableDataPtr,
290    old_size: usize,
291    align: usize,
292    new_size: usize,
293) -> SendableDataPtr {
294    let old = sendable_data_ptr_get(old_ptr);
295
296    // Recover RAWSXP from header (stored directly, no DLL cell indirection).
297    // Use _unchecked since we're guaranteed to be on R main thread via with_r_thread_or_inline.
298    let header = unsafe { old.sub(HEADER_SIZE) }.cast::<Header>();
299    let sexp = unsafe { (*header).sexp };
300
301    // Check if existing allocation has capacity
302    let raw_base = unsafe { crate::sys::RAW_unchecked(sexp) }.cast::<u8>();
303    let cap: usize = match unsafe { sexp.xlength_unchecked() }.try_into() {
304        Ok(n) => n,
305        Err(_) => return sendable_data_ptr_null(),
306    };
307
308    let used = unsafe { old.cast_const().offset_from(raw_base.cast_const()) };
309    let Ok(used_usize) = usize::try_from(used) else {
310        // Should be impossible if `old` came from this allocator, but don't UB.
311        return sendable_data_ptr_null();
312    };
313    let available = cap.saturating_sub(used_usize);
314
315    if new_size <= available {
316        return old_ptr; // Reuse existing allocation
317    }
318
319    // Need new allocation
320    let Ok(new_layout) = Layout::from_size_align(new_size, align) else {
321        return sendable_data_ptr_null();
322    };
323
324    let new_ptr = unsafe { alloc_main_thread(new_layout) };
325    if sendable_data_ptr_is_null(new_ptr) {
326        // On realloc failure, the old allocation must remain valid.
327        return sendable_data_ptr_null();
328    }
329
330    unsafe {
331        ptr::copy_nonoverlapping(old, sendable_data_ptr_get(new_ptr), old_size.min(new_size))
332    };
333    unsafe { R_ReleaseObject_unchecked(sexp) }; // Free old allocation
334
335    new_ptr
336}
337// endregion