miniextendr_api/s4_helpers.rs
1//! S4 slot access and class checking helpers.
2//!
3//! For new packages, [`S7`](https://rconsortium.github.io/S7/) is the
4//! recommended class system (use `#[miniextendr(s7)]`). These S4 helpers
5//! exist for interoperating with existing S4 packages — for example,
6//! reading slots from Bioconductor objects passed as function arguments.
7//!
8//! Since R's C API for S4 slot access (`R_has_slot`, `R_do_slot`,
9//! `R_do_slot_assign`) is not exposed in miniextendr's FFI bindings,
10//! these helpers use R expression evaluation via [`RCall`]
11//! as a fallback.
12//!
13//! All functions require being called from the R main thread and operate
14/// on raw SEXP values.
15///
16/// # Example
17///
18/// ```ignore
19/// use miniextendr_api::SEXP;
20/// use miniextendr_api::s4_helpers;
21///
22/// unsafe {
23/// if s4_helpers::s4_is(obj) {
24/// if let Some(class) = s4_helpers::s4_class_name(obj) {
25/// println!("S4 class: {class}");
26/// }
27/// let slot_val = s4_helpers::s4_get_slot(obj, "data")?;
28/// }
29/// }
30/// ```
31use crate::expression::{RCall, REnv};
32use crate::{SEXP, SexpExt};
33
34/// Get the `methods` package namespace for evaluating S4 functions.
35///
36/// # Safety
37///
38/// Must be called from the R main thread.
39unsafe fn methods_namespace() -> Result<REnv, String> {
40 unsafe { REnv::package_namespace("methods") }
41}
42
43/// Check if a SEXP is an S4 object.
44///
45/// # Safety
46///
47/// - `obj` must be a valid SEXP.
48/// - Must be called from the R main thread.
49#[inline]
50pub unsafe fn s4_is(obj: SEXP) -> bool {
51 obj.is_s4()
52}
53
54/// Check if an S4 object has a named slot.
55///
56/// Attempts to access the slot via [`s4_get_slot`]. Returns `true` if the
57/// slot exists and is accessible, `false` if accessing it errors (i.e.,
58/// the slot does not exist).
59///
60/// # Safety
61///
62/// - `obj` must be a valid SEXP (typically an S4 object).
63/// - Must be called from the R main thread.
64pub unsafe fn s4_has_slot(obj: SEXP, slot_name: &str) -> bool {
65 unsafe { s4_get_slot(obj, slot_name).is_ok() }
66}
67
68/// Get the value of a named slot from an S4 object.
69///
70/// Uses R's `slot(obj, name)` to access the slot value.
71///
72/// # Safety
73///
74/// - `obj` must be a valid S4 SEXP with the named slot.
75/// - Must be called from the R main thread.
76///
77/// # Returns
78///
79/// - `Ok(SEXP)` with the slot value (unprotected).
80/// - `Err(String)` if the slot doesn't exist or another R error occurs.
81pub unsafe fn s4_get_slot(obj: SEXP, slot_name: &str) -> Result<SEXP, String> {
82 unsafe {
83 let env = methods_namespace()?;
84 RCall::new("slot")
85 .arg(obj)
86 .named_arg("name", scalar_string(slot_name))
87 .eval(env.as_sexp())
88 }
89}
90
91/// Set the value of a named slot on an S4 object.
92///
93/// Uses R's `slot(obj, name) <- value` to assign the slot value.
94///
95/// # Safety
96///
97/// - `obj` must be a valid S4 SEXP with the named slot.
98/// - `value` must be a valid SEXP of the appropriate type for the slot.
99/// - Must be called from the R main thread.
100///
101/// # Returns
102///
103/// - `Ok(())` on success.
104/// - `Err(String)` if the slot doesn't exist or the value type is incompatible.
105pub unsafe fn s4_set_slot(obj: SEXP, slot_name: &str, value: SEXP) -> Result<(), String> {
106 unsafe {
107 // slot(obj, name) <- value is equivalent to `slot<-`(obj, name, value)
108 let env = methods_namespace()?;
109 RCall::new("slot<-")
110 .arg(obj)
111 .named_arg("name", scalar_string(slot_name))
112 .named_arg("value", value)
113 .eval(env.as_sexp())?;
114 Ok(())
115 }
116}
117
118/// Extract the S4 class name from an object.
119///
120/// Reads the `class` attribute and returns the first element as a `String`.
121/// Returns `None` if the object has no class attribute or the attribute is empty.
122///
123/// # Safety
124///
125/// - `obj` must be a valid SEXP.
126/// - Must be called from the R main thread.
127pub unsafe fn s4_class_name(obj: SEXP) -> Option<String> {
128 unsafe {
129 let class_attr = obj.get_class();
130 if class_attr.is_null_or_nil() || class_attr.xlength() == 0 {
131 return None;
132 }
133
134 let first = class_attr.string_elt(0);
135 crate::from_r::charsxp_to_string_lossy(first)
136 }
137}
138
139// region: Internal helpers
140
141/// Create a scalar R character string from a Rust `&str`.
142///
143/// The returned SEXP is unprotected — caller must protect if further
144/// allocations will occur before use.
145#[inline]
146fn scalar_string(s: &str) -> SEXP {
147 SEXP::scalar_string_from_str(s)
148}
149
150#[cfg(test)]
151mod tests {
152 use super::*;
153
154 #[test]
155 fn s4_is_compiles() {
156 // Verify the function signature compiles.
157 // Actual testing requires the R runtime.
158 fn assert_fn<F: Fn(SEXP) -> bool>(_f: F) {}
159 assert_fn(|s| unsafe { s4_is(s) });
160 }
161
162 #[test]
163 fn s4_class_name_compiles() {
164 fn assert_fn<F: Fn(SEXP) -> Option<String>>(_f: F) {}
165 assert_fn(|s| unsafe { s4_class_name(s) });
166 }
167}
168// endregion