artichoke_backend/
class.rs

1use std::any::Any;
2use std::borrow::Cow;
3use std::collections::HashSet;
4use std::ffi::CStr;
5use std::hash::{Hash, Hasher};
6use std::ptr::NonNull;
7
8use crate::Artichoke;
9use crate::def::{ConstantNameError, EnclosingRubyScope, Free, Method, NotDefinedError};
10use crate::error::Error;
11use crate::ffi::InterpreterExtractError;
12use crate::method;
13use crate::sys;
14
15#[derive(Debug)]
16pub struct Builder<'a> {
17    interp: &'a mut Artichoke,
18    spec: &'a Spec,
19    is_mrb_tt_data: bool,
20    super_class: Option<NonNull<sys::RClass>>,
21    methods: HashSet<method::Spec>,
22}
23
24impl<'a> Builder<'a> {
25    #[must_use]
26    pub fn for_spec(interp: &'a mut Artichoke, spec: &'a Spec) -> Self {
27        Self {
28            interp,
29            spec,
30            is_mrb_tt_data: false,
31            super_class: None,
32            methods: HashSet::default(),
33        }
34    }
35
36    #[must_use]
37    pub fn value_is_rust_object(mut self) -> Self {
38        self.is_mrb_tt_data = true;
39        self
40    }
41
42    pub fn with_super_class<T, U>(mut self, classname: U) -> Result<Self, Error>
43    where
44        T: Any,
45        U: Into<Cow<'static, str>>,
46    {
47        let state = self.interp.state.as_deref().ok_or_else(InterpreterExtractError::new)?;
48        let Some(spec) = state.classes.get::<T>() else {
49            return Err(NotDefinedError::super_class(classname.into()).into());
50        };
51        let rclass = spec.rclass();
52        let rclass = unsafe { self.interp.with_ffi_boundary(|mrb| rclass.resolve(mrb))? };
53        let Some(rclass) = rclass else {
54            return Err(NotDefinedError::super_class(classname.into()).into());
55        };
56        self.super_class = Some(rclass);
57        Ok(self)
58    }
59
60    pub fn add_method<T>(mut self, name: T, method: Method, args: sys::mrb_aspec) -> Result<Self, ConstantNameError>
61    where
62        T: Into<Cow<'static, str>>,
63    {
64        let spec = method::Spec::new(method::Type::Instance, name.into(), method, args)?;
65        self.methods.insert(spec);
66        Ok(self)
67    }
68
69    pub fn add_self_method<T>(
70        mut self,
71        name: T,
72        method: Method,
73        args: sys::mrb_aspec,
74    ) -> Result<Self, ConstantNameError>
75    where
76        T: Into<Cow<'static, str>>,
77    {
78        let spec = method::Spec::new(method::Type::Class, name.into(), method, args)?;
79        self.methods.insert(spec);
80        Ok(self)
81    }
82
83    pub fn define(self) -> Result<(), NotDefinedError> {
84        use sys::mrb_vtype::MRB_TT_CDATA;
85
86        let name = self.spec.name_c_str().as_ptr();
87
88        let mut super_class = if let Some(super_class) = self.super_class {
89            super_class
90        } else {
91            // SAFETY: Although this direct access of the `mrb` property on the
92            // interpreter does not go through `Artichoke::with_ffi_boundary`, no
93            // `MRB_API` functions are called, which means it is not required to
94            // re-box the Artichoke `State` into the `mrb_state->ud` pointer.
95            //
96            // This code only performs a memory access to read a field from the
97            // `mrb_state`.
98            let rclass = unsafe { self.interp.mrb.as_ref().object_class };
99            NonNull::new(rclass).ok_or_else(|| NotDefinedError::super_class("Object"))?
100        };
101
102        let rclass = self.spec.rclass();
103        let rclass = unsafe { self.interp.with_ffi_boundary(|mrb| rclass.resolve(mrb)) };
104
105        let mut rclass = if let Ok(Some(rclass)) = rclass {
106            rclass
107        } else if let Some(enclosing_scope) = self.spec.enclosing_scope() {
108            let scope = unsafe { self.interp.with_ffi_boundary(|mrb| enclosing_scope.rclass(mrb)) };
109            let Ok(Some(mut scope)) = scope else {
110                return Err(NotDefinedError::enclosing_scope(enclosing_scope.fqname().into_owned()));
111            };
112            let rclass = unsafe {
113                self.interp.with_ffi_boundary(|mrb| {
114                    sys::mrb_define_class_under(mrb, scope.as_mut(), name, super_class.as_mut())
115                })
116            };
117            let rclass = rclass.map_err(|_| NotDefinedError::class(self.spec.name()))?;
118            NonNull::new(rclass).ok_or_else(|| NotDefinedError::class(self.spec.name()))?
119        } else {
120            let rclass = unsafe {
121                self.interp
122                    .with_ffi_boundary(|mrb| sys::mrb_define_class(mrb, name, super_class.as_mut()))
123            };
124            let rclass = rclass.map_err(|_| NotDefinedError::class(self.spec.name()))?;
125            NonNull::new(rclass).ok_or_else(|| NotDefinedError::class(self.spec.name()))?
126        };
127
128        for method in &self.methods {
129            unsafe {
130                method.define(self.interp, rclass.as_mut())?;
131            }
132        }
133
134        // If a `Spec` defines a `Class` whose instances own a pointer to a
135        // Rust object, mark them as `MRB_TT_CDATA`.
136        if self.is_mrb_tt_data {
137            unsafe {
138                sys::mrb_sys_set_instance_tt(rclass.as_mut(), MRB_TT_CDATA);
139            }
140        }
141        Ok(())
142    }
143}
144
145#[derive(Debug, Clone, PartialEq, Eq)]
146pub struct Rclass {
147    name: &'static CStr,
148    enclosing_scope: Option<EnclosingRubyScope>,
149}
150
151impl Rclass {
152    #[must_use]
153    pub const fn new(name: &'static CStr, enclosing_scope: Option<EnclosingRubyScope>) -> Self {
154        Self { name, enclosing_scope }
155    }
156
157    /// Resolve a type's [`sys::RClass`] using its enclosing scope and name.
158    ///
159    /// # Safety
160    ///
161    /// This function must be called within an [`Artichoke::with_ffi_boundary`]
162    /// closure because the FFI APIs called in this function may require access
163    /// to the Artichoke [`State`].
164    ///
165    /// [`State`]: crate::state::State
166    pub unsafe fn resolve(&self, mrb: *mut sys::mrb_state) -> Option<NonNull<sys::RClass>> {
167        let class_name = self.name.as_ptr();
168        if let Some(ref scope) = self.enclosing_scope {
169            // short circuit if enclosing scope does not exist.
170            //
171            // SAFETY: callers must uphold that `mrb` is initialized.
172            let mut scope = unsafe { scope.rclass(mrb)? };
173            // SAFETY: the enclosing scope exists, callers must uphold that
174            // `mrb` is initialized.
175            let is_defined_under = unsafe { sys::mrb_class_defined_under(mrb, scope.as_mut(), class_name) };
176            if is_defined_under {
177                // SAFETY: the enclosing scope exists and the class is defined
178                // under the enclosing scope. Callers must uphold that `mrb` is
179                // initialized.
180                let class = unsafe { sys::mrb_class_get_under(mrb, scope.as_mut(), class_name) };
181                NonNull::new(class)
182            } else {
183                // Enclosing scope exists.
184                // Class is not defined under the enclosing scope.
185                None
186            }
187        } else {
188            // SAFETY: callers must uphold that `mrb` is initialized.
189            let is_defined = unsafe { sys::mrb_class_defined(mrb, class_name) };
190            if is_defined {
191                // SAFETY: Class exists in root scope. Callers must uphold that
192                // `mrb` is initialized.
193                let class = unsafe { sys::mrb_class_get(mrb, class_name) };
194                NonNull::new(class)
195            } else {
196                // Class does not exist in root scope.
197                None
198            }
199        }
200    }
201}
202
203#[derive(Debug)]
204pub struct Spec {
205    name: Cow<'static, str>,
206    name_cstr: &'static CStr,
207    data_type: Box<sys::mrb_data_type>,
208    enclosing_scope: Option<EnclosingRubyScope>,
209}
210
211impl Spec {
212    pub fn new<T>(
213        name: T,
214        name_cstr: &'static CStr,
215        enclosing_scope: Option<EnclosingRubyScope>,
216        free: Option<Free>,
217    ) -> Result<Self, ConstantNameError>
218    where
219        T: Into<Cow<'static, str>>,
220    {
221        let name = name.into();
222        // SAFETY: The constructed `mrb_data_type` has `'static` lifetime:
223        //
224        // - `name_cstr` is `&'static` so it will outlive the `data_type`.
225        // - `Spec` does not offer mutable access to these fields.
226        let data_type = sys::mrb_data_type {
227            struct_name: name_cstr.as_ptr(),
228            dfree: free,
229        };
230        let data_type = Box::new(data_type);
231        Ok(Self {
232            name,
233            name_cstr,
234            data_type,
235            enclosing_scope,
236        })
237    }
238
239    #[must_use]
240    pub fn data_type(&self) -> *const sys::mrb_data_type {
241        self.data_type.as_ref()
242    }
243
244    #[must_use]
245    pub fn name(&self) -> Cow<'static, str> {
246        match &self.name {
247            Cow::Borrowed(name) => Cow::Borrowed(name),
248            Cow::Owned(name) => name.clone().into(),
249        }
250    }
251
252    #[must_use]
253    pub fn name_c_str(&self) -> &'static CStr {
254        self.name_cstr
255    }
256
257    #[must_use]
258    pub fn enclosing_scope(&self) -> Option<&EnclosingRubyScope> {
259        self.enclosing_scope.as_ref()
260    }
261
262    #[must_use]
263    pub fn fqname(&self) -> Cow<'_, str> {
264        let Some(scope) = self.enclosing_scope() else {
265            return self.name();
266        };
267        let mut fqname = String::from(scope.fqname());
268        fqname.push_str("::");
269        fqname.push_str(self.name.as_ref());
270        fqname.into()
271    }
272
273    #[must_use]
274    pub fn rclass(&self) -> Rclass {
275        Rclass::new(self.name_cstr, self.enclosing_scope.clone())
276    }
277}
278
279impl Hash for Spec {
280    fn hash<H: Hasher>(&self, state: &mut H) {
281        self.name().hash(state);
282        self.enclosing_scope().hash(state);
283    }
284}
285
286impl Eq for Spec {}
287
288impl PartialEq for Spec {
289    fn eq(&self, other: &Self) -> bool {
290        self.fqname() == other.fqname()
291    }
292}
293
294#[cfg(test)]
295mod tests {
296    use spinoso_exception::StandardError;
297
298    use crate::extn::core::kernel::Kernel;
299    use crate::test::prelude::*;
300
301    struct RustError;
302
303    #[test]
304    fn super_class() {
305        let mut interp = interpreter();
306        let spec = class::Spec::new("RustError", c"RustError", None, None).unwrap();
307        class::Builder::for_spec(&mut interp, &spec)
308            .with_super_class::<StandardError, _>("StandardError")
309            .unwrap()
310            .define()
311            .unwrap();
312        interp.def_class::<RustError>(spec).unwrap();
313
314        let result = interp.eval(b"RustError.new.is_a?(StandardError)").unwrap();
315        let result = result.try_convert_into::<bool>(&interp).unwrap();
316        assert!(result, "RustError instances are instance of StandardError");
317
318        let result = interp.eval(b"RustError < StandardError").unwrap();
319        let result = result.try_convert_into::<bool>(&interp).unwrap();
320        assert!(result, "RustError inherits from StandardError");
321    }
322
323    #[test]
324    fn rclass_for_undef_root_class() {
325        let mut interp = interpreter();
326        let spec = class::Spec::new("Foo", c"Foo", None, None).unwrap();
327        let rclass = unsafe { interp.with_ffi_boundary(|mrb| spec.rclass().resolve(mrb)) }.unwrap();
328        assert!(rclass.is_none());
329    }
330
331    #[test]
332    fn rclass_for_undef_nested_class() {
333        let mut interp = interpreter();
334        let scope = interp.module_spec::<Kernel>().unwrap().unwrap();
335        let spec = class::Spec::new("Foo", c"Foo", Some(EnclosingRubyScope::module(scope)), None).unwrap();
336        let rclass = unsafe { interp.with_ffi_boundary(|mrb| spec.rclass().resolve(mrb)) }.unwrap();
337        assert!(rclass.is_none());
338    }
339
340    #[test]
341    fn rclass_for_nested_class() {
342        let mut interp = interpreter();
343        interp.eval(b"module Foo; class Bar; end; end").unwrap();
344        let spec = module::Spec::new(&mut interp, "Foo", c"Foo", None).unwrap();
345        let spec = class::Spec::new("Bar", c"Bar", Some(EnclosingRubyScope::module(&spec)), None).unwrap();
346        let rclass = unsafe { interp.with_ffi_boundary(|mrb| spec.rclass().resolve(mrb)) }.unwrap();
347        assert!(rclass.is_some());
348    }
349
350    #[test]
351    fn rclass_for_nested_class_under_class() {
352        let mut interp = interpreter();
353        interp.eval(b"class Foo; class Bar; end; end").unwrap();
354        let spec = class::Spec::new("Foo", c"Foo", None, None).unwrap();
355        let spec = class::Spec::new("Bar", c"Bar", Some(EnclosingRubyScope::class(&spec)), None).unwrap();
356        let rclass = unsafe { interp.with_ffi_boundary(|mrb| spec.rclass().resolve(mrb)) }.unwrap();
357        assert!(rclass.is_some());
358    }
359}