Skip to main content

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}