Skip to main content

miniextendr_api/
strvec.rs

1//! Thin wrapper around R character vector (`STRSXP`).
2//!
3//! Provides safe construction and element insertion for string vectors.
4
5use std::borrow::Cow;
6
7use crate::SEXPTYPE::STRSXP;
8use crate::from_r::{SexpError, SexpTypeError, TryFromSexp, charsxp_to_cow, charsxp_to_str};
9use crate::gc_protect::{ChildRoots, OwnedProtect, ProtectScope};
10use crate::into_r::IntoR;
11use crate::{Preserved, SEXP, SexpExt};
12
13/// Borrowed view over an R character vector (`STRSXP`).
14///
15/// Strings returned by the view carry `'a`. Constructors must tie that lifetime
16/// to a root that keeps the source reachable. [`ProtectedStrVec::as_strvec`]
17/// does so through a borrow of its owner.
18///
19/// [`try_from_root`](Self::try_from_root) borrows a lifetime-carrying root.
20/// Generated wrappers use this path for `StrVec<'_>` parameters. Raw
21/// `TryFromSexp` is unavailable: a copyable SEXP cannot constrain the view's
22/// lifetime. Use [`ProtectedStrVec`] when the result must own its root.
23///
24/// `Copy` and cheap (a `SEXP` and a retention reference); covariant in `'a`.
25/// Uncached ALTREP elements read through a rooted view stay alive for `'a`.
26#[derive(#[automatically_derived]
#[doc(hidden)]
unsafe impl<'a> ::core::clone::TrivialClone for StrVec<'a> { }
#[automatically_derived]
impl<'a> ::core::clone::Clone for StrVec<'a> {
    #[inline]
    fn clone(&self) -> Self {
        let _: ::core::clone::AssertParamIsClone<SEXP>;
        let _: ::core::clone::AssertParamIsClone<Option<&'a ChildRoots>>;
        *self
    }
}Clone, #[automatically_derived]
impl<'a> ::core::marker::Copy for StrVec<'a> { }Copy)]
27pub struct StrVec<'a>(SEXP, Option<&'a ChildRoots>);
28
29impl std::fmt::Debug for StrVec<'_> {
30    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
31        f.debug_tuple("StrVec").field(&self.0).finish()
32    }
33}
34
35impl<'a> StrVec<'a> {
36    /// Strings do not retain a pointer to a native numeric vector.
37    #[doc(hidden)]
38    pub const NATIVE_BORROW: Option<crate::from_r::NativeBorrow> = None;
39
40    /// Wrap an existing `STRSXP` without additional checks.
41    ///
42    /// # Safety
43    ///
44    /// Caller must ensure `sexp` is a valid character vector (`STRSXP`)
45    /// that stays GC-reachable for the inferred lifetime `'a`. An ALTREP class
46    /// must also retain every CHARSXP read through this raw view for `'a`;
47    /// otherwise use `try_from_root` or `ProtectedStrVec`.
48    #[inline]
49    pub const unsafe fn from_raw(sexp: SEXP) -> Self {
50        StrVec(sexp, None)
51    }
52
53    /// Get the underlying `SEXP`.
54    #[inline]
55    pub const fn as_sexp(self) -> SEXP {
56        self.0
57    }
58
59    /// Length of the character vector (number of elements).
60    #[inline]
61    pub fn len(self) -> isize {
62        self.0.xlength()
63    }
64
65    /// Returns true if the vector is empty.
66    #[inline]
67    pub fn is_empty(self) -> bool {
68        self.len() == 0
69    }
70
71    /// Get the CHARSXP at the given index.
72    ///
73    /// Returns `None` if out of bounds.
74    #[inline]
75    pub fn get_charsxp(self, idx: isize) -> Option<SEXP> {
76        if idx < 0 || idx >= self.len() {
77            return None;
78        }
79        let charsxp = self.0.string_elt(idx);
80        Some(match self.1 {
81            // The root/owner behind this reference lives for 'a.
82            Some(children) => unsafe { children.protect(self.0, charsxp) },
83            None => charsxp, // from_raw's caller guarantees element retention
84        })
85    }
86
87    /// Get the string at the given index (zero-copy), leashed to `'a`.
88    ///
89    /// Returns `None` if out of bounds or if the element is `NA_character_`.
90    /// Panics if the CHARSXP is not valid UTF-8 (should not happen in a UTF-8 locale).
91    #[inline]
92    pub fn get_str(self, idx: isize) -> Option<&'a str> {
93        let charsxp = self.get_charsxp(idx)?;
94        unsafe {
95            if charsxp == SEXP::na_string() {
96                return None;
97            }
98            // charsxp_to_str fabricates &'static; covariance narrows it to &'a.
99            Some(charsxp_to_str(charsxp))
100        }
101    }
102
103    /// Get the string at the given index as `Cow<str>` (encoding-safe), leashed to `'a`.
104    ///
105    /// Returns `Cow::Borrowed` for UTF-8 strings (zero-copy), `Cow::Owned` for
106    /// non-UTF-8 strings (translated via `Rf_translateCharUTF8`).
107    /// Returns `None` if out of bounds or `NA_character_`.
108    #[inline]
109    pub fn get_cow(self, idx: isize) -> Option<Cow<'a, str>> {
110        let charsxp = self.get_charsxp(idx)?;
111        unsafe {
112            if charsxp == SEXP::na_string() {
113                return None;
114            }
115            Some(charsxp_to_cow(charsxp))
116        }
117    }
118
119    /// Get the string at the given index as `&'static str` — the courageous escape.
120    ///
121    /// Unlike [`get_str`](Self::get_str), the returned reference is **not** leashed
122    /// to `'a`, so it can be stored in an `ExternalPtr`, a global, or sent across
123    /// threads. The string data lives in R's CHARSXP; the CHARSXP must remain
124    /// reachable. For an uncached ALTREP element, retain this view's protection
125    /// scope or `ProtectedStrVec` owner as well as the source vector.
126    ///
127    /// # Safety
128    ///
129    /// The caller must keep the returned CHARSXP reachable for as long as the
130    /// reference is used. Rooting an ordinary source STRSXP suffices; an ALTREP
131    /// source may need this view's scope/owner or a separate root for the child.
132    /// Letting R collect that child is use-after-free.
133    #[inline]
134    pub unsafe fn get_str_static(self, idx: isize) -> Option<&'static str> {
135        let charsxp = self.get_charsxp(idx)?;
136        unsafe {
137            if charsxp == SEXP::na_string() {
138                return None;
139            }
140            Some(charsxp_to_str(charsxp))
141        }
142    }
143
144    /// Get the string at the given index as `Cow<'static, str>` — the courageous escape.
145    ///
146    /// `Cow` analogue of [`get_str_static`](Self::get_str_static); same rooting
147    /// obligation for the borrowed (`Cow::Borrowed`) case.
148    ///
149    /// # Safety
150    ///
151    /// See [`get_str_static`](Self::get_str_static).
152    #[inline]
153    pub unsafe fn get_cow_static(self, idx: isize) -> Option<Cow<'static, str>> {
154        let charsxp = self.get_charsxp(idx)?;
155        unsafe {
156            if charsxp == SEXP::na_string() {
157                return None;
158            }
159            Some(charsxp_to_cow(charsxp))
160        }
161    }
162
163    /// Iterate over elements as `Option<&str>` (leashed to `'a`).
164    ///
165    /// `NA_character_` elements yield `None`, valid strings yield `Some(&str)`.
166    /// Zero-copy — each `&str` borrows directly from R's CHARSXP.
167    #[inline]
168    pub fn iter(self) -> StrVecIter<'a> {
169        StrVecIter {
170            vec: self,
171            idx: 0,
172            len: self.len(),
173        }
174    }
175
176    /// Iterate over elements as `Option<Cow<str>>` (encoding-safe, leashed to `'a`).
177    ///
178    /// Like [`iter`](Self::iter) but handles non-UTF-8 CHARSXPs gracefully.
179    #[inline]
180    pub fn iter_cow(self) -> StrVecCowIter<'a> {
181        StrVecCowIter {
182            vec: self,
183            idx: 0,
184            len: self.len(),
185        }
186    }
187
188    // region: Safe element insertion
189
190    /// Set a CHARSXP at the given index, protecting it during insertion.
191    ///
192    /// This is the safe way to insert a freshly allocated CHARSXP into a string vector.
193    ///
194    /// # Safety
195    ///
196    /// - Must be called from the R main thread
197    /// - `charsxp` must be a valid CHARSXP (from `Rf_mkChar*` or `STRING_ELT`)
198    /// - `self` must be a valid, protected STRSXP
199    ///
200    /// # Panics
201    ///
202    /// Panics if `idx` is out of bounds.
203    #[inline]
204    pub unsafe fn set_charsxp(self, idx: isize, charsxp: SEXP) {
205        if !(idx >= 0 && idx < self.len()) {
    { ::core::panicking::panic_fmt(format_args!("index out of bounds")); }
};assert!(idx >= 0 && idx < self.len(), "index out of bounds");
206        // SAFETY: caller guarantees R main thread and valid SEXPs
207        unsafe {
208            // Protect CHARSXP during SET_STRING_ELT.
209            // Note: Rf_mkCharLenCE returns a CHARSXP that may be from the global
210            // CHARSXP cache, but protection is still needed for newly allocated ones.
211            let _guard = OwnedProtect::new(charsxp);
212            self.0.set_string_elt(idx, charsxp);
213        }
214    }
215
216    /// Set a CHARSXP without protecting it.
217    ///
218    /// # Safety
219    ///
220    /// In addition to the safety requirements of [`set_charsxp`](Self::set_charsxp):
221    /// - The caller must ensure `charsxp` is already protected or from the
222    ///   global CHARSXP cache.
223    #[inline]
224    pub unsafe fn set_charsxp_unchecked(self, idx: isize, charsxp: SEXP) {
225        if true {
    if !(idx >= 0 && idx < self.len()) {
        { ::core::panicking::panic_fmt(format_args!("index out of bounds")); }
    };
};debug_assert!(idx >= 0 && idx < self.len(), "index out of bounds");
226        // SAFETY: caller guarantees charsxp is protected/cached
227        self.0.set_string_elt(idx, charsxp);
228    }
229
230    /// Set an element from a Rust string.
231    ///
232    /// Creates a CHARSXP from the string and inserts it safely.
233    ///
234    /// # Safety
235    ///
236    /// - Must be called from the R main thread
237    /// - `self` must be a valid, protected STRSXP
238    ///
239    /// # Panics
240    ///
241    /// Panics if `idx` is out of bounds.
242    #[inline]
243    pub unsafe fn set_str(self, idx: isize, s: &str) {
244        if !(idx >= 0 && idx < self.len()) {
    { ::core::panicking::panic_fmt(format_args!("index out of bounds")); }
};assert!(idx >= 0 && idx < self.len(), "index out of bounds");
245        // SAFETY: caller guarantees R main thread
246        unsafe {
247            let charsxp = SEXP::charsxp(s);
248            // CHARSXP may be cached, but protect anyway for safety
249            let _guard = OwnedProtect::new(charsxp);
250            self.0.set_string_elt(idx, charsxp);
251        }
252    }
253
254    /// Set an element to `NA_character_`.
255    ///
256    /// # Safety
257    ///
258    /// - Must be called from the R main thread
259    /// - `self` must be a valid, protected STRSXP
260    ///
261    /// # Panics
262    ///
263    /// Panics if `idx` is out of bounds.
264    #[inline]
265    pub unsafe fn set_na(self, idx: isize) {
266        if !(idx >= 0 && idx < self.len()) {
    { ::core::panicking::panic_fmt(format_args!("index out of bounds")); }
};assert!(idx >= 0 && idx < self.len(), "index out of bounds");
267        // R_NaString is a global constant, no protection needed
268        self.0.set_string_elt(idx, SEXP::na_string());
269    }
270
271    /// Set an element from an optional string.
272    ///
273    /// `None` becomes `NA_character_`.
274    ///
275    /// # Safety
276    ///
277    /// - Must be called from the R main thread
278    /// - `self` must be a valid, protected STRSXP
279    ///
280    /// # Panics
281    ///
282    /// Panics if `idx` is out of bounds.
283    #[inline]
284    pub unsafe fn set_opt_str(self, idx: isize, s: Option<&str>) {
285        match s {
286            Some(s) => unsafe { self.set_str(idx, s) },
287            None => unsafe { self.set_na(idx) },
288        }
289    }
290    // endregion
291}
292
293// region: StrVec iterators
294
295/// Iterator over `StrVec` elements as `Option<&str>` (leashed to `'a`).
296///
297/// Yields `None` for `NA_character_`, `Some(&str)` for valid strings.
298/// Zero-copy — each `&str` borrows directly from R's CHARSXP.
299pub struct StrVecIter<'a> {
300    vec: StrVec<'a>,
301    idx: isize,
302    len: isize,
303}
304
305impl<'a> Iterator for StrVecIter<'a> {
306    type Item = Option<&'a str>;
307
308    #[inline]
309    fn next(&mut self) -> Option<Self::Item> {
310        if self.idx >= self.len {
311            return None;
312        }
313        let value = self.vec.get_str(self.idx);
314        self.idx += 1;
315        Some(value)
316    }
317
318    #[inline]
319    fn size_hint(&self) -> (usize, Option<usize>) {
320        let remaining = (self.len - self.idx) as usize;
321        (remaining, Some(remaining))
322    }
323}
324
325impl ExactSizeIterator for StrVecIter<'_> {}
326
327/// Iterator over `StrVec` elements as `Option<Cow<'a, str>>`.
328///
329/// Like [`StrVecIter`] but handles non-UTF-8 CHARSXPs via `Rf_translateCharUTF8`.
330pub struct StrVecCowIter<'a> {
331    vec: StrVec<'a>,
332    idx: isize,
333    len: isize,
334}
335
336impl<'a> Iterator for StrVecCowIter<'a> {
337    type Item = Option<Cow<'a, str>>;
338
339    #[inline]
340    fn next(&mut self) -> Option<Self::Item> {
341        if self.idx >= self.len {
342            return None;
343        }
344        let value = self.vec.get_cow(self.idx);
345        self.idx += 1;
346        Some(value)
347    }
348
349    #[inline]
350    fn size_hint(&self) -> (usize, Option<usize>) {
351        let remaining = (self.len - self.idx) as usize;
352        (remaining, Some(remaining))
353    }
354}
355
356impl ExactSizeIterator for StrVecCowIter<'_> {}
357
358impl<'a> IntoIterator for StrVec<'a> {
359    type Item = Option<&'a str>;
360    type IntoIter = StrVecIter<'a>;
361
362    #[inline]
363    fn into_iter(self) -> Self::IntoIter {
364        self.iter()
365    }
366}
367
368// endregion
369
370// region: StrVecBuilder - efficient batch string vector construction
371
372/// Builder for constructing string vectors with efficient protection management.
373///
374/// # Example
375///
376/// ```ignore
377/// unsafe fn build_strvec(strings: &[&str]) -> SEXP {
378///     let scope = ProtectScope::new();
379///     let builder = StrVecBuilder::new(&scope, strings.len() as isize);
380///
381///     for (i, s) in strings.iter().enumerate() {
382///         builder.set_str(i as isize, s);
383///     }
384///
385///     builder.into_sexp()
386/// }
387/// ```
388pub struct StrVecBuilder<'a> {
389    vec: SEXP,
390    _scope: &'a ProtectScope,
391}
392
393impl<'a> StrVecBuilder<'a> {
394    /// Create a new string vector builder with the given length.
395    ///
396    /// # Safety
397    ///
398    /// Must be called from the R main thread.
399    #[inline]
400    pub unsafe fn new(scope: &'a ProtectScope, len: usize) -> Self {
401        // SAFETY: caller guarantees R main thread
402        let vec = unsafe { scope.alloc_character(len).into_raw() };
403        Self { vec, _scope: scope }
404    }
405
406    /// Create a new string vector builder via the **unchecked** FFI allocation path.
407    ///
408    /// `_unchecked` twin of [`new`](Self::new): the STRSXP is allocated with
409    /// `Rf_allocVector_unchecked` (see [`ProtectScope::alloc_character_unchecked`]),
410    /// bypassing the main-thread assertion. Use inside ALTREP callbacks,
411    /// `with_r_unwind_protect`, or `with_r_thread` bodies, and pair element
412    /// insertion with the `_unchecked` string-element setters.
413    ///
414    /// # Safety
415    ///
416    /// Must be called from the R main thread, in a context where the checked-FFI
417    /// assertion is intentionally bypassed (see CLAUDE.md "FFI thread checking").
418    #[inline]
419    pub unsafe fn new_unchecked(scope: &'a ProtectScope, len: usize) -> Self {
420        // SAFETY: caller guarantees R main thread in a checked-bypass context.
421        let vec = unsafe { scope.alloc_character_unchecked(len).into_raw() };
422        Self { vec, _scope: scope }
423    }
424
425    /// Set an element from a Rust string.
426    ///
427    /// # Safety
428    ///
429    /// Must be called from the R main thread.
430    #[inline]
431    pub unsafe fn set_str(&self, idx: isize, s: &str) {
432        if true {
    if !(idx >= 0 && idx < self.vec.xlength()) {
        ::core::panicking::panic("assertion failed: idx >= 0 && idx < self.vec.xlength()")
    };
};debug_assert!(idx >= 0 && idx < self.vec.xlength());
433        let charsxp = SEXP::charsxp(s);
434        self.vec.set_string_elt(idx, charsxp);
435    }
436
437    /// Set an element to `NA_character_`.
438    ///
439    /// # Safety
440    ///
441    /// Must be called from the R main thread.
442    #[inline]
443    pub unsafe fn set_na(&self, idx: isize) {
444        if true {
    if !(idx >= 0 && idx < self.vec.xlength()) {
        ::core::panicking::panic("assertion failed: idx >= 0 && idx < self.vec.xlength()")
    };
};debug_assert!(idx >= 0 && idx < self.vec.xlength());
445        self.vec.set_string_elt(idx, SEXP::na_string());
446    }
447
448    /// Set an element from an optional string.
449    ///
450    /// # Safety
451    ///
452    /// Must be called from the R main thread.
453    #[inline]
454    pub unsafe fn set_opt_str(&self, idx: isize, s: Option<&str>) {
455        match s {
456            // SAFETY: caller guarantees R main thread
457            Some(s) => unsafe { self.set_str(idx, s) },
458            None => unsafe { self.set_na(idx) },
459        }
460    }
461
462    /// Get the underlying SEXP.
463    #[inline]
464    pub fn as_sexp(&self) -> SEXP {
465        self.vec
466    }
467
468    /// Convert to a `StrVec` view, leashed to the builder's protect scope `'a`.
469    #[inline]
470    pub fn into_strvec(self) -> StrVec<'a> {
471        // The builder allocates an ordinary STRSXP, whose elements are traced
472        // directly. Its scope already roots that vector for 'a.
473        StrVec(self.vec, None)
474    }
475
476    /// Convert to the underlying SEXP.
477    #[inline]
478    pub fn into_sexp(self) -> SEXP {
479        self.vec
480    }
481
482    /// Get the length.
483    #[inline]
484    pub fn len(&self) -> isize {
485        self.vec.xlength()
486    }
487
488    /// Check if empty.
489    #[inline]
490    pub fn is_empty(&self) -> bool {
491        self.len() == 0
492    }
493}
494// endregion
495
496// region: Trait implementations
497
498impl IntoR for StrVec<'_> {
499    type Error = std::convert::Infallible;
500    fn try_into_sexp(self) -> Result<SEXP, Self::Error> {
501        Ok(self.into_sexp())
502    }
503    unsafe fn try_into_sexp_unchecked(self) -> Result<SEXP, Self::Error> {
504        self.try_into_sexp()
505    }
506    #[inline]
507    fn into_sexp(self) -> SEXP {
508        self.0
509    }
510}
511
512impl<'a> StrVec<'a> {
513    /// Borrow a character vector for the lifetime of its root.
514    ///
515    /// The result cannot acquire a lifetime longer than the supplied root.
516    /// Raw `TryFromSexp` is intentionally unavailable for this borrowed view.
517    ///
518    /// ```compile_fail
519    /// use miniextendr_api::{Root, StrVec};
520    /// fn escape<'a>(root: Root<'a>) -> StrVec<'static> {
521    ///     StrVec::try_from_root(root).unwrap()
522    /// }
523    /// ```
524    pub fn try_from_root(root: crate::Root<'a>) -> Result<Self, SexpError> {
525        let sexp = root.get();
526        let actual = sexp.type_of();
527        if actual != STRSXP {
528            return Err(SexpTypeError {
529                expected: STRSXP,
530                actual,
531            }
532            .into());
533        }
534        // The root, not an unconstrained return annotation, fixes `'a`.
535        Ok(StrVec(sexp, Some(root.child_roots())))
536    }
537}
538// endregion
539
540// region: ProtectedStrVec — GC-protected string vector with proper lifetimes
541
542/// GC-protected view over an R character vector (`STRSXP`).
543///
544/// Unlike [`StrVec`] (which is `Copy` and trusts the caller for GC protection),
545/// `ProtectedStrVec` holds a [`Preserved`] root on the STRSXP for its Rust
546/// lifetime. It can outlive a `.Call` frame and be dropped in any order
547/// relative to other handles.
548/// Borrowed strings and iterators are tied to `&self`. It is `!Send + !Sync`:
549/// reading and releasing the R object must happen on R's main thread.
550///
551/// # When to use
552///
553/// - **`StrVec`**: for SEXP arguments to `.Call` (R protects them), or when you
554///   manage protection yourself. Lightweight, `Copy`.
555/// - **`ProtectedStrVec`**: when you allocate or receive an STRSXP and need to
556///   keep it alive beyond the immediate scope. Not `Copy`.
557///
558/// # Example
559///
560/// ```ignore
561/// #[miniextendr]
562/// pub fn count_unique(strings: ProtectedStrVec) -> i32 {
563///     let unique: HashSet<&str> = strings.iter()
564///         .filter_map(|s| s)
565///         .collect();
566///     unique.len() as i32
567/// }
568/// ```
569pub struct ProtectedStrVec {
570    sexp: SEXP,
571    /// `None` for [`from_sexp_trusted`](Self::from_sexp_trusted), whose caller
572    /// roots `sexp`. The field also makes the handle `!Send + !Sync`.
573    _root: Option<Preserved>,
574    children: ChildRoots,
575    len: isize,
576}
577
578impl ProtectedStrVec {
579    /// Create a protected view over an STRSXP.
580    ///
581    /// Roots the SEXP with a [`Preserved`] handle, released on drop,
582    /// independently of R's protection stack. Use
583    /// [`from_sexp_trusted`](Self::from_sexp_trusted) only when another owner
584    /// guarantees reachability for this handle's entire lifetime.
585    ///
586    /// # Safety
587    ///
588    /// - `sexp` must be a valid STRSXP.
589    /// - Must be called from the R main thread.
590    #[inline]
591    pub unsafe fn new(sexp: SEXP) -> Self {
592        // Root before the length read, which can run an ALTREP callback.
593        let root = Preserved::new(sexp);
594        Self {
595            sexp,
596            len: sexp.xlength(),
597            _root: Some(root),
598            children: ChildRoots::default(),
599        }
600    }
601
602    /// Create a view without adding GC protection.
603    ///
604    /// Use this when the SEXP is already protected by R (e.g., a `.Call`
605    /// argument, or in a `ProtectScope`) for the handle's entire lifetime.
606    /// Avoids an additional preserved root.
607    ///
608    /// The lifetime-bound `&str` borrows are still enforced — this only
609    /// skips acquiring a root; the unsafe caller must supply it.
610    ///
611    /// # Safety
612    ///
613    /// - `sexp` must be a valid STRSXP.
614    /// - `sexp` must remain GC-protected for the lifetime of this struct.
615    /// - Must be called from the R main thread.
616    #[inline]
617    pub unsafe fn from_sexp_trusted(sexp: SEXP) -> Self {
618        Self {
619            sexp,
620            _root: None,
621            children: ChildRoots::default(),
622            len: sexp.xlength(),
623        }
624    }
625
626    /// Number of elements.
627    #[inline]
628    pub fn len(&self) -> isize {
629        self.len
630    }
631
632    /// Whether the vector is empty.
633    #[inline]
634    pub fn is_empty(&self) -> bool {
635        self.len == 0
636    }
637
638    /// Get the string at index (zero-copy, lifetime tied to `&self`).
639    ///
640    /// Returns `None` for out-of-bounds or `NA_character_`.
641    #[inline]
642    pub fn get_str(&self, idx: isize) -> Option<&str> {
643        self.as_strvec().get_str(idx)
644    }
645
646    /// Get the string at index as `Cow<str>` (encoding-safe, lifetime tied to `&self`).
647    #[inline]
648    pub fn get_cow(&self, idx: isize) -> Option<Cow<'_, str>> {
649        self.as_strvec().get_cow(idx)
650    }
651
652    /// Iterate over elements as `Option<&str>` (lifetime tied to `&self`).
653    #[inline]
654    pub fn iter(&self) -> ProtectedStrVecIter<'_> {
655        ProtectedStrVecIter {
656            vec: self,
657            idx: 0,
658            len: self.len,
659        }
660    }
661
662    /// Iterate over elements as `Option<Cow<str>>` (encoding-safe).
663    #[inline]
664    pub fn iter_cow(&self) -> ProtectedStrVecCowIter<'_> {
665        ProtectedStrVecCowIter {
666            vec: self,
667            idx: 0,
668            len: self.len,
669        }
670    }
671
672    /// Get the underlying SEXP (still protected by this handle).
673    #[inline]
674    pub fn as_sexp(&self) -> SEXP {
675        self.sexp
676    }
677
678    /// Get the inner `StrVec` view, leashed to `&self` (the protect guard).
679    #[inline]
680    pub fn as_strvec(&self) -> StrVec<'_> {
681        // The owned root (or from_sexp_trusted's caller) keeps the parent
682        // reachable; uncached children live in this owner's pool. Both outlive
683        // the borrowed view, without materializing or changing the source.
684        StrVec(self.sexp, Some(&self.children))
685    }
686}
687
688/// Iterator over `ProtectedStrVec` with lifetime tied to the protection guard.
689pub struct ProtectedStrVecIter<'a> {
690    vec: &'a ProtectedStrVec,
691    idx: isize,
692    len: isize,
693}
694
695impl<'a> Iterator for ProtectedStrVecIter<'a> {
696    type Item = Option<&'a str>;
697
698    #[inline]
699    fn next(&mut self) -> Option<Self::Item> {
700        if self.idx >= self.len {
701            return None;
702        }
703        let result = self.vec.get_str(self.idx);
704        self.idx += 1;
705        // get_str returns None for NA; we need to distinguish "end of iter" from "NA element"
706        // Wrap: Some(None) = NA, Some(Some(&str)) = value, None = end
707        Some(result)
708    }
709
710    #[inline]
711    fn size_hint(&self) -> (usize, Option<usize>) {
712        let remaining = (self.len - self.idx) as usize;
713        (remaining, Some(remaining))
714    }
715}
716
717impl ExactSizeIterator for ProtectedStrVecIter<'_> {}
718
719/// Encoding-safe iterator over `ProtectedStrVec`.
720pub struct ProtectedStrVecCowIter<'a> {
721    vec: &'a ProtectedStrVec,
722    idx: isize,
723    len: isize,
724}
725
726impl<'a> Iterator for ProtectedStrVecCowIter<'a> {
727    type Item = Option<Cow<'a, str>>;
728
729    #[inline]
730    fn next(&mut self) -> Option<Self::Item> {
731        if self.idx >= self.len {
732            return None;
733        }
734        let result = self.vec.get_cow(self.idx);
735        self.idx += 1;
736        Some(result)
737    }
738
739    #[inline]
740    fn size_hint(&self) -> (usize, Option<usize>) {
741        let remaining = (self.len - self.idx) as usize;
742        (remaining, Some(remaining))
743    }
744}
745
746impl ExactSizeIterator for ProtectedStrVecCowIter<'_> {}
747
748impl<'a> IntoIterator for &'a ProtectedStrVec {
749    type Item = Option<&'a str>;
750    type IntoIter = ProtectedStrVecIter<'a>;
751
752    #[inline]
753    fn into_iter(self) -> Self::IntoIter {
754        self.iter()
755    }
756}
757
758impl IntoR for ProtectedStrVec {
759    type Error = std::convert::Infallible;
760    fn try_into_sexp(self) -> Result<SEXP, Self::Error> {
761        Ok(self.as_sexp())
762    }
763    unsafe fn try_into_sexp_unchecked(self) -> Result<SEXP, Self::Error> {
764        Ok(self.as_sexp())
765    }
766    #[inline]
767    fn into_sexp(self) -> SEXP {
768        self.as_sexp()
769    }
770}
771
772impl TryFromSexp for ProtectedStrVec {
773    type Error = SexpError;
774    const CHARACTER_ONLY: bool = true;
775
776    fn try_from_sexp(sexp: SEXP) -> Result<Self, Self::Error> {
777        let actual = sexp.type_of();
778        if actual != STRSXP {
779            return Err(SexpTypeError {
780                expected: STRSXP,
781                actual,
782            }
783            .into());
784        }
785        // A raw conversion has no frame lifetime. Its result must own a root
786        // even when the input happens to be a .Call argument.
787        Ok(unsafe { ProtectedStrVec::new(sexp) })
788    }
789}
790
791impl std::fmt::Debug for ProtectedStrVec {
792    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
793        f.debug_struct("ProtectedStrVec")
794            .field("len", &self.len)
795            .finish()
796    }
797}
798// endregion