miniextendr_api/preserve.rs
1//! **Deprecated**: Use [`ProtectPool`](crate::protect_pool::ProtectPool) or
2//! [`R_PreserveObject`] instead. This module
3//! remains for benchmark comparisons only. See `analysis/gc-protection-benchmarks-results.md`.
4//!
5//! R object preservation using a circular doubly-linked list.
6//!
7//! This module provides a protection scheme for R objects (SEXPs) that need to
8//! survive R's garbage collection **across multiple `.Call` invocations**.
9//!
10//! # Protection Strategies in miniextendr
11//!
12//! miniextendr provides three complementary protection mechanisms for different scenarios:
13//!
14//! | Strategy | Module | Lifetime | Release Order | Use Case |
15//! |----------|--------|----------|---------------|----------|
16//! | **PROTECT stack** | [`gc_protect`](crate::gc_protect) | Within `.Call` | LIFO (stack) | Temporary allocations |
17//! | **Preserve list** | [`preserve`](crate::preserve) | Across `.Call`s | Any order | Long-lived R objects |
18//! | **R ownership** | [`ExternalPtr`](struct@crate::ExternalPtr) | Until R GCs | R decides | Rust data owned by R |
19//!
20//! ## When to Use This Module
21//!
22//! **Use `preserve` (this module) when:**
23//! - Objects must survive across multiple `.Call` invocations
24//! - You need to release protections in arbitrary order (not LIFO)
25//! - Example: [`RAllocator`](crate::RAllocator) backing memory
26//!
27//! **Use [`gc_protect`](crate::gc_protect) instead when:**
28//! - Protection is short-lived (within a single `.Call`)
29//! - You want RAII-based automatic PROTECT/UNPROTECT balancing
30//!
31//! **Use [`ExternalPtr`](struct@crate::ExternalPtr) instead when:**
32//! - You want R to own a Rust value with automatic cleanup
33//!
34//! # Architecture
35//!
36//! This module uses a cpp11-style circular doubly-linked list approach,
37//! which has advantages over the R protect stack:
38//!
39//! - No balance requirement (PROTECT/UNPROTECT pairs must be balanced)
40//! - Can release protections in any order
41//! - Thread-local storage (each thread has its own preserve list)
42//! - More ergonomic with RAII patterns
43//!
44//! The preservation list is a circular doubly-linked cons list where:
45//! - The list itself is preserved with `R_PreserveObject` (never GC'd)
46//! - Each protected SEXP is stored as the TAG of a cell
47//! - CAR points to previous cell, CDR points to next cell
48//! - Head and tail are sentinel nodes
49//!
50//! # Safety
51//!
52//! All functions in this module are unsafe and must be called from the R main thread.
53
54use crate::ffi::{PairListExt, R_PreserveObject, Rf_protect, Rf_unprotect, SEXP, SexpExt};
55use std::cell::OnceCell;
56
57thread_local! {
58 /// The per-thread preservation list.
59 ///
60 /// Initialized on first use with a circular doubly-linked list
61 /// that is preserved from R's GC via `R_PreserveObject`.
62 static PRESERVE_LIST: OnceCell<SEXP> = const { OnceCell::new() };
63}
64
65/// Initialize the preservation list.
66///
67/// Creates a circular doubly-linked list: `(head -> sentinel -> head)`
68/// and preserves it with `R_PreserveObject` so it's never GC'd.
69///
70/// # Safety
71///
72/// Must be called from the R main thread.
73#[inline]
74unsafe fn init() -> SEXP {
75 unsafe {
76 let out = SEXP::nil().cons(SEXP::nil().cons(SEXP::nil()));
77 R_PreserveObject(out);
78 out
79 }
80}
81
82/// Initialize the preservation list (unchecked version).
83///
84/// Skips thread safety checks for performance-critical paths.
85///
86/// # Safety
87///
88/// Must be called from the R main thread. Only use in contexts where
89/// you're certain you're on the main thread.
90#[inline]
91unsafe fn init_unchecked() -> SEXP {
92 use crate::ffi::{PairListExt, R_PreserveObject_unchecked};
93
94 unsafe {
95 let out = SEXP::nil().cons_unchecked(SEXP::nil().cons_unchecked(SEXP::nil()));
96 R_PreserveObject_unchecked(out);
97 out
98 }
99}
100
101/// Get the current thread's preservation list, initializing if needed.
102///
103/// # Safety
104///
105/// Must be called from the R main thread.
106#[inline]
107pub(crate) unsafe fn get() -> SEXP {
108 // One global preserve list per thread.
109 PRESERVE_LIST.with(|x| *x.get_or_init(|| unsafe { init() }))
110}
111
112/// Get the current thread's preservation list (unchecked version).
113///
114/// Skips thread safety checks for performance-critical paths.
115///
116/// # Safety
117///
118/// Must be called from the R main thread. Only use in contexts where
119/// you're certain you're on the main thread (ALTREP callbacks, extern "C-unwind" functions).
120#[inline]
121pub(crate) unsafe fn get_unchecked() -> SEXP {
122 // Use unchecked init for full consistency
123 PRESERVE_LIST.with(|x| *x.get_or_init(|| unsafe { init_unchecked() }))
124}
125
126/// Count the number of currently protected objects.
127///
128/// This is useful for debugging and testing, but not typically needed
129/// in production code.
130///
131/// # Safety
132///
133/// Must be called from the R main thread.
134#[cfg(feature = "debug-preserve")]
135#[inline]
136pub unsafe fn count() -> crate::ffi::R_xlen_t {
137 use crate::ffi::{R_xlen_t, Rf_xlength};
138 unsafe {
139 let head: R_xlen_t = 1;
140 let tail: R_xlen_t = 1;
141 let list = get();
142 Rf_xlength(list) - head - tail
143 }
144}
145
146/// Count the number of currently protected objects (unchecked version).
147///
148/// Skips thread safety checks for performance-critical paths.
149///
150/// # Safety
151///
152/// Must be called from the R main thread. Only use in contexts where
153/// you're certain you're on the main thread.
154#[cfg(feature = "debug-preserve")]
155#[inline]
156pub unsafe fn count_unchecked() -> crate::ffi::R_xlen_t {
157 use crate::ffi::{R_xlen_t, Rf_xlength_unchecked};
158
159 unsafe {
160 let head: R_xlen_t = 1;
161 let tail: R_xlen_t = 1;
162 let list = get_unchecked();
163 Rf_xlength_unchecked(list) - head - tail
164 }
165}
166
167/// Insert a SEXP into the preservation list, protecting it from GC.
168///
169/// Returns a "cell" (a cons cell) that can later be passed to [`release`]
170/// to stop protecting the object.
171///
172/// If `x` is `R_NilValue`, returns `R_NilValue` without protection
173/// (since NIL is never collected).
174///
175/// # Safety
176///
177/// Must be called from the R main thread. The returned cell must eventually
178/// be passed to [`release`] to prevent leaking memory in the preserve list.
179#[inline]
180pub unsafe fn insert(x: SEXP) -> SEXP {
181 unsafe {
182 if x.is_nil() {
183 return SEXP::nil();
184 }
185
186 Rf_protect(x);
187
188 let list = get();
189
190 // head is the list itself; next is the node after head
191 let head = list;
192 let next = head.cdr();
193
194 // New cell points to current head and next
195 let cell = Rf_protect(head.cons(next));
196 cell.set_tag(x);
197
198 // Splice cell between head and next
199 head.set_cdr(cell);
200 next.set_car(cell);
201
202 Rf_unprotect(2);
203
204 cell
205 }
206}
207
208/// Insert a SEXP into the preservation list (unchecked version).
209///
210/// Skips thread safety checks for performance-critical paths.
211/// Otherwise identical to [`insert`].
212///
213/// # Safety
214///
215/// Must be called from the R main thread. Only use in contexts where
216/// you're certain you're on the main thread (ALTREP callbacks, extern "C-unwind" functions).
217/// The returned cell must eventually be passed to [`release_unchecked`].
218#[inline]
219pub unsafe fn insert_unchecked(x: SEXP) -> SEXP {
220 use crate::ffi::{PairListExt, Rf_protect_unchecked, Rf_unprotect_unchecked};
221
222 unsafe {
223 if x.is_nil() {
224 return SEXP::nil();
225 }
226
227 Rf_protect_unchecked(x);
228
229 let list = get_unchecked();
230
231 // head is the list itself; next is the node after head
232 let head = list;
233 let next = head.cdr_unchecked();
234
235 // New cell points to current head and next
236 let cell = Rf_protect_unchecked(head.cons_unchecked(next));
237 cell.set_tag_unchecked(x);
238
239 // Splice cell between head and next
240 head.set_cdr_unchecked(cell);
241 next.set_car_unchecked(cell);
242
243 Rf_unprotect_unchecked(2);
244
245 cell
246 }
247}
248
249/// Release a previously protected SEXP from the preservation list.
250///
251/// The `cell` parameter should be a value returned from [`insert`].
252///
253/// If `cell` is `R_NilValue`, this is a no-op.
254///
255/// # Safety
256///
257/// Must be called from the R main thread. The `cell` must be a valid
258/// cell returned from [`insert`] and must not have been released already.
259#[inline]
260pub unsafe fn release(cell: SEXP) {
261 if cell.is_nil() {
262 return;
263 }
264
265 // Neighbors around the cell
266 let lhs = cell.car();
267 let rhs = cell.cdr();
268
269 // Bypass cell
270 lhs.set_cdr(rhs);
271 rhs.set_car(lhs);
272}
273
274/// Release a previously protected SEXP (unchecked version).
275///
276/// Skips thread safety checks for performance-critical paths.
277/// Otherwise identical to [`release`].
278///
279/// # Safety
280///
281/// Must be called from the R main thread. Only use in contexts where
282/// you're certain you're on the main thread. The `cell` must be a valid
283/// cell returned from [`insert_unchecked`] and must not have been released already.
284#[inline]
285pub unsafe fn release_unchecked(cell: SEXP) {
286 use crate::ffi::PairListExt;
287
288 unsafe {
289 if cell.is_nil() {
290 return;
291 }
292
293 // Neighbors around the cell
294 let lhs = cell.car_unchecked();
295 let rhs = cell.cdr_unchecked();
296
297 // Bypass cell
298 lhs.set_cdr_unchecked(rhs);
299 rhs.set_car_unchecked(lhs);
300 }
301}