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