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* — `TryFromSexp` hands back
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_sexp`], 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//! As with `Cow<[T]>`, a borrowed `RCow` is valid only for the duration of the
36//! enclosing `.Call` (R protects the argument SEXP while Rust runs). Write
37//! `RCow<'_, T>` at a `#[miniextendr]` boundary: the `'a` leashes the borrow to
38//! the call, so sending it to another thread or storing it past the call return
39//! is a *compile error*, not an honor-system pitfall.
40
41use std::ops::Deref;
42
43use crate::from_r::{SexpTypeError, TryFromSexp};
44use crate::into_r::IntoR;
45use crate::{RNativeType, SEXP};
46
47/// Borrowed arm of [`RCow`]: a whole-vector view that remembers its source SEXP.
48///
49/// Fields are private by design — the only constructor is
50/// [`RCow::try_from_sexp`], so a borrowed view can never be a sub-slice. That
51/// invariant is what lets [`IntoR`] return the source SEXP zero-copy without the
52/// provenance-free pointer probe that `Cow<[T]>` required (#880).
53pub struct RBorrow<'a, T> {
54 /// Source R vector. Valid for the duration of the enclosing `.Call`.
55 sexp: SEXP,
56 /// View over `sexp`'s data — always the whole vector (see invariant above).
57 data: &'a [T],
58}
59
60impl<T> RBorrow<'_, T> {
61 /// The borrowed view (the whole source vector).
62 #[inline]
63 pub fn as_slice(&self) -> &[T] {
64 self.data
65 }
66
67 /// The source R vector this view borrows from.
68 #[inline]
69 pub fn source_sexp(&self) -> SEXP {
70 self.sexp
71 }
72}
73
74/// An R-aware copy-on-write slice — the safe, zero-copy-round-trip alternative
75/// to [`std::borrow::Cow<[T]>`](std::borrow::Cow).
76///
77/// See the [module docs](self) for why this exists and how it closes the #880
78/// hazard. In brief: the [`Borrowed`](RCow::Borrowed) arm carries its source
79/// SEXP, so returning it to R is a direct hand-back rather than a speculative
80/// pointer recovery.
81///
82/// # Example
83///
84/// ```ignore
85/// // Zero-copy in *and* out: the returned SEXP is the original R vector.
86/// #[miniextendr]
87/// pub fn passthrough(x: RCow<'_, f64>) -> RCow<'_, f64> {
88/// x
89/// }
90///
91/// // Mutating forces a copy (copy-on-write), then materializes a fresh vector.
92/// #[miniextendr]
93/// pub fn doubled(mut x: RCow<'_, f64>) -> RCow<'_, f64> {
94/// for v in x.to_mut() {
95/// *v *= 2.0;
96/// }
97/// x
98/// }
99/// ```
100pub enum RCow<'a, T> {
101 /// Zero-copy view of a whole R vector, carrying its source SEXP.
102 Borrowed(RBorrow<'a, T>),
103 /// Owned data; materializes a fresh R vector on [`IntoR`].
104 Owned(Vec<T>),
105}
106
107impl<T> RCow<'_, T> {
108 /// `true` if this is a borrowed (zero-copy) view of an R vector.
109 #[inline]
110 pub fn is_borrowed(&self) -> bool {
111 matches!(self, RCow::Borrowed(_))
112 }
113
114 /// `true` if this owns its data.
115 #[inline]
116 pub fn is_owned(&self) -> bool {
117 matches!(self, RCow::Owned(_))
118 }
119}
120
121impl<T: Clone> RCow<'_, T> {
122 /// Acquire a mutable reference to the owned data, cloning out of R first if
123 /// borrowed (copy-on-write). After this the `RCow` is always
124 /// [`Owned`](RCow::Owned).
125 pub fn to_mut(&mut self) -> &mut Vec<T> {
126 match self {
127 RCow::Borrowed(b) => {
128 *self = RCow::Owned(b.data.to_vec());
129 match self {
130 RCow::Owned(v) => v,
131 // The line above just assigned `Owned`.
132 RCow::Borrowed(_) => unreachable!(),
133 }
134 }
135 RCow::Owned(v) => v,
136 }
137 }
138
139 /// Consume into an owned [`Vec<T>`], cloning out of R if borrowed.
140 pub fn into_owned(self) -> Vec<T> {
141 match self {
142 RCow::Borrowed(b) => b.data.to_vec(),
143 RCow::Owned(v) => v,
144 }
145 }
146}
147
148impl<T> Deref for RCow<'_, T> {
149 type Target = [T];
150 #[inline]
151 fn deref(&self) -> &[T] {
152 match self {
153 RCow::Borrowed(b) => b.data,
154 RCow::Owned(v) => v,
155 }
156 }
157}
158
159impl<T> From<Vec<T>> for RCow<'_, T> {
160 #[inline]
161 fn from(v: Vec<T>) -> Self {
162 RCow::Owned(v)
163 }
164}
165
166/// Reads an R vector zero-copy, remembering the source SEXP so it can be handed
167/// straight back by [`IntoR`]. Rejects a mismatched [`SEXPTYPE`](crate::SEXPTYPE)
168/// just like the `&[T]` impl it delegates to.
169///
170/// The impl is generic over `'a` so a borrowed `RCow<'_, T>` argument is leashed
171/// to the enclosing `.Call` (R protects the argument SEXP for the call's
172/// duration). Storing it past the call return — in an `ExternalPtr`, a global,
173/// or another thread — is a compile error, not an honor-system hazard.
174impl<'a, T: RNativeType> TryFromSexp for RCow<'a, T> {
175 type Error = SexpTypeError;
176
177 #[inline]
178 fn try_from_sexp(sexp: SEXP) -> Result<Self, Self::Error> {
179 // The `&[T]` impl fabricates &'static; covariance narrows it to &'a.
180 let data: &'static [T] = TryFromSexp::try_from_sexp(sexp)?;
181 Ok(RCow::Borrowed(RBorrow { sexp, data }))
182 }
183
184 #[inline]
185 unsafe fn try_from_sexp_unchecked(sexp: SEXP) -> Result<Self, Self::Error> {
186 let data: &'static [T] = unsafe { TryFromSexp::try_from_sexp_unchecked(sexp)? };
187 Ok(RCow::Borrowed(RBorrow { sexp, data }))
188 }
189}
190
191/// Returns the source SEXP unchanged for the borrowed arm (true zero-copy), or
192/// materializes a fresh R vector for the owned arm.
193impl<T: RNativeType> IntoR for RCow<'_, T> {
194 type Error = std::convert::Infallible;
195
196 #[inline]
197 fn try_into_sexp(self) -> Result<SEXP, Self::Error> {
198 Ok(self.into_sexp())
199 }
200
201 #[inline]
202 unsafe fn try_into_sexp_unchecked(self) -> Result<SEXP, Self::Error> {
203 Ok(unsafe { self.into_sexp_unchecked() })
204 }
205
206 #[inline]
207 fn into_sexp(self) -> SEXP {
208 match self {
209 // Invariant (enforced by `RBorrow`'s private fields): a borrowed
210 // `RCow` always spans its entire source vector, so the source SEXP
211 // *is* the correct result — hand it back directly. No
212 // `data_ptr − header` probe, hence none of the #880 / #867
213 // speculative-read hazard.
214 RCow::Borrowed(b) => b.sexp,
215 // Copies via the `&[T]` impl.
216 RCow::Owned(v) => v.as_slice().into_sexp(),
217 }
218 }
219
220 #[inline]
221 unsafe fn into_sexp_unchecked(self) -> SEXP {
222 match self {
223 RCow::Borrowed(b) => b.sexp,
224 RCow::Owned(v) => unsafe { v.as_slice().into_sexp_unchecked() },
225 }
226 }
227}
228
229#[cfg(test)]
230mod tests {
231 use super::*;
232
233 // The borrowed arm's behavior (TryFromSexp / zero-copy IntoR) requires a
234 // live R session and is exercised by the rpkg `zero_copy_rcow_*` fixtures
235 // and the `gc_stress_rcow_roundtrip` gctorture guard. These unit tests cover
236 // the R-independent surface (the owned arm and the Cow-like helpers).
237
238 #[test]
239 fn owned_deref_and_into_owned() {
240 let c: RCow<'static, i32> = RCow::Owned(vec![1, 2, 3]);
241 assert!(c.is_owned());
242 assert!(!c.is_borrowed());
243 assert_eq!(&*c, &[1, 2, 3]);
244 assert_eq!(c.into_owned(), vec![1, 2, 3]);
245 }
246
247 #[test]
248 fn to_mut_on_owned_mutates_in_place() {
249 let mut c: RCow<'static, f64> = RCow::Owned(vec![1.0]);
250 c.to_mut().push(2.0);
251 assert_eq!(&*c, &[1.0, 2.0]);
252 }
253
254 #[test]
255 fn from_vec_is_owned() {
256 let c: RCow<'static, i32> = vec![9, 8, 7].into();
257 assert!(c.is_owned());
258 assert_eq!(c.len(), 3);
259 }
260}