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}