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