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::ffi::SEXP {
42 static CACHE: ::std::sync::OnceLock<$crate::ffi::SEXP> = ::std::sync::OnceLock::new();
43 *CACHE.get_or_init(|| unsafe { $crate::ffi::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::ffi::SEXP {
73 static CACHE: ::std::sync::OnceLock<$crate::ffi::SEXP> = ::std::sync::OnceLock::new();
74 *CACHE.get_or_init(|| unsafe {
75 use $crate::ffi::SexpExt as _;
76 let strings: &[&::std::ffi::CStr] = &[$($cstr),+];
77 let sexp = $crate::ffi::Rf_allocVector(
78 $crate::ffi::SEXPTYPE::STRSXP,
79 strings.len() as ::std::primitive::isize,
80 );
81 $crate::ffi::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::ffi::SEXP {
110 use crate::ffi::SexpExt;
111 unsafe { crate::ffi::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 = "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 = "arrow"))]
127 pub(crate) fn date_class_sexp() = [c"Date"]
128);
129
130cached_strsxp!(
131 /// Cached `"data.frame"` class STRSXP.
132 pub(crate) fn data_frame_class_sexp() = [c"data.frame"]
133);
134
135cached_strsxp!(
136 /// Cached `"rust_error_value"` class STRSXP.
137 pub(crate) fn rust_error_class_sexp() = [c"rust_error_value"]
138);
139
140cached_strsxp!(
141 /// Cached `c("error", "kind", "call")` names STRSXP for error values.
142 pub(crate) fn error_names_sexp() = [c"error", c"kind", c"call"]
143);
144
145// endregion
146
147// region: Scalar strings
148
149cached_strsxp!(
150 /// Cached `"UTC"` scalar string SEXP for the `tzone` attribute.
151 #[cfg(feature = "time")]
152 fn utc_tzone_sexp() = [c"UTC"]
153);
154
155// endregion
156
157// region: Symbols
158
159cached_symbol!(
160 /// Cached `tzone` symbol.
161 #[cfg(any(feature = "time", feature = "arrow"))]
162 pub(crate) fn tzone_symbol() = c"tzone"
163);
164
165cached_symbol!(
166 /// Cached `__rust_error__` symbol.
167 pub(crate) fn rust_error_attr_symbol() = c"__rust_error__"
168);
169
170cached_symbol!(
171 /// Cached `mx_raw_type` symbol (for raw conversion type tags).
172 #[cfg(feature = "raw_conversions")]
173 pub(crate) fn mx_raw_type_symbol() = c"mx_raw_type"
174);
175
176cached_symbol!(
177 /// Cached `ptype` symbol (vctrs list_of attribute).
178 #[cfg(feature = "vctrs")]
179 pub(crate) fn ptype_symbol() = c"ptype"
180);
181
182cached_symbol!(
183 /// Cached `size` symbol (vctrs list_of attribute).
184 #[cfg(feature = "vctrs")]
185 pub(crate) fn size_symbol() = c"size"
186);
187
188// endregion
189
190// region: Composite helpers
191
192/// Set class = `c("POSIXct", "POSIXt")` and tzone = `"UTC"` on an SEXP.
193///
194/// Uses cached class vector + tzone string — zero allocations after first call.
195///
196/// # Safety
197///
198/// `sexp` must be a valid REALSXP. Must be called on R's main thread.
199#[cfg(feature = "time")]
200pub(crate) fn set_posixct_utc(sexp: crate::ffi::SEXP) {
201 use crate::ffi::SexpExt as _;
202 sexp.set_class(posixct_class_sexp());
203 sexp.set_attr(tzone_symbol(), utc_tzone_sexp());
204}
205
206// endregion