miniextendr_api/rcow.rs
1//! [`RCow`] — an R-aware copy-on-write slice.
2//!
3//! Like [`std::borrow::Cow<[T]>`](std::borrow::Cow), but the borrowed arm
4//! carries the *source SEXP* it was read from. That single extra field is what
5//! makes a zero-copy round-trip back to R **sound**, where `Cow<[T]>` could not
6//! be (see #880).
7//!
8//! # The `Cow<[T]>` hazard this replaces
9//!
10//! `Cow<[T]>` is zero-copy on the way *in* through `TryFromRoot`, which returns
11//! `Cow::Borrowed(&[T])` pointing straight at R's vector data. But it cannot be
12//! returned to R zero-copy *safely*. A bare `&[T]` carries no provenance, so a
13//! borrowed **sub-slice** (`&cow[2..5]`) is byte-for-byte indistinguishable from
14//! a full borrow, yet `as_ptr()` points into the *middle* of the R vector. The
15//! old recovery path computed `data_ptr − SEXPREC_header` and speculatively read
16//! there — landing mid-header for a sub-slice (false-positive corruption) or off
17//! the front of a Rust-allocated buffer entirely (segfault — the same class of
18//! bug fixed for Arrow in #867). Arrow could keep zero-copy because
19//! `arrow::Buffer` *proves* it is unsliced and R-backed (`ptr_offset`/
20//! `capacity`); a `&[T]` proves nothing.
21//!
22//! # How `RCow` fixes it structurally
23//!
24//! [`RCow::Borrowed`] wraps [`RBorrow`], whose fields are private. The *only*
25//! constructor is [`RCow::try_from_root`], which always borrows a whole vector.
26//! There is no way to build a borrowed `RCow` over a sub-range (slice via
27//! [`Deref`] to get a plain `&[T]` instead). So a borrowed `RCow` always spans
28//! its entire source vector, and [`IntoR`] simply returns the stored SEXP — no
29//! pointer arithmetic, no speculative read, no hazard. Need to mutate or reshape?
30//! [`to_mut`](RCow::to_mut) / [`into_owned`](RCow::into_owned) copy out into an
31//! owned [`Vec<T>`].
32//!
33//! # Lifetime contract
34//!
35//! A borrowed `RCow` is tied to the lifetime-carrying [`Root`](crate::Root)
36//! passed to `try_from_root`. Generated wrappers borrow that root from the
37//! live `.Call` argument slot. Write `RCow<'_, T>`: a `'static` parameter
38//! would require the call's local root to live forever and is rejected.
39//! Raw `TryFromSexp` is unavailable for `RCow`; callers outside wrappers use
40//! a [`ProtectScope`](crate::ProtectScope) root.
41
42use std::ops::Deref;
43
44use crate::from_r::{SexpTypeError, TryFromSexp};
45use crate::into_r::IntoR;
46use crate::{RNativeType, SEXP};
47
48/// Borrowed arm of [`RCow`]: a whole-vector view that remembers its source SEXP.
49///
50/// Fields are private by design — the only constructor is
51/// [`RCow::try_from_root`], so a borrowed view can never be a sub-slice. That
52/// invariant is what lets [`IntoR`] return the source SEXP zero-copy without the
53/// provenance-free pointer probe that `Cow<[T]>` required (#880).
54pub struct RBorrow<'a, T> {
55 /// Source R vector. Valid for the duration of the enclosing `.Call`.
56 sexp: SEXP,
57 /// View over `sexp`'s data — always the whole vector (see invariant above).
58 data: &'a [T],
59}
60
61impl<T> RBorrow<'_, T> {
62 /// The borrowed view (the whole source vector).
63 #[inline]
64 pub fn as_slice(&self) -> &[T] {
65 self.data
66 }
67
68 /// The source R vector this view borrows from.
69 #[inline]
70 pub fn source_sexp(&self) -> SEXP {
71 self.sexp
72 }
73}
74
75/// An R-aware copy-on-write slice — the safe, zero-copy-round-trip alternative
76/// to [`std::borrow::Cow<[T]>`](std::borrow::Cow).
77///
78/// See the [module docs](self) for why this exists and how it closes the #880
79/// hazard. In brief: the [`Borrowed`](RCow::Borrowed) arm carries its source
80/// SEXP, so returning it to R is a direct hand-back rather than a speculative
81/// pointer recovery.
82///
83/// # Example
84///
85/// ```ignore
86/// // Zero-copy in *and* out: the returned SEXP is the original R vector.
87/// #[miniextendr]
88/// pub fn passthrough(x: RCow<'_, f64>) -> RCow<'_, f64> {
89/// x
90/// }
91///
92/// // Mutating forces a copy (copy-on-write), then materializes a fresh vector.
93/// #[miniextendr]
94/// pub fn doubled(mut x: RCow<'_, f64>) -> RCow<'_, f64> {
95/// for v in x.to_mut() {
96/// *v *= 2.0;
97/// }
98/// x
99/// }
100/// ```
101pub enum RCow<'a, T> {
102 /// Zero-copy view of a whole R vector, carrying its source SEXP.
103 Borrowed(RBorrow<'a, T>),
104 /// Owned data; materializes a fresh R vector on [`IntoR`].
105 Owned(Vec<T>),
106}
107
108impl<T> RCow<'_, T> {
109 /// `true` if this is a borrowed (zero-copy) view of an R vector.
110 #[inline]
111 pub fn is_borrowed(&self) -> bool {
112 matches!(self, RCow::Borrowed(_))
113 }
114
115 /// `true` if this owns its data.
116 #[inline]
117 pub fn is_owned(&self) -> bool {
118 matches!(self, RCow::Owned(_))
119 }
120}
121
122impl<T: Clone> RCow<'_, T> {
123 /// Acquire a mutable reference to the owned data, cloning out of R first if
124 /// borrowed (copy-on-write). After this the `RCow` is always
125 /// [`Owned`](RCow::Owned).
126 pub fn to_mut(&mut self) -> &mut Vec<T> {
127 match self {
128 RCow::Borrowed(b) => {
129 *self = RCow::Owned(b.data.to_vec());
130 match self {
131 RCow::Owned(v) => v,
132 // The line above just assigned `Owned`.
133 RCow::Borrowed(_) => unreachable!(),
134 }
135 }
136 RCow::Owned(v) => v,
137 }
138 }
139
140 /// Consume into an owned [`Vec<T>`], cloning out of R if borrowed.
141 pub fn into_owned(self) -> Vec<T> {
142 match self {
143 RCow::Borrowed(b) => b.data.to_vec(),
144 RCow::Owned(v) => v,
145 }
146 }
147}
148
149impl<T> Deref for RCow<'_, T> {
150 type Target = [T];
151 #[inline]
152 fn deref(&self) -> &[T] {
153 match self {
154 RCow::Borrowed(b) => b.data,
155 RCow::Owned(v) => v,
156 }
157 }
158}
159
160impl<T> From<Vec<T>> for RCow<'_, T> {
161 #[inline]
162 fn from(v: Vec<T>) -> Self {
163 RCow::Owned(v)
164 }
165}
166
167impl<'a, T: RNativeType> RCow<'a, T> {
168 /// Native storage retained by a borrowed wrapper argument.
169 #[doc(hidden)]
170 pub const NATIVE_BORROW: Option<crate::from_r::NativeBorrow> =
171 <&[T] as TryFromSexp>::NATIVE_BORROW;
172
173 /// Borrow an entire native vector for the lifetime of its root.
174 ///
175 /// Type checking is the same as for `&[T]`. Retaining the source SEXP
176 /// preserves zero-copy round-trip identity; retaining the root's lifetime
177 /// prevents the borrowed value escaping that root.
178 ///
179 /// ```compile_fail
180 /// use miniextendr_api::{Root, RCow};
181 /// fn escape<'a>(root: Root<'a>) -> RCow<'static, i32> {
182 /// RCow::try_from_root(root).unwrap()
183 /// }
184 /// ```
185 #[inline]
186 pub fn try_from_root(root: crate::Root<'a>) -> Result<Self, SexpTypeError> {
187 let sexp = root.get();
188 // The root keeps the source reachable for `'a`, which bounds the
189 // slice; the conversion checks the element type.
190 let data: &'a [T] = TryFromSexp::try_from_sexp(sexp)?;
191 Ok(RCow::Borrowed(RBorrow { sexp, data }))
192 }
193}
194
195/// Returns the source SEXP unchanged for the borrowed arm (true zero-copy), or
196/// materializes a fresh R vector for the owned arm.
197impl<T: RNativeType> IntoR for RCow<'_, T> {
198 type Error = std::convert::Infallible;
199
200 #[inline]
201 fn try_into_sexp(self) -> Result<SEXP, Self::Error> {
202 Ok(self.into_sexp())
203 }
204
205 #[inline]
206 unsafe fn try_into_sexp_unchecked(self) -> Result<SEXP, Self::Error> {
207 Ok(unsafe { self.into_sexp_unchecked() })
208 }
209
210 #[inline]
211 fn into_sexp(self) -> SEXP {
212 match self {
213 // Invariant (enforced by `RBorrow`'s private fields): a borrowed
214 // `RCow` always spans its entire source vector, so the source SEXP
215 // *is* the correct result — hand it back directly. No
216 // `data_ptr − header` probe, hence none of the #880 / #867
217 // speculative-read hazard.
218 RCow::Borrowed(b) => b.sexp,
219 // Copies via the `&[T]` impl.
220 RCow::Owned(v) => v.as_slice().into_sexp(),
221 }
222 }
223
224 #[inline]
225 unsafe fn into_sexp_unchecked(self) -> SEXP {
226 match self {
227 RCow::Borrowed(b) => b.sexp,
228 RCow::Owned(v) => unsafe { v.as_slice().into_sexp_unchecked() },
229 }
230 }
231}
232
233#[cfg(test)]
234mod tests {
235 use super::*;
236
237 // The borrowed arm's behavior (try_from_root / zero-copy IntoR) requires a
238 // live R session and is exercised by the rpkg `zero_copy_rcow_*` fixtures
239 // and the `gc_stress_rcow_roundtrip` gctorture guard. These unit tests cover
240 // the R-independent surface (the owned arm and the Cow-like helpers).
241
242 #[test]
243 fn owned_deref_and_into_owned() {
244 let c: RCow<'static, i32> = RCow::Owned(vec![1, 2, 3]);
245 assert!(c.is_owned());
246 assert!(!c.is_borrowed());
247 assert_eq!(&*c, &[1, 2, 3]);
248 assert_eq!(c.into_owned(), vec![1, 2, 3]);
249 }
250
251 #[test]
252 fn to_mut_on_owned_mutates_in_place() {
253 let mut c: RCow<'static, f64> = RCow::Owned(vec![1.0]);
254 c.to_mut().push(2.0);
255 assert_eq!(&*c, &[1.0, 2.0]);
256 }
257
258 #[test]
259 fn from_vec_is_owned() {
260 let c: RCow<'static, i32> = vec![9, 8, 7].into();
261 assert!(c.is_owned());
262 assert_eq!(c.len(), 3);
263 }
264}