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}