miniextendr_api/altrep_impl.rs
1//! ALTREP implementation utilities.
2//!
3//! This module provides helper functions used by the declarative macros
4//! generated for ALTREP classes. The proc-macro derives (`#[derive(AltrepInteger)]`
5//! etc.) lower to invocations of `impl_alt*_from_data!` macros which expand
6//! to code that references items in this module via `$crate::altrep_impl::*`
7//! paths.
8//!
9//! Use `crate::altrep_data1_as` (re-exported from externalptr) to extract
10//! data from an ALTREP's data1 slot.
11//!
12//! ## Layout
13//!
14//! - [`macros`] — `#[macro_export]` declarative macros (`impl_alt*_from_data!`
15//! and their `__impl_*` helpers).
16//! - [`builtins`] — crate-private meta-macro + invocations for `Vec<T>` /
17//! `Box<[T]>` / `Cow<T>` / `Range<T>` ALTREP families.
18//! - [`arrays`] — const-generic `[T; N]` ALTREP impls.
19//! - [`static_slices`] — hand-written `&'static [T]` ALTREP impls.
20
21/// Clone a derive-generated ALTREP owner while preserving its current R cache.
22/// R's default DuplicateEX copies attributes and object flags after this hook.
23///
24/// # Safety
25/// `x` must be a rooted ALTREP of `T` with ExternalPtr data1, on R's thread.
26/// `T::clone` must detach any allocation that this class exposes for R writes.
27#[doc(hidden)]
28pub unsafe fn duplicate_altrep_owner<T>(x: crate::SEXP) -> crate::SEXP
29where
30 T: crate::externalptr::TypedExternal + crate::altrep::RegisterAltrep + Clone,
31{
32 unsafe {
33 let original =
34 crate::altrep_data1_as_unchecked::<T>(x).expect("ALTREP owner type mismatch");
35 // Clone the handle as well as T so sidecar slots are preserved.
36 let owner = original.clone();
37 let data1 = crate::OwnedProtect::new(owner.as_sexp());
38 let class = T::get_or_init_class();
39 let out =
40 crate::OwnedProtect::new(class.new_altrep_unchecked(data1.get(), crate::SEXP::nil()));
41 if let Some(cache) = crate::altrep_data::materialized_altrep_data2(x) {
42 out.get().set_altrep_data2(duplicate_altrep_data2(cache));
43 }
44 out.get()
45 }
46}
47
48// region: Checked string-to-CHARSXP helper
49
50/// Create a CHARSXP from a Rust string, with checked length conversion.
51///
52/// # Safety
53///
54/// Must be called from R's main thread.
55///
56/// # Panics
57///
58/// Panics if `s.len() > i32::MAX`.
59#[inline]
60pub unsafe fn checked_mkchar(s: &str) -> crate::SEXP {
61 let _len = i32::try_from(s.len()).unwrap_or_else(|_| {
62 panic!(
63 "string length {} exceeds i32::MAX for Rf_mkCharLenCE",
64 s.len()
65 )
66 });
67 crate::SEXP::charsxp(s)
68}
69// endregion
70
71// region: String ALTREP element cache
72
73/// Return a bridge string ALTREP's data2 element cache, allocating it on first use.
74///
75/// Slots not computed yet hold C `NULL`, as in R's own deferred string class
76/// (`altclasses.c`). `NA_character_` is therefore an ordinary cached value, so
77/// an NA that R writes through `Set_elt` or `Dataptr` stays NA. Every CHARSXP
78/// handed to R stays reachable from `x`, as R code expects of `STRING_ELT`.
79///
80/// # Safety
81///
82/// `x` must be a live ALTSTRING using the bridge's data2 convention, on R's
83/// main thread.
84unsafe fn altstring_cache(x: crate::SEXP, n: crate::R_xlen_t) -> crate::SEXP {
85 use crate::SexpExt;
86 use crate::altrep_ext::AltrepSexpExt;
87
88 let data2 = unsafe { AltrepSexpExt::altrep_data2_raw(&x) };
89 if !data2.is_null_ptr() && data2.type_of() == crate::SEXPTYPE::STRSXP {
90 return data2;
91 }
92 unsafe {
93 let cache = new_altstring_cache(n);
94 // Attaching the cache allocates nothing, so it needs no protection.
95 AltrepSexpExt::set_altrep_data2(&x, cache);
96 cache
97 }
98}
99
100/// Allocate a string cache of length `n` with every slot empty (C `NULL`).
101///
102/// # Safety
103///
104/// On R's main thread. The result is unprotected.
105unsafe fn new_altstring_cache(n: crate::R_xlen_t) -> crate::SEXP {
106 // Inside ALTREP dispatch — _unchecked variants skip the redundant
107 // with_r_thread check/route (MXL301 permits).
108 unsafe {
109 let cache = crate::sys::Rf_allocVector_unchecked(crate::SEXPTYPE::STRSXP, n);
110 if n > 0 {
111 // A fresh STRSXP holds only R_BlankString, so clearing its slots
112 // drops no reference the write barrier tracks.
113 let slots = crate::sys::DATAPTR_RO_unchecked(cache)
114 .cast_mut()
115 .cast::<crate::SEXP>();
116 let len = usize::try_from(n).expect("R vector length is non-negative");
117 std::ptr::write_bytes(slots, 0, len);
118 }
119 cache
120 }
121}
122
123/// Copy a bridge ALTREP's data2 for a `Duplicate` callback.
124///
125/// A string cache can hold C `NULL` slots, which `Rf_duplicate` can't copy:
126/// it stores every slot with `SET_STRING_ELT`. Its copy keeps those slots
127/// empty, so the new vector computes them from its own Rust data, and keeps
128/// the written mark. Any other data2 (a materialized vector) goes through
129/// `Rf_duplicate`.
130///
131/// # Safety
132///
133/// `data2` is a live ALTREP's data2, kept reachable by the caller, on R's
134/// main thread. The result is unprotected.
135#[doc(hidden)]
136pub unsafe fn duplicate_altrep_data2(data2: crate::SEXP) -> crate::SEXP {
137 use crate::SexpExt;
138
139 if data2.type_of() != crate::SEXPTYPE::STRSXP {
140 return unsafe { crate::sys::Rf_duplicate_unchecked(data2) };
141 }
142 unsafe {
143 let n = data2.xlength();
144 let copy = crate::OwnedProtect::new(new_altstring_cache(n));
145 for i in 0..n {
146 let value = data2.string_elt(i);
147 if !value.is_null_ptr() {
148 copy.get().set_string_elt(i, value);
149 }
150 }
151 if altstring_cache_written(data2) {
152 mark_altstring_written(copy.get());
153 }
154 copy.get()
155 }
156}
157
158/// Read cache slot `i`, computing it from the Rust data on first use.
159///
160/// # Safety
161///
162/// As for [`altstring_cache`]; `cache` is `x`'s cache and `0 <= i < length`.
163unsafe fn altstring_cached_elt<T>(
164 x: crate::SEXP,
165 cache: crate::SEXP,
166 i: crate::R_xlen_t,
167) -> crate::SEXP
168where
169 T: crate::altrep_data::AltrepExtract + crate::altrep_data::AltStringData + 'static,
170{
171 use crate::SexpExt;
172
173 let cached = cache.string_elt(i);
174 if !cached.is_null_ptr() {
175 return cached;
176 }
177 let data = unsafe { T::altrep_extract_ref(x) };
178 let index = usize::try_from(i).expect("ALTREP element index is non-negative");
179 let value = match data.elt(index) {
180 Some(s) => unsafe { checked_mkchar(s) },
181 None => crate::SEXP::na_string(),
182 };
183 cache.set_string_elt(i, value);
184 value
185}
186
187/// `Elt` for bridge-generated string ALTREPs: each element is computed once.
188///
189/// # Safety
190///
191/// `x` must be a live ALTSTRING using the bridge's data2 convention, on R's
192/// main thread.
193#[doc(hidden)]
194pub unsafe fn altstring_elt<T>(x: crate::SEXP, i: crate::R_xlen_t) -> crate::SEXP
195where
196 T: crate::altrep_traits::Altrep
197 + crate::altrep_data::AltrepExtract
198 + crate::altrep_data::AltStringData
199 + 'static,
200{
201 // Clamp before touching data2: it is a plain STRSXP, so STRING_ELT would
202 // error on a negative index instead of using the clamped one (#1190).
203 let i = i.max(0);
204 unsafe {
205 let cache = altstring_cache(x, T::length(x));
206 altstring_cached_elt::<T>(x, cache, i)
207 }
208}
209
210/// `Dataptr` for bridge-generated string ALTREPs: fill every cache slot and
211/// return the cache's pointer. A writable request marks the cache written
212/// first (see [`altstring_cache_written`]), since R writes through it.
213///
214/// A panic part-way leaves the filled slots cached; a retry fills the rest.
215///
216/// # Safety
217///
218/// As for [`altstring_elt`].
219#[doc(hidden)]
220pub unsafe fn altstring_dataptr<T>(x: crate::SEXP, writable: bool) -> *mut core::ffi::c_void
221where
222 T: crate::altrep_traits::Altrep
223 + crate::altrep_data::AltrepExtract
224 + crate::altrep_data::AltStringData
225 + 'static,
226{
227 unsafe {
228 let n = T::length(x);
229 let cache = altstring_cache(x, n);
230 for i in 0..n {
231 altstring_cached_elt::<T>(x, cache, i);
232 }
233 if writable {
234 mark_altstring_written(cache);
235 }
236 crate::sys::DATAPTR_RO_unchecked(cache).cast_mut()
237 }
238}
239
240/// `Set_elt` for bridge-generated string ALTREPs; R's default raises an error.
241///
242/// # Safety
243///
244/// As for [`altstring_elt`]; `v` is a CHARSXP and `0 <= i < length`.
245#[doc(hidden)]
246pub unsafe fn altstring_set_elt<T>(x: crate::SEXP, i: crate::R_xlen_t, v: crate::SEXP)
247where
248 T: crate::altrep_traits::Altrep,
249{
250 use crate::SexpExt;
251
252 unsafe {
253 // R disables GC around Set_elt, but a direct caller may not: keep `v`
254 // live across the cache allocation.
255 let v = crate::OwnedProtect::new(v);
256 let cache = altstring_cache(x, T::length(x));
257 mark_altstring_written(cache);
258 cache.set_string_elt(i, v.get());
259 }
260}
261
262/// Whether R has stored a value into a string ALTREP's cache.
263///
264/// Reads fill the cache too, with values computed from the Rust data, so a
265/// cache alone doesn't mean the Rust data is stale. `Set_elt` and a writable
266/// `Dataptr` mark it before R can store into it, so the mark errs towards
267/// written.
268///
269/// # Safety
270///
271/// `cache` is a string ALTREP's data2 cache, on R's main thread.
272pub(crate) unsafe fn altstring_cache_written(cache: crate::SEXP) -> bool {
273 use crate::SexpExt;
274
275 !unsafe { cache.get_attr_unchecked(crate::cached_class::altstring_written_symbol()) }.is_nil()
276}
277
278/// Mark a string ALTREP's cache written; see [`altstring_cache_written`].
279///
280/// # Safety
281///
282/// As for [`altstring_cache_written`].
283unsafe fn mark_altstring_written(cache: crate::SEXP) {
284 use crate::SexpExt;
285
286 unsafe {
287 if !altstring_cache_written(cache) {
288 // The symbol is its own value: a symbol is never collected, so
289 // setting it allocates nothing but the attribute's cell.
290 let symbol = crate::cached_class::altstring_written_symbol();
291 cache.set_attr_unchecked(symbol, symbol);
292 }
293 }
294}
295// endregion
296
297// region: List ALTREP writes
298
299/// `Set_elt` for bridge-generated list ALTREPs; R's default raises an error.
300///
301/// The first write copies every element into a plain list in data2. From then
302/// on that list holds the vector's values: `Elt` and `Length` read it,
303/// `Duplicate` copies it, and serialization leaves the vector to R.
304///
305/// # Safety
306///
307/// `x` is a live list ALTREP of `T` on R's main thread, `v` a valid SEXP and
308/// `0 <= i < length`.
309#[doc(hidden)]
310pub unsafe fn altlist_set_elt<T>(x: crate::SEXP, i: crate::R_xlen_t, v: crate::SEXP)
311where
312 T: crate::altrep_traits::AltList,
313{
314 use crate::SexpExt;
315
316 unsafe {
317 // R disables GC around Set_elt, but a direct caller may not: keep `v`
318 // and the new list live while the element reads allocate.
319 let v = crate::OwnedProtect::new(v);
320 let values = match crate::altrep_data::materialized_altrep_data2(x) {
321 Some(values) => values,
322 None => {
323 let n = T::length(x);
324 let values = crate::OwnedProtect::new(crate::SEXP::alloc_list(n));
325 for j in 0..n {
326 values.get().set_vector_elt(j, T::elt(x, j));
327 }
328 crate::altrep_ext::AltrepSexpExt::set_altrep_data2(&x, values.get());
329 values.get()
330 }
331 };
332 values.set_vector_elt(i, v.get());
333 }
334}
335// endregion
336
337// region: Centralized ALTREP buffer access helper
338
339/// Create a mutable slice from an ALTREP `get_region` output buffer pointer.
340///
341/// Called by the bridge trampolines (`altrep_bridge.rs`) to convert the raw
342/// `*mut T` buffer from R's ALTREP dispatch into a `&mut [T]` before passing
343/// it to the trait methods.
344///
345/// # Safety
346///
347/// - `buf` must be a valid, aligned, writable pointer to at least `len` elements of `T`.
348/// - The caller must ensure no aliasing references to the same memory exist.
349/// - This is guaranteed when called from R's ALTREP `Get_region` dispatch, which
350/// provides a freshly allocated buffer.
351#[inline]
352pub unsafe fn altrep_region_buf<T>(buf: *mut T, len: usize) -> &'static mut [T] {
353 unsafe { std::slice::from_raw_parts_mut(buf, len) }
354}
355// endregion
356
357// `macros` is declared first so its `#[macro_export]` items are textually
358// in-scope before the `builtins`/`arrays`/`static_slices` modules that invoke
359// them via `$crate::__impl_alt_*` paths.
360pub mod macros;
361
362pub mod arrays;
363pub mod buffers;
364pub mod builtins;
365pub mod shared_slices;
366pub mod static_slices;