Skip to main content

boa_engine/
class.rs

1//! Traits and structs for implementing native classes.
2//!
3//! Native classes are implemented through the [`Class`][class-trait] trait.
4//!
5//! # Examples
6//!
7//! ```
8//! # use boa_engine::{
9//! #    NativeFunction,
10//! #    property::Attribute,
11//! #    class::{Class, ClassBuilder},
12//! #    Context, JsResult, JsValue,
13//! #    JsArgs, Source, JsObject, js_str, js_string,
14//! #    JsNativeError, JsData,
15//! # };
16//! # use boa_gc::{Finalize, Trace};
17//! #
18//! // Can also be a struct containing `Trace` types.
19//! #[derive(Debug, Trace, Finalize, JsData)]
20//! enum Animal {
21//!     Cat,
22//!     Dog,
23//!     Other,
24//! }
25//!
26//! impl Class for Animal {
27//!     // we set the binding name of this function to be `"Animal"`.
28//!     const NAME: &'static str = "Animal";
29//!
30//!     // We set the length to `2` since we accept 2 arguments in the constructor.
31//!     const LENGTH: usize = 2;
32//!
33//!     // This is what is called when we do `new Animal()` to construct the inner data of the class.
34//!     // `_new_target` is the target of the `new` invocation, in this case the `Animal` constructor
35//!     // object.
36//!     fn data_constructor(_new_target: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<Self> {
37//!         // This is equivalent to `String(arg)`.
38//!         let kind = args.get_or_undefined(0).to_string(context)?;
39//!
40//!         let animal = match kind.to_std_string_escaped().as_str() {
41//!             "cat" => Self::Cat,
42//!             "dog" => Self::Dog,
43//!             _ => Self::Other,
44//!         };
45//!
46//!         Ok(animal)
47//!     }
48//!
49//!     // This is also called on instance construction, but it receives the object wrapping the
50//!     // native data as its `instance` argument.
51//!     fn object_constructor(
52//!         instance: &JsObject<Self>,
53//!         args: &[JsValue],
54//!         context: &mut Context,
55//!     ) -> JsResult<()> {
56//!         let age = args.get_or_undefined(1).to_number(context)?;
57//!
58//!         // Roughly equivalent to `this.age = Number(age)`.
59//!         instance.clone().upcast().set(js_string!("age"), age, true, context)?;
60//!
61//!         Ok(())
62//!     }
63//!
64//!     /// This is where the class object is initialized.
65//!     fn init(class: &mut ClassBuilder) -> JsResult<()> {
66//!         class.method(
67//!             js_string!("speak"),
68//!             0,
69//!             NativeFunction::from_fn_ptr(|this, _args, _ctx| {
70//!                 if let Some(object) = this.as_object() {
71//!                     if let Some(animal) = object.downcast_ref::<Animal>() {
72//!                         return Ok(match &*animal {
73//!                             Self::Cat => js_string!("meow"),
74//!                             Self::Dog => js_string!("woof"),
75//!                             Self::Other => js_string!(r"¯\_(ツ)_/¯"),
76//!                         }.into());
77//!                     }
78//!                 }
79//!                 Err(JsNativeError::typ().with_message("invalid this for class method").into())
80//!             }),
81//!         );
82//!         Ok(())
83//!     }
84//! }
85//!
86//! fn main() {
87//!     let mut context = Context::default();
88//!
89//!     context.register_global_class::<Animal>().unwrap();
90//!
91//!     let result = context.eval(Source::from_bytes(r#"
92//!         let pet = new Animal("dog", 3);
93//!
94//!         `My pet is ${pet.age} years old. Right, buddy? - ${pet.speak()}!`
95//!     "#)).unwrap();
96//!
97//!     assert_eq!(
98//!         result.as_string().unwrap(),
99//!         js_str!("My pet is 3 years old. Right, buddy? - woof!")
100//!     );
101//! }
102//! ```
103//!
104//! [class-trait]: ./trait.Class.html
105
106use crate::{
107    Context, JsResult, JsValue,
108    context::intrinsics::StandardConstructor,
109    error::JsNativeError,
110    native_function::NativeFunction,
111    object::{ConstructorBuilder, FunctionBinding, JsFunction, JsObject, NativeObject, PROTOTYPE},
112    property::{Attribute, PropertyDescriptor, PropertyKey},
113};
114
115/// Native class.
116///
117/// See the [module-level documentation][self] for more details.
118pub trait Class: NativeObject + Sized {
119    /// The binding name of this class.
120    const NAME: &'static str;
121    /// The amount of arguments this class' constructor takes. Default is `0`.
122    const LENGTH: usize = 0;
123    /// The property attributes of this class' constructor in the global object.
124    /// Default is `writable`, `enumerable`, `configurable`.
125    const ATTRIBUTES: Attribute = Attribute::all();
126
127    /// Initializes the properties and methods of this class.
128    fn init(class: &mut ClassBuilder<'_>) -> JsResult<()>;
129
130    /// Creates the internal data for an instance of this class.
131    fn data_constructor(
132        new_target: &JsValue,
133        args: &[JsValue],
134        context: &mut Context,
135    ) -> JsResult<Self>;
136
137    /// Initializes the properties of the constructed object for an instance of this class.
138    ///
139    /// Useful to initialize additional properties for the constructed object that aren't
140    /// stored inside the native data.
141    #[allow(unused_variables)] // Saves work when IDEs autocomplete trait impls.
142    fn object_constructor(
143        instance: &JsObject<Self>,
144        args: &[JsValue],
145        context: &mut Context,
146    ) -> JsResult<()> {
147        Ok(())
148    }
149
150    /// Creates a new [`JsObject`] with its internal data set to the result of calling
151    /// [`Class::data_constructor`] and [`Class::object_constructor`].
152    ///
153    /// # Errors
154    ///
155    /// - Throws an error if `new_target` is undefined.
156    /// - Throws an error if this class is not registered in `new_target`'s realm.
157    ///   See [`Context::register_global_class`].
158    ///
159    /// <div class="warning">
160    /// Overriding this method could be useful for certain usages, but incorrectly implementing this
161    /// could lead to weird errors like missing inherited methods or incorrect internal data.
162    /// </div>
163    fn construct(
164        new_target: &JsValue,
165        args: &[JsValue],
166        context: &mut Context,
167    ) -> JsResult<JsObject> {
168        if new_target.is_undefined() {
169            return Err(JsNativeError::typ()
170                .with_message(format!(
171                    "cannot call constructor of native class `{}` without new",
172                    Self::NAME
173                ))
174                .into());
175        }
176
177        let prototype = 'proto: {
178            let realm = if let Some(constructor) = new_target.as_object() {
179                if let Some(proto) = constructor.get(PROTOTYPE, context)?.as_object() {
180                    break 'proto proto.clone();
181                }
182                constructor.get_function_realm(context)?
183            } else {
184                context.realm().clone()
185            };
186            realm
187                .get_class::<Self>()
188                .ok_or_else(|| {
189                    JsNativeError::typ().with_message(format!(
190                        "could not find native class `{}` in the map of registered classes",
191                        Self::NAME
192                    ))
193                })?
194                .prototype()
195        };
196
197        let data = Self::data_constructor(new_target, args, context)?;
198
199        let object =
200            JsObject::from_proto_and_data_with_shared_shape(context.root_shape(), prototype, data);
201
202        Self::object_constructor(&object, args, context)?;
203
204        Ok(object.upcast())
205    }
206
207    /// Constructs an instance of this class from its inner native data.
208    ///
209    /// Note that the default implementation won't call [`Class::data_constructor`], but it will
210    /// call [`Class::object_constructor`] with no arguments.
211    ///
212    /// # Errors
213    /// - Throws an error if this class is not registered in the context's realm. See
214    ///   [`Context::register_global_class`].
215    ///
216    /// <div class="warning">
217    /// Overriding this method could be useful for certain usages, but incorrectly implementing this
218    /// could lead to weird errors like missing inherited methods or incorrect internal data.
219    /// </div>
220    fn from_data(data: Self, context: &mut Context) -> JsResult<JsObject> {
221        let prototype = context
222            .get_global_class::<Self>()
223            .ok_or_else(|| {
224                JsNativeError::typ().with_message(format!(
225                    "could not find native class `{}` in the map of registered classes",
226                    Self::NAME
227                ))
228            })?
229            .prototype();
230
231        let object =
232            JsObject::from_proto_and_data_with_shared_shape(context.root_shape(), prototype, data);
233
234        Self::object_constructor(&object, &[], context)?;
235
236        Ok(object.upcast())
237    }
238}
239
240/// Class builder which allows adding methods and static methods to the class.
241#[derive(Debug)]
242pub struct ClassBuilder<'ctx> {
243    builder: ConstructorBuilder<'ctx>,
244}
245
246impl<'ctx> ClassBuilder<'ctx> {
247    /// Create a new `ClassBuilder` from a [`Class`] type.
248    pub fn new<T>(context: &'ctx mut Context) -> Self
249    where
250        T: Class,
251    {
252        let mut builder = ConstructorBuilder::new(
253            context,
254            NativeFunction::from_fn_ptr(|t, a, c| T::construct(t, a, c).map(JsValue::from)),
255        );
256        builder.name(T::NAME);
257        builder.length(T::LENGTH);
258        Self { builder }
259    }
260
261    /// Create the [`StandardConstructor`] from this class builder.
262    #[must_use]
263    pub fn build(self) -> StandardConstructor {
264        self.builder.build()
265    }
266
267    /// Add a method to the class.
268    ///
269    /// It is added to `prototype`.
270    pub fn method<N>(&mut self, name: N, length: usize, function: NativeFunction) -> &mut Self
271    where
272        N: Into<FunctionBinding>,
273    {
274        self.builder.method(function, name, length);
275        self
276    }
277
278    /// Add a static method to the class.
279    ///
280    /// It is added to class object itself.
281    pub fn static_method<N>(
282        &mut self,
283        name: N,
284        length: usize,
285        function: NativeFunction,
286    ) -> &mut Self
287    where
288        N: Into<FunctionBinding>,
289    {
290        self.builder.static_method(function, name, length);
291        self
292    }
293
294    /// Add a data property to the class, with the specified attribute.
295    ///
296    /// It is added to `prototype`.
297    pub fn property<K, V>(&mut self, key: K, value: V, attribute: Attribute) -> &mut Self
298    where
299        K: Into<PropertyKey>,
300        V: Into<JsValue>,
301    {
302        self.builder.property(key, value, attribute);
303        self
304    }
305
306    /// Add a static data property to the class, with the specified attribute.
307    ///
308    /// It is added to class object itself.
309    pub fn static_property<K, V>(&mut self, key: K, value: V, attribute: Attribute) -> &mut Self
310    where
311        K: Into<PropertyKey>,
312        V: Into<JsValue>,
313    {
314        self.builder.static_property(key, value, attribute);
315        self
316    }
317
318    /// Add an accessor property to the class, with the specified attribute.
319    ///
320    /// It is added to `prototype`.
321    pub fn accessor<K>(
322        &mut self,
323        key: K,
324        get: Option<JsFunction>,
325        set: Option<JsFunction>,
326        attribute: Attribute,
327    ) -> &mut Self
328    where
329        K: Into<PropertyKey>,
330    {
331        self.builder.accessor(key, get, set, attribute);
332        self
333    }
334
335    /// Add a static accessor property to the class, with the specified attribute.
336    ///
337    /// It is added to class object itself.
338    pub fn static_accessor<K>(
339        &mut self,
340        key: K,
341        get: Option<JsFunction>,
342        set: Option<JsFunction>,
343        attribute: Attribute,
344    ) -> &mut Self
345    where
346        K: Into<PropertyKey>,
347    {
348        self.builder.static_accessor(key, get, set, attribute);
349        self
350    }
351
352    /// Add a property descriptor to the class, with the specified attribute.
353    ///
354    /// It is added to `prototype`.
355    pub fn property_descriptor<K, P>(&mut self, key: K, property: P) -> &mut Self
356    where
357        K: Into<PropertyKey>,
358        P: Into<PropertyDescriptor>,
359    {
360        self.builder.property_descriptor(key, property);
361        self
362    }
363
364    /// Add a static property descriptor to the class, with the specified attribute.
365    ///
366    /// It is added to class object itself.
367    pub fn static_property_descriptor<K, P>(&mut self, key: K, property: P) -> &mut Self
368    where
369        K: Into<PropertyKey>,
370        P: Into<PropertyDescriptor>,
371    {
372        self.builder.static_property_descriptor(key, property);
373        self
374    }
375
376    /// Return the current context.
377    #[inline]
378    pub fn context(&mut self) -> &mut Context {
379        self.builder.context()
380    }
381}