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 binging 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,
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.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::intrinsics::StandardConstructor,
108    error::JsNativeError,
109    native_function::NativeFunction,
110    object::{ConstructorBuilder, FunctionBinding, JsFunction, JsObject, NativeObject, PROTOTYPE},
111    property::{Attribute, PropertyDescriptor, PropertyKey},
112    Context, JsResult, JsValue,
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,
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)
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)
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    pub(crate) fn new<T>(context: &'ctx mut Context) -> Self
248    where
249        T: Class,
250    {
251        let mut builder = ConstructorBuilder::new(
252            context,
253            NativeFunction::from_fn_ptr(|t, a, c| T::construct(t, a, c).map(JsValue::from)),
254        );
255        builder.name(T::NAME);
256        builder.length(T::LENGTH);
257        Self { builder }
258    }
259
260    pub(crate) fn build(self) -> StandardConstructor {
261        self.builder.build()
262    }
263
264    /// Add a method to the class.
265    ///
266    /// It is added to `prototype`.
267    pub fn method<N>(&mut self, name: N, length: usize, function: NativeFunction) -> &mut Self
268    where
269        N: Into<FunctionBinding>,
270    {
271        self.builder.method(function, name, length);
272        self
273    }
274
275    /// Add a static method to the class.
276    ///
277    /// It is added to class object itself.
278    pub fn static_method<N>(
279        &mut self,
280        name: N,
281        length: usize,
282        function: NativeFunction,
283    ) -> &mut Self
284    where
285        N: Into<FunctionBinding>,
286    {
287        self.builder.static_method(function, name, length);
288        self
289    }
290
291    /// Add a data property to the class, with the specified attribute.
292    ///
293    /// It is added to `prototype`.
294    pub fn property<K, V>(&mut self, key: K, value: V, attribute: Attribute) -> &mut Self
295    where
296        K: Into<PropertyKey>,
297        V: Into<JsValue>,
298    {
299        self.builder.property(key, value, attribute);
300        self
301    }
302
303    /// Add a static data property to the class, with the specified attribute.
304    ///
305    /// It is added to class object itself.
306    pub fn static_property<K, V>(&mut self, key: K, value: V, attribute: Attribute) -> &mut Self
307    where
308        K: Into<PropertyKey>,
309        V: Into<JsValue>,
310    {
311        self.builder.static_property(key, value, attribute);
312        self
313    }
314
315    /// Add an accessor property to the class, with the specified attribute.
316    ///
317    /// It is added to `prototype`.
318    pub fn accessor<K>(
319        &mut self,
320        key: K,
321        get: Option<JsFunction>,
322        set: Option<JsFunction>,
323        attribute: Attribute,
324    ) -> &mut Self
325    where
326        K: Into<PropertyKey>,
327    {
328        self.builder.accessor(key, get, set, attribute);
329        self
330    }
331
332    /// Add a static accessor property to the class, with the specified attribute.
333    ///
334    /// It is added to class object itself.
335    pub fn static_accessor<K>(
336        &mut self,
337        key: K,
338        get: Option<JsFunction>,
339        set: Option<JsFunction>,
340        attribute: Attribute,
341    ) -> &mut Self
342    where
343        K: Into<PropertyKey>,
344    {
345        self.builder.static_accessor(key, get, set, attribute);
346        self
347    }
348
349    /// Add a property descriptor to the class, with the specified attribute.
350    ///
351    /// It is added to `prototype`.
352    pub fn property_descriptor<K, P>(&mut self, key: K, property: P) -> &mut Self
353    where
354        K: Into<PropertyKey>,
355        P: Into<PropertyDescriptor>,
356    {
357        self.builder.property_descriptor(key, property);
358        self
359    }
360
361    /// Add a static property descriptor to the class, with the specified attribute.
362    ///
363    /// It is added to class object itself.
364    pub fn static_property_descriptor<K, P>(&mut self, key: K, property: P) -> &mut Self
365    where
366        K: Into<PropertyKey>,
367        P: Into<PropertyDescriptor>,
368    {
369        self.builder.static_property_descriptor(key, property);
370        self
371    }
372
373    /// Return the current context.
374    #[inline]
375    pub fn context(&mut self) -> &mut Context {
376        self.builder.context()
377    }
378}