Skip to main content

miniextendr_api/
cached_class.rs

1//! Cached R class attribute SEXPs.
2//!
3//! Frequently-used class vectors (like `c("POSIXct", "POSIXt")`) and attribute
4//! values are allocated once and preserved permanently. This avoids repeated
5//! `Rf_mkCharLenCE` hash lookups on hot paths.
6//!
7//! Two declarative macros handle the boilerplate:
8//!
9//! ```ignore
10//! // Cache a symbol (Rf_install result):
11//! cached_symbol!(pub(crate) fn tzone_symbol() = c"tzone");
12//!
13//! // Cache a STRSXP vector (class, names, etc.):
14//! cached_strsxp!(pub(crate) fn posixct_class_sexp() = [c"POSIXct", c"POSIXt"]);
15//! cached_strsxp!(pub(crate) fn date_class_sexp() = [c"Date"]);
16//! ```
17//!
18//! Both expand to a function with a `static OnceLock<SEXP>` inside.
19//! First call initializes; subsequent calls are a single atomic load.
20//!
21//! CHARSXPs are obtained via `Rf_install` + `PRINTNAME` — symbols are never
22//! collected, so the CHARSXP is permanently valid. STRSXP vectors are kept
23//! alive via `R_PreserveObject`.
24
25// region: Macros
26
27/// Cache an `Rf_install` symbol result.
28///
29/// Expands to a function that returns the cached SEXP. First call does the
30/// `Rf_install`; subsequent calls are a single atomic load.
31///
32/// ```ignore
33/// cached_symbol!(pub(crate) fn tzone_symbol() = c"tzone");
34///
35/// // With feature gate:
36/// cached_symbol!(#[cfg(feature = "time")] pub(crate) fn tzone_symbol() = c"tzone");
37/// ```
38macro_rules! cached_symbol {
39    ($(#[$meta:meta])* $vis:vis fn $name:ident() = $cstr:expr) => {
40        $(#[$meta])*
41        $vis fn $name() -> $crate::SEXP {
42            static CACHE: ::std::sync::OnceLock<$crate::SEXP> = ::std::sync::OnceLock::new();
43            *CACHE.get_or_init(|| unsafe { $crate::sys::Rf_install($cstr.as_ptr()) })
44        }
45    };
46}
47#[allow(unused_imports)] // exported for use by other modules
48pub(crate) use cached_symbol;
49
50/// Cache a STRSXP vector built from permanent CHARSXPs.
51///
52/// Each element is a `&CStr` literal routed through `Rf_install` + `PRINTNAME`
53/// for a never-GC'd CHARSXP. The STRSXP itself is kept alive via
54/// `R_PreserveObject`.
55///
56/// ```ignore
57/// // Single-element class:
58/// cached_strsxp!(pub(crate) fn date_class_sexp() = [c"Date"]);
59///
60/// // Multi-element class:
61/// cached_strsxp!(pub(crate) fn posixct_class_sexp() = [c"POSIXct", c"POSIXt"]);
62///
63/// // With feature gate:
64/// cached_strsxp!(
65///     #[cfg(any(feature = "time", feature = "arrow"))]
66///     pub(crate) fn posixct_class_sexp() = [c"POSIXct", c"POSIXt"]
67/// );
68/// ```
69macro_rules! cached_strsxp {
70    ($(#[$meta:meta])* $vis:vis fn $name:ident() = [$($cstr:expr),+ $(,)?]) => {
71        $(#[$meta])*
72        $vis fn $name() -> $crate::SEXP {
73            static CACHE: ::std::sync::OnceLock<$crate::SEXP> = ::std::sync::OnceLock::new();
74            *CACHE.get_or_init(|| unsafe {
75                use $crate::SexpExt as _;
76                let strings: &[&::std::ffi::CStr] = &[$($cstr),+];
77                let sexp = $crate::sys::Rf_allocVector(
78                    $crate::SEXPTYPE::STRSXP,
79                    strings.len() as ::std::primitive::isize,
80                );
81                $crate::sys::R_PreserveObject(sexp);
82                for (i, s) in strings.iter().enumerate() {
83                    sexp.set_string_elt(
84                        i as ::std::primitive::isize,
85                        $crate::cached_class::permanent_charsxp(s),
86                    );
87                }
88                sexp
89            })
90        }
91    };
92}
93#[allow(unused_imports)] // exported for use by other modules
94pub(crate) use cached_strsxp;
95
96// endregion
97
98// region: Permanent CHARSXP helper
99
100/// Get a permanent CHARSXP for a string by going through `Rf_install` + `PRINTNAME`.
101///
102/// Symbols are never GC'd, so the CHARSXP from `PRINTNAME` is valid forever.
103/// This avoids the `Rf_mkCharLenCE` hash lookup on repeated calls.
104///
105/// Used by [`cached_strsxp!`] — `pub(crate)` so the macro can reference it
106/// from any module.
107#[doc(hidden)]
108#[inline]
109pub(crate) fn permanent_charsxp(name: &std::ffi::CStr) -> crate::SEXP {
110    use crate::SexpExt;
111    unsafe { crate::sys::Rf_install(name.as_ptr()) }.printname()
112}
113
114// endregion
115
116// region: Class vectors
117
118cached_strsxp!(
119    /// Cached `c("POSIXct", "POSIXt")` class STRSXP.
120    #[cfg(any(feature = "time", feature = "jiff", feature = "arrow"))]
121    pub(crate) fn posixct_class_sexp() = [c"POSIXct", c"POSIXt"]
122);
123
124cached_strsxp!(
125    /// Cached `"Date"` class STRSXP.
126    #[cfg(any(feature = "time", feature = "jiff", feature = "arrow"))]
127    pub(crate) fn date_class_sexp() = [c"Date"]
128);
129
130#[doc = r#" Cached `"data.frame"` class STRSXP."#]
pub(crate) fn data_frame_class_sexp() -> crate::SEXP {
    static CACHE: ::std::sync::OnceLock<crate::SEXP> =
        ::std::sync::OnceLock::new();
    *CACHE.get_or_init(||
                unsafe {
                    use crate::SexpExt as _;
                    let strings: &[&::std::ffi::CStr] = &[c"data.frame"];
                    let sexp =
                        crate::sys::Rf_allocVector(crate::SEXPTYPE::STRSXP,
                            strings.len() as ::std::primitive::isize);
                    crate::sys::R_PreserveObject(sexp);
                    for (i, s) in strings.iter().enumerate() {
                        sexp.set_string_elt(i as ::std::primitive::isize,
                            crate::cached_class::permanent_charsxp(s));
                    }
                    sexp
                })
}cached_strsxp!(
131    /// Cached `"data.frame"` class STRSXP.
132    pub(crate) fn data_frame_class_sexp() = [c"data.frame"]
133);
134
135#[doc =
r#" Cached `"rust_condition_value"` class STRSXP — applied to every tagged"#]
#[doc =
r" SEXP produced by `make_rust_condition_value` to identify panic-transport"]
#[doc = r" values to the generated R wrappers."]
pub(crate) fn rust_condition_class_sexp() -> crate::SEXP {
    static CACHE: ::std::sync::OnceLock<crate::SEXP> =
        ::std::sync::OnceLock::new();
    *CACHE.get_or_init(||
                unsafe {
                    use crate::SexpExt as _;
                    let strings: &[&::std::ffi::CStr] =
                        &[c"rust_condition_value"];
                    let sexp =
                        crate::sys::Rf_allocVector(crate::SEXPTYPE::STRSXP,
                            strings.len() as ::std::primitive::isize);
                    crate::sys::R_PreserveObject(sexp);
                    for (i, s) in strings.iter().enumerate() {
                        sexp.set_string_elt(i as ::std::primitive::isize,
                            crate::cached_class::permanent_charsxp(s));
                    }
                    sexp
                })
}cached_strsxp!(
136    /// Cached `"rust_condition_value"` class STRSXP — applied to every tagged
137    /// SEXP produced by `make_rust_condition_value` to identify panic-transport
138    /// values to the generated R wrappers.
139    pub(crate) fn rust_condition_class_sexp() = [c"rust_condition_value"]
140);
141
142#[doc =
r#" Cached `c("error", "kind", "class", "call", "data")` names STRSXP for"#]
#[doc = r" condition values."]
#[doc = r""]
#[doc =
r" Used by `make_rust_condition_value` which writes a 5-element list: the"]
#[doc =
r" error message, kind, optional user-supplied custom class, optional R"]
#[doc =
r" call, and an optional named-list `data` payload (`NULL` when absent)."]
pub(crate) fn condition_names_sexp() -> crate::SEXP {
    static CACHE: ::std::sync::OnceLock<crate::SEXP> =
        ::std::sync::OnceLock::new();
    *CACHE.get_or_init(||
                unsafe {
                    use crate::SexpExt as _;
                    let strings: &[&::std::ffi::CStr] =
                        &[c"error", c"kind", c"class", c"call", c"data"];
                    let sexp =
                        crate::sys::Rf_allocVector(crate::SEXPTYPE::STRSXP,
                            strings.len() as ::std::primitive::isize);
                    crate::sys::R_PreserveObject(sexp);
                    for (i, s) in strings.iter().enumerate() {
                        sexp.set_string_elt(i as ::std::primitive::isize,
                            crate::cached_class::permanent_charsxp(s));
                    }
                    sexp
                })
}cached_strsxp!(
143    /// Cached `c("error", "kind", "class", "call", "data")` names STRSXP for
144    /// condition values.
145    ///
146    /// Used by `make_rust_condition_value` which writes a 5-element list: the
147    /// error message, kind, optional user-supplied custom class, optional R
148    /// call, and an optional named-list `data` payload (`NULL` when absent).
149    pub(crate) fn condition_names_sexp() = [c"error", c"kind", c"class", c"call", c"data"]
150);
151
152// endregion
153
154// region: Scalar strings
155
156cached_strsxp!(
157    /// Cached `"UTC"` scalar string SEXP for the `tzone` attribute.
158    #[cfg(any(feature = "time", feature = "jiff"))]
159    fn utc_tzone_sexp() = [c"UTC"]
160);
161
162// endregion
163
164// region: Symbols
165
166cached_symbol!(
167    /// Cached `tzone` symbol.
168    #[cfg(any(feature = "time", feature = "jiff", feature = "arrow"))]
169    pub(crate) fn tzone_symbol() = c"tzone"
170);
171
172#[doc =
r" Cached `__rust_condition__` symbol — the marker attribute on tagged"]
#[doc =
r" panic-transport SEXPs. Wrapper code asserts this is `TRUE` in addition"]
#[doc =
r" to the class check before re-raising via `.miniextendr_raise_condition`."]
pub(crate) fn rust_condition_attr_symbol() -> crate::SEXP {
    static CACHE: ::std::sync::OnceLock<crate::SEXP> =
        ::std::sync::OnceLock::new();
    *CACHE.get_or_init(||
                unsafe {
                    crate::sys::Rf_install(c"__rust_condition__".as_ptr())
                })
}cached_symbol!(
173    /// Cached `__rust_condition__` symbol — the marker attribute on tagged
174    /// panic-transport SEXPs. Wrapper code asserts this is `TRUE` in addition
175    /// to the class check before re-raising via `.miniextendr_raise_condition`.
176    pub(crate) fn rust_condition_attr_symbol() = c"__rust_condition__"
177);
178
179#[doc =
r" Cached `miniextendr_written` symbol: the attribute a string ALTREP's"]
#[doc = r" data2 cache carries once R has stored a value into it."]
pub(crate) fn altstring_written_symbol() -> crate::SEXP {
    static CACHE: ::std::sync::OnceLock<crate::SEXP> =
        ::std::sync::OnceLock::new();
    *CACHE.get_or_init(||
                unsafe {
                    crate::sys::Rf_install(c"miniextendr_written".as_ptr())
                })
}cached_symbol!(
180    /// Cached `miniextendr_written` symbol: the attribute a string ALTREP's
181    /// data2 cache carries once R has stored a value into it.
182    pub(crate) fn altstring_written_symbol() = c"miniextendr_written"
183);
184
185cached_symbol!(
186    /// Cached `mx_raw_type` symbol (for raw conversion type tags).
187    #[cfg(feature = "raw_conversions")]
188    pub(crate) fn mx_raw_type_symbol() = c"mx_raw_type"
189);
190
191cached_symbol!(
192    /// Cached `ptype` symbol (vctrs list_of attribute).
193    #[cfg(feature = "vctrs")]
194    pub(crate) fn ptype_symbol() = c"ptype"
195);
196
197cached_symbol!(
198    /// Cached `size` symbol (vctrs list_of attribute).
199    #[cfg(feature = "vctrs")]
200    pub(crate) fn size_symbol() = c"size"
201);
202
203// endregion
204
205// region: Composite helpers
206
207/// Set class = `c("POSIXct", "POSIXt")` and tzone = `"UTC"` on an SEXP.
208///
209/// Uses cached class vector + tzone string — zero allocations after first call.
210///
211/// # Safety
212///
213/// `sexp` must be a valid REALSXP. Must be called on R's main thread.
214#[cfg(any(feature = "time", feature = "jiff"))]
215pub fn set_posixct_utc(sexp: crate::SEXP) {
216    use crate::SexpExt as _;
217    sexp.set_class(posixct_class_sexp());
218    sexp.set_attr(tzone_symbol(), utc_tzone_sexp());
219}
220
221/// Set class = `c("POSIXct", "POSIXt")` and tzone = `iana` on an SEXP.
222///
223/// Used by the jiff integration to round-trip `Zoned` timezone identity.
224/// Falls back to `"UTC"` for zones without an IANA name (e.g., fixed-offset zones).
225///
226/// # Safety
227///
228/// `sexp` must be a valid REALSXP. Must be called on R's main thread.
229#[cfg(feature = "jiff")]
230pub fn set_posixct_tz(sexp: crate::SEXP, iana: &str) {
231    use crate::SexpExt as _;
232    sexp.set_class(posixct_class_sexp());
233    // Build a one-element STRSXP for the tzone attribute.
234    unsafe {
235        let tzone_charsxp = crate::SEXP::charsxp(iana);
236        let tzone_sexp = crate::sys::Rf_allocVector(crate::SEXPTYPE::STRSXP, 1);
237        let _guard = crate::OwnedProtect::new(tzone_sexp);
238        tzone_sexp.set_string_elt(0, tzone_charsxp);
239        sexp.set_attr(tzone_symbol(), tzone_sexp);
240    }
241}
242
243// endregion