Skip to main content

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::ffi::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::ffi::{self, SEXP, SexpExt};
33use std::ffi::CStr;
34
35/// Get the `methods` package namespace for evaluating S4 functions.
36///
37/// # Safety
38///
39/// Must be called from the R main thread.
40unsafe fn methods_namespace() -> Result<REnv, String> {
41    unsafe { REnv::package_namespace("methods") }
42}
43
44/// Check if a SEXP is an S4 object.
45///
46/// # Safety
47///
48/// - `obj` must be a valid SEXP.
49/// - Must be called from the R main thread.
50#[inline]
51pub unsafe fn s4_is(obj: SEXP) -> bool {
52    obj.is_s4()
53}
54
55/// Check if an S4 object has a named slot.
56///
57/// Attempts to access the slot via [`s4_get_slot`]. Returns `true` if the
58/// slot exists and is accessible, `false` if accessing it errors (i.e.,
59/// the slot does not exist).
60///
61/// # Safety
62///
63/// - `obj` must be a valid SEXP (typically an S4 object).
64/// - Must be called from the R main thread.
65pub unsafe fn s4_has_slot(obj: SEXP, slot_name: &str) -> bool {
66    unsafe { s4_get_slot(obj, slot_name).is_ok() }
67}
68
69/// Get the value of a named slot from an S4 object.
70///
71/// Uses R's `slot(obj, name)` to access the slot value.
72///
73/// # Safety
74///
75/// - `obj` must be a valid S4 SEXP with the named slot.
76/// - Must be called from the R main thread.
77///
78/// # Returns
79///
80/// - `Ok(SEXP)` with the slot value (unprotected).
81/// - `Err(String)` if the slot doesn't exist or another R error occurs.
82pub unsafe fn s4_get_slot(obj: SEXP, slot_name: &str) -> Result<SEXP, String> {
83    unsafe {
84        let env = methods_namespace()?;
85        RCall::new("slot")
86            .arg(obj)
87            .named_arg("name", scalar_string(slot_name))
88            .eval(env.as_sexp())
89    }
90}
91
92/// Set the value of a named slot on an S4 object.
93///
94/// Uses R's `slot(obj, name) <- value` to assign the slot value.
95///
96/// # Safety
97///
98/// - `obj` must be a valid S4 SEXP with the named slot.
99/// - `value` must be a valid SEXP of the appropriate type for the slot.
100/// - Must be called from the R main thread.
101///
102/// # Returns
103///
104/// - `Ok(())` on success.
105/// - `Err(String)` if the slot doesn't exist or the value type is incompatible.
106pub unsafe fn s4_set_slot(obj: SEXP, slot_name: &str, value: SEXP) -> Result<(), String> {
107    unsafe {
108        // slot(obj, name) <- value  is equivalent to `slot<-`(obj, name, value)
109        let env = methods_namespace()?;
110        RCall::new("slot<-")
111            .arg(obj)
112            .named_arg("name", scalar_string(slot_name))
113            .named_arg("value", value)
114            .eval(env.as_sexp())?;
115        Ok(())
116    }
117}
118
119/// Extract the S4 class name from an object.
120///
121/// Reads the `class` attribute and returns the first element as a `String`.
122/// Returns `None` if the object has no class attribute or the attribute is empty.
123///
124/// # Safety
125///
126/// - `obj` must be a valid SEXP.
127/// - Must be called from the R main thread.
128pub unsafe fn s4_class_name(obj: SEXP) -> Option<String> {
129    unsafe {
130        let class_attr = obj.get_class();
131        if class_attr.is_null_or_nil() || ffi::Rf_xlength(class_attr) == 0 {
132            return None;
133        }
134
135        let first = class_attr.string_elt(0);
136        if first.is_null_or_nil() {
137            return None;
138        }
139
140        let ptr = first.r_char();
141        if ptr.is_null() {
142            return None;
143        }
144
145        Some(CStr::from_ptr(ptr).to_string_lossy().into_owned())
146    }
147}
148
149// region: Internal helpers
150
151/// Create a scalar R character string from a Rust `&str`.
152///
153/// The returned SEXP is unprotected — caller must protect if further
154/// allocations will occur before use.
155#[inline]
156fn scalar_string(s: &str) -> SEXP {
157    SEXP::scalar_string_from_str(s)
158}
159
160#[cfg(test)]
161mod tests {
162    use super::*;
163
164    #[test]
165    fn s4_is_compiles() {
166        // Verify the function signature compiles.
167        // Actual testing requires the R runtime.
168        fn assert_fn<F: Fn(SEXP) -> bool>(_f: F) {}
169        assert_fn(|s| unsafe { s4_is(s) });
170    }
171
172    #[test]
173    fn s4_class_name_compiles() {
174        fn assert_fn<F: Fn(SEXP) -> Option<String>>(_f: F) {}
175        assert_fn(|s| unsafe { s4_class_name(s) });
176    }
177}
178// endregion