Skip to main content

rquickjs_core/
class.rs

1//! JavaScript classes defined from Rust.
2
3use crate::{
4    function::Params,
5    qjs::{self},
6    value::Constructor,
7    Atom, Ctx, Error, FromJs, IntoJs, JsLifetime, Object, Result, Value,
8};
9use alloc::boxed::Box;
10use alloc::vec::Vec;
11use core::{hash::Hash, marker::PhantomData, mem, ops::Deref, ptr::NonNull};
12
13mod cell;
14mod trace;
15
16pub(crate) mod ffi;
17
18pub use cell::{
19    Borrow, BorrowMut, JsCell, Mutability, OwnedBorrow, OwnedBorrowMut, Readable, Writable,
20};
21use ffi::{ClassCell, VTable};
22pub use trace::{Trace, Tracer};
23#[doc(hidden)]
24pub mod impl_;
25
26/// The kind of a JavaScript class.
27///
28/// A class can't be both callable and exotic, so this enum encodes that constraint.
29#[derive(Debug, Clone, Copy, PartialEq, Eq)]
30pub enum ClassKind {
31    /// A regular class.
32    Plain,
33    /// A callable class (i.e. can be used as a function).
34    Callable,
35    /// An exotic class (i.e. has custom property access behavior).
36    Exotic,
37}
38
39/// A JavaScript property descriptor returned from [`JsClass::exotic_get_own_property`].
40pub struct PropertyDescriptor<'js> {
41    /// The property value (for data descriptors).
42    pub value: Value<'js>,
43    /// The getter function (for accessor descriptors).
44    pub getter: Value<'js>,
45    /// The setter function (for accessor descriptors).
46    pub setter: Value<'js>,
47    /// Whether the property is configurable.
48    pub configurable: bool,
49    /// Whether the property is enumerable.
50    pub enumerable: bool,
51    /// Whether the property is writable (data descriptors only).
52    pub writable: bool,
53    /// Whether this is a getter/setter descriptor.
54    pub is_getset: bool,
55}
56
57impl<'js> PropertyDescriptor<'js> {
58    /// Create a simple value property descriptor.
59    pub fn new_value(
60        value: Value<'js>,
61        configurable: bool,
62        enumerable: bool,
63        writable: bool,
64    ) -> Self {
65        let ctx = value.ctx().clone();
66        PropertyDescriptor {
67            value,
68            getter: Value::new_undefined(ctx.clone()),
69            setter: Value::new_undefined(ctx),
70            configurable,
71            enumerable,
72            writable,
73            is_getset: false,
74        }
75    }
76}
77
78/// A property name entry returned from [`JsClass::exotic_get_own_property_names`].
79pub struct PropertyName<'js> {
80    /// The atom identifying the property.
81    pub atom: Atom<'js>,
82    /// Whether this property is enumerable.
83    pub is_enumerable: bool,
84}
85
86/// The trait which allows Rust types to be used from JavaScript.
87pub trait JsClass<'js>: Trace<'js> + JsLifetime<'js> + Sized {
88    /// The name the constructor has in JavaScript
89    const NAME: &'static str;
90
91    /// The kind of this class (plain, callable, or exotic).
92    const KIND: ClassKind = ClassKind::Plain;
93
94    /// Can the type be mutated while a JavaScript value.
95    ///
96    /// This should either be [`Readable`] or [`Writable`].
97    type Mutable: Mutability;
98
99    /// Returns the class prototype,
100    fn prototype(ctx: &Ctx<'js>) -> Result<Option<Object<'js>>> {
101        Object::new(ctx.clone()).map(Some)
102    }
103
104    /// Returns a predefined constructor for this specific class type if there is one.
105    fn constructor(ctx: &Ctx<'js>) -> Result<Option<Constructor<'js>>>;
106
107    /// The function which will be called if [`Self::KIND`] is [`ClassKind::Callable`] and an object with this
108    /// class is called as if it is a function.
109    fn call<'a>(this: &JsCell<'js, Self>, params: Params<'a, 'js>) -> Result<Value<'js>> {
110        let _ = this;
111        Ok(Value::new_undefined(params.ctx().clone()))
112    }
113
114    /// The function which will be called if a get property is performed on an object with this class
115    fn exotic_get_property(
116        this: &JsCell<'js, Self>,
117        ctx: &Ctx<'js>,
118        _atom: Atom<'js>,
119        _receiver: Value<'js>,
120    ) -> Result<Value<'js>> {
121        let _ = this;
122        Ok(Value::new_undefined(ctx.clone()))
123    }
124
125    /// The function which will be called if a set property is performed on an object with this class
126    fn exotic_set_property(
127        this: &JsCell<'js, Self>,
128        _ctx: &Ctx<'js>,
129        _atom: Atom<'js>,
130        _receiver: Value<'js>,
131        _value: Value<'js>,
132    ) -> Result<bool> {
133        let _ = this;
134        Ok(false)
135    }
136
137    /// The function which will be called if a delete property is performed on an object with this class
138    fn exotic_delete_property(
139        this: &JsCell<'js, Self>,
140        _ctx: &Ctx<'js>,
141        _atom: Atom<'js>,
142    ) -> Result<bool> {
143        let _ = this;
144        Ok(false)
145    }
146
147    /// The function which will be called if has property or similar is called on an object with this class
148    fn exotic_has_property(
149        this: &JsCell<'js, Self>,
150        _ctx: &Ctx<'js>,
151        _atom: Atom<'js>,
152    ) -> Result<bool> {
153        let _ = this;
154        Ok(false)
155    }
156
157    /// Called to get the own property descriptor for a given property name.
158    ///
159    /// Return `Ok(Some(descriptor))` if the property exists, `Ok(None)` if it doesn't.
160    fn exotic_get_own_property(
161        this: &JsCell<'js, Self>,
162        _ctx: &Ctx<'js>,
163        _atom: Atom<'js>,
164    ) -> Result<Option<PropertyDescriptor<'js>>> {
165        let _ = this;
166        Ok(None)
167    }
168
169    /// Called to enumerate the own property names of this object.
170    ///
171    /// Return a list of property names.
172    fn exotic_get_own_property_names(
173        this: &JsCell<'js, Self>,
174        _ctx: &Ctx<'js>,
175    ) -> Result<Vec<PropertyName<'js>>> {
176        let _ = this;
177        Ok(Vec::new())
178    }
179}
180
181/// A object which is instance of a Rust class.
182#[repr(transparent)]
183pub struct Class<'js, C: JsClass<'js>>(pub(crate) Object<'js>, PhantomData<C>);
184
185impl<'js, C: JsClass<'js>> Clone for Class<'js, C> {
186    fn clone(&self) -> Self {
187        Class(self.0.clone(), PhantomData)
188    }
189}
190
191impl<'js, C: JsClass<'js>> PartialEq for Class<'js, C> {
192    fn eq(&self, other: &Self) -> bool {
193        self.0 == other.0
194    }
195}
196
197impl<'js, C: JsClass<'js>> Eq for Class<'js, C> {}
198
199impl<'js, C: JsClass<'js>> Hash for Class<'js, C> {
200    fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
201        self.0.hash(state)
202    }
203}
204
205unsafe impl<'js, C> JsLifetime<'js> for Class<'js, C>
206where
207    C: JsClass<'js> + JsLifetime<'js>,
208    for<'to> C::Changed<'to>: JsClass<'to>,
209{
210    type Changed<'to> = Class<'to, C::Changed<'to>>;
211}
212
213impl<'js, C: JsClass<'js>> Deref for Class<'js, C> {
214    type Target = Object<'js>;
215
216    fn deref(&self) -> &Self::Target {
217        &self.0
218    }
219}
220
221impl<'js, C: JsClass<'js>> Class<'js, C> {
222    /// Create a class from a Rust object.
223    pub fn instance(ctx: Ctx<'js>, value: C) -> Result<Class<'js, C>> {
224        let id = unsafe { class_id::<C>(&ctx)? };
225
226        let prototype = Self::prototype(&ctx)?;
227
228        let prototype = prototype.map(|x| x.as_js_value()).unwrap_or(qjs::JS_NULL);
229        let val = unsafe {
230            ctx.handle_exception(qjs::JS_NewObjectProtoClass(ctx.as_ptr(), prototype, id))?
231        };
232
233        let ptr = Box::into_raw(Box::new(ClassCell::new(value)));
234        unsafe { qjs::JS_SetOpaque(val, ptr.cast()) };
235        Ok(Self(
236            unsafe { Object::from_js_value(ctx, val) },
237            PhantomData,
238        ))
239    }
240
241    /// Create a class from a Rust object with a given prototype.
242    pub fn instance_proto(value: C, proto: Object<'js>) -> Result<Class<'js, C>> {
243        let id = unsafe { class_id::<C>(proto.ctx())? };
244
245        let val = unsafe {
246            proto.ctx.handle_exception(qjs::JS_NewObjectProtoClass(
247                proto.ctx().as_ptr(),
248                proto.0.as_js_value(),
249                id,
250            ))?
251        };
252        let ptr = Box::into_raw(Box::new(ClassCell::new(value)));
253        unsafe { qjs::JS_SetOpaque(val, ptr.cast()) };
254        Ok(Self(
255            unsafe { Object::from_js_value(proto.ctx.clone(), val) },
256            PhantomData,
257        ))
258    }
259
260    /// Returns the prototype for the class.
261    ///
262    /// Returns `None` if the class is not yet registered or if the class doesn't have a prototype.
263    pub fn prototype(ctx: &Ctx<'js>) -> Result<Option<Object<'js>>> {
264        unsafe { ctx.get_opaque().get_or_insert_prototype::<C>(ctx) }
265    }
266
267    /// Create a constructor for the current class using its definition.
268    pub fn create_constructor(ctx: &Ctx<'js>) -> Result<Option<Constructor<'js>>> {
269        C::constructor(ctx)
270    }
271
272    /// Defines the predefined constructor of this class, if there is one, onto the given object.
273    pub fn define(object: &Object<'js>) -> Result<()> {
274        if let Some(constructor) = Self::create_constructor(object.ctx())? {
275            object.set(C::NAME, constructor)?;
276        }
277        Ok(())
278    }
279
280    /// Returns a reference to the underlying object contained in a cell.
281    #[inline]
282    pub(crate) fn get_class_cell<'a>(&self) -> &'a ClassCell<JsCell<'js, C>> {
283        unsafe { self.get_class_ptr().as_ref() }
284    }
285
286    /// Returns a reference to the underlying object contained in a cell.
287    #[inline]
288    pub fn get_cell<'a>(&self) -> &'a JsCell<'js, C> {
289        &self.get_class_cell().data
290    }
291
292    /// Borrow the Rust class type.
293    ///
294    /// JavaScript classes behave similar to [`Rc`](std::rc::Rc) in Rust, you can essentially think
295    /// of a class object as a `Rc<RefCell<C>>` and with similar borrowing functionality.
296    ///
297    /// # Panic
298    /// This function panics if the class is already borrowed mutably.
299    #[inline]
300    pub fn borrow<'a>(&'a self) -> Borrow<'a, 'js, C> {
301        self.get_cell().borrow()
302    }
303
304    /// Borrow the Rust class type mutably.
305    ///
306    /// JavaScript classes behave similar to [`Rc`](std::rc::Rc) in Rust, you can essentially think
307    /// of a class object as a `Rc<RefCell<C>>` and with similar borrowing functionality.
308    ///
309    /// # Panic
310    /// This function panics if the class is already borrowed mutably or immutably, or the Class
311    /// can't be borrowed mutably.
312    #[inline]
313    pub fn borrow_mut<'a>(&'a self) -> BorrowMut<'a, 'js, C> {
314        self.get_cell().borrow_mut()
315    }
316
317    /// Try to borrow the Rust class type.
318    ///
319    /// JavaScript classes behave similar to [`Rc`](std::rc::Rc) in Rust, you can essentially think
320    /// of a class object as a `Rc<RefCell<C>>` and with similar borrowing functionality.
321    ///
322    /// This returns an error when the class is already borrowed mutably.
323    #[inline]
324    pub fn try_borrow<'a>(&'a self) -> Result<Borrow<'a, 'js, C>> {
325        self.get_cell().try_borrow().map_err(Error::ClassBorrow)
326    }
327
328    /// Try to borrow the Rust class type mutably.
329    ///
330    /// JavaScript classes behave similar to [`Rc`](std::rc::Rc) in Rust, you can essentially think
331    /// of a class object as a `Rc<RefCell<C>>` and with similar borrowing functionality.
332    ///
333    /// This returns an error when the class is already borrowed mutably, immutably or the class
334    /// can't be borrowed mutably.
335    #[inline]
336    pub fn try_borrow_mut<'a>(&'a self) -> Result<BorrowMut<'a, 'js, C>> {
337        self.get_cell().try_borrow_mut().map_err(Error::ClassBorrow)
338    }
339
340    /// returns a pointer to the class object.
341    #[inline]
342    pub(crate) fn get_class_ptr(&self) -> NonNull<ClassCell<JsCell<'js, C>>> {
343        let id = unsafe { class_id::<C>(&self.ctx).expect("invalid class") };
344
345        let ptr = unsafe { qjs::JS_GetOpaque2(self.0.ctx.as_ptr(), self.0 .0.as_js_value(), id) };
346
347        NonNull::new(ptr.cast()).expect("invalid class object, object didn't have opaque value")
348    }
349
350    /// Turns the class back into a generic object.
351    #[inline]
352    pub fn into_inner(self) -> Object<'js> {
353        self.0
354    }
355
356    /// Turns the class back into a generic object.
357    #[inline]
358    pub fn as_inner(&self) -> &Object<'js> {
359        &self.0
360    }
361
362    /// Convert from value.
363    #[inline]
364    pub fn from_value(value: &Value<'js>) -> Result<Self> {
365        if let Some(cls) = value.as_object().and_then(Self::from_object) {
366            return Ok(cls);
367        }
368        Err(Error::FromJs {
369            from: value.type_name(),
370            to: C::NAME,
371            message: None,
372        })
373    }
374
375    /// Turn the class into a value.
376    #[inline]
377    pub fn into_value(self) -> Value<'js> {
378        self.0.into_value()
379    }
380
381    /// Converts a generic object into a class if the object is of the right class.
382    #[inline]
383    pub fn from_object(object: &Object<'js>) -> Option<Self> {
384        object.into_class().ok()
385    }
386}
387
388impl<'js> Object<'js> {
389    /// Returns if the object is of a certain Rust class.
390    pub fn instance_of<C: JsClass<'js>>(&self) -> bool {
391        let Ok(id) = (unsafe { class_id::<C>(&self.ctx) }) else {
392            return false;
393        };
394
395        // This checks if the class is of the right class id.
396        let Some(x) = NonNull::new(unsafe {
397            qjs::JS_GetOpaque2(self.0.ctx.as_ptr(), self.0.as_js_value(), id)
398        }) else {
399            return false;
400        };
401
402        let v_table = unsafe { x.cast::<ClassCell<()>>().as_ref().v_table };
403
404        // If the pointer is equal it must be of the right type, as the inclusion of a call to
405        // generate a TypeId means that each type must have a unique v table.
406        // however if it is not equal then it can still be the right type if the v_table is
407        // duplicated, which is possible when compilation with multiple code-gen units.
408        //
409        // Doing check avoids a lookup and an dynamic function call in some cases.
410        if core::ptr::eq(v_table, VTable::get::<C>()) {
411            return true;
412        }
413
414        v_table.is_of_class::<C>()
415    }
416
417    /// Turn the object into the class if it is an instance of that class.
418    pub fn into_class<C: JsClass<'js>>(&self) -> core::result::Result<Class<'js, C>, &Self> {
419        if self.instance_of::<C>() {
420            Ok(Class(self.clone(), PhantomData))
421        } else {
422            Err(self)
423        }
424    }
425
426    /// Turn the object into the class if it is an instance of that class.
427    pub fn as_class<C: JsClass<'js>>(&self) -> Option<&Class<'js, C>> {
428        if self.instance_of::<C>() {
429            // SAFETY:
430            // Safe because class is a transparent wrapper
431            unsafe { Some(mem::transmute::<&Object<'js>, &Class<'js, C>>(self)) }
432        } else {
433            None
434        }
435    }
436}
437
438impl<'js, C: JsClass<'js>> FromJs<'js> for Class<'js, C> {
439    fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> Result<Self> {
440        Self::from_value(&value)
441    }
442}
443
444impl<'js, C: JsClass<'js>> IntoJs<'js> for Class<'js, C> {
445    fn into_js(self, _ctx: &Ctx<'js>) -> Result<Value<'js>> {
446        Ok(self.0 .0)
447    }
448}
449
450unsafe fn class_id<'js, C: JsClass<'js>>(ctx: &Ctx<'js>) -> Result<qjs::JSClassID> {
451    match C::KIND {
452        ClassKind::Plain => Ok(ctx.get_opaque().get_class_id()),
453        ClassKind::Callable => Ok(ctx.get_opaque().get_callable_id()),
454        ClassKind::Exotic => Ok(ctx.get_opaque().get_exotic_id()),
455    }
456}
457
458#[cfg(test)]
459mod test {
460    use core::sync::atomic::AtomicI32;
461    use std::sync::{
462        atomic::{AtomicBool, Ordering},
463        Arc,
464    };
465
466    use crate::{
467        class::{ClassKind, JsClass, Readable, Trace, Tracer, Writable},
468        function::This,
469        test_with,
470        value::Constructor,
471        CatchResultExt, Class, Context, FromIteratorJs, FromJs, Function, IntoJs, JsLifetime,
472        Object, Runtime,
473    };
474
475    /// Test circular references.
476    #[test]
477    fn trace() {
478        pub struct Container<'js> {
479            inner: Vec<Class<'js, Container<'js>>>,
480            test: Arc<AtomicBool>,
481        }
482
483        impl<'js> Drop for Container<'js> {
484            fn drop(&mut self) {
485                self.test.store(true, Ordering::SeqCst);
486            }
487        }
488
489        impl<'js> Trace<'js> for Container<'js> {
490            fn trace<'a>(&self, tracer: Tracer<'a, 'js>) {
491                self.inner.iter().for_each(|x| x.trace(tracer))
492            }
493        }
494
495        unsafe impl<'js> JsLifetime<'js> for Container<'js> {
496            type Changed<'to> = Container<'to>;
497        }
498
499        impl<'js> JsClass<'js> for Container<'js> {
500            const NAME: &'static str = "Container";
501
502            type Mutable = Writable;
503
504            fn prototype(ctx: &crate::Ctx<'js>) -> crate::Result<Option<crate::Object<'js>>> {
505                Ok(Some(Object::new(ctx.clone())?))
506            }
507
508            fn constructor(
509                _ctx: &crate::Ctx<'js>,
510            ) -> crate::Result<Option<crate::value::Constructor<'js>>> {
511                Ok(None)
512            }
513        }
514
515        let rt = Runtime::new().unwrap();
516        let ctx = Context::full(&rt).unwrap();
517
518        let drop_test = Arc::new(AtomicBool::new(false));
519
520        ctx.with(|ctx| {
521            let cls = Class::instance(
522                ctx.clone(),
523                Container {
524                    inner: Vec::new(),
525                    test: drop_test.clone(),
526                },
527            )
528            .unwrap();
529
530            assert!(cls.instance_of::<Container>());
531
532            let cls_clone = cls.clone();
533            cls.borrow_mut().inner.push(cls_clone);
534        });
535        rt.run_gc();
536        assert!(drop_test.load(Ordering::SeqCst));
537        ctx.with(|ctx| {
538            let cls = Class::instance(
539                ctx.clone(),
540                Container {
541                    inner: Vec::new(),
542                    test: drop_test.clone(),
543                },
544            )
545            .unwrap();
546            let cls_clone = cls.clone();
547            cls.borrow_mut().inner.push(cls_clone);
548            ctx.globals().set("t", cls).unwrap();
549        });
550    }
551
552    #[derive(Clone, Copy)]
553    pub struct Vec3 {
554        x: f32,
555        y: f32,
556        z: f32,
557    }
558
559    impl Vec3 {
560        pub fn new(x: f32, y: f32, z: f32) -> Self {
561            Vec3 { x, y, z }
562        }
563
564        pub fn add(self, v: Vec3) -> Self {
565            Vec3 {
566                x: self.x + v.x,
567                y: self.y + v.y,
568                z: self.z + v.z,
569            }
570        }
571    }
572
573    impl<'js> Trace<'js> for Vec3 {
574        fn trace<'a>(&self, _tracer: Tracer<'a, 'js>) {}
575    }
576
577    impl<'js> FromJs<'js> for Vec3 {
578        fn from_js(ctx: &crate::Ctx<'js>, value: crate::Value<'js>) -> crate::Result<Self> {
579            Ok(*Class::<Vec3>::from_js(ctx, value)?.try_borrow()?)
580        }
581    }
582
583    impl<'js> IntoJs<'js> for Vec3 {
584        fn into_js(self, ctx: &crate::Ctx<'js>) -> crate::Result<crate::Value<'js>> {
585            Class::instance(ctx.clone(), self).into_js(ctx)
586        }
587    }
588
589    unsafe impl<'js> JsLifetime<'js> for Vec3 {
590        type Changed<'to> = Vec3;
591    }
592
593    impl<'js> JsClass<'js> for Vec3 {
594        const NAME: &'static str = "Vec3";
595
596        type Mutable = Writable;
597
598        fn prototype(ctx: &crate::Ctx<'js>) -> crate::Result<Option<crate::Object<'js>>> {
599            let proto = Object::new(ctx.clone())?;
600            let func = Function::new(ctx.clone(), |this: This<Vec3>, other: Vec3| this.add(other))?
601                .with_name("add")?;
602
603            proto.set("add", func)?;
604            Ok(Some(proto))
605        }
606
607        fn constructor(
608            ctx: &crate::Ctx<'js>,
609        ) -> crate::Result<Option<crate::value::Constructor<'js>>> {
610            let constr =
611                Constructor::new_class::<Vec3, _, _>(ctx.clone(), |x: f32, y: f32, z: f32| {
612                    Vec3::new(x, y, z)
613                })?;
614
615            Ok(Some(constr))
616        }
617    }
618
619    #[test]
620    fn constructor() {
621        test_with(|ctx| {
622            Class::<Vec3>::define(&ctx.globals()).unwrap();
623
624            let v = ctx
625                .eval::<Vec3, _>(
626                    r"
627                let a = new Vec3(1,2,3);
628                let b = new Vec3(4,2,8);
629                a.add(b)
630            ",
631                )
632                .catch(&ctx)
633                .unwrap();
634
635            approx::assert_abs_diff_eq!(v.x, 5.0);
636            approx::assert_abs_diff_eq!(v.y, 4.0);
637            approx::assert_abs_diff_eq!(v.z, 11.0);
638
639            let name: String = ctx.eval("new Vec3(1,2,3).constructor.name").unwrap();
640            assert_eq!(name, Vec3::NAME);
641        })
642    }
643
644    #[test]
645    fn extend_class() {
646        test_with(|ctx| {
647            Class::<Vec3>::define(&ctx.globals()).unwrap();
648
649            let v = ctx
650                .eval::<Vec3, _>(
651                    r"
652                    class Vec4 extends Vec3 {
653                        w = 0;
654                        constructor(x,y,z,w){
655                            super(x,y,z);
656                            this.w
657                        }
658                    }
659
660                    new Vec4(1,2,3,4);
661                ",
662                )
663                .catch(&ctx)
664                .unwrap();
665
666            approx::assert_abs_diff_eq!(v.x, 1.0);
667            approx::assert_abs_diff_eq!(v.y, 2.0);
668            approx::assert_abs_diff_eq!(v.z, 3.0);
669        })
670    }
671
672    #[test]
673    fn get_prototype() {
674        pub struct X;
675
676        impl<'js> Trace<'js> for X {
677            fn trace<'a>(&self, _tracer: Tracer<'a, 'js>) {}
678        }
679
680        unsafe impl<'js> JsLifetime<'js> for X {
681            type Changed<'to> = X;
682        }
683
684        impl<'js> JsClass<'js> for X {
685            const NAME: &'static str = "X";
686
687            type Mutable = Readable;
688
689            fn prototype(ctx: &crate::Ctx<'js>) -> crate::Result<Option<Object<'js>>> {
690                let object = Object::new(ctx.clone())?;
691                object.set("foo", "bar")?;
692                Ok(Some(object))
693            }
694
695            fn constructor(_ctx: &crate::Ctx<'js>) -> crate::Result<Option<Constructor<'js>>> {
696                Ok(None)
697            }
698        }
699
700        test_with(|ctx| {
701            let proto = Class::<X>::prototype(&ctx).unwrap().unwrap();
702            assert_eq!(proto.get::<_, String>("foo").unwrap(), "bar")
703        })
704    }
705
706    #[test]
707    fn generic_types() {
708        pub struct DebugPrinter<D: std::fmt::Debug> {
709            d: D,
710        }
711
712        impl<'js, D: std::fmt::Debug> Trace<'js> for DebugPrinter<D> {
713            fn trace<'a>(&self, _tracer: Tracer<'a, 'js>) {}
714        }
715
716        unsafe impl<'js, D: std::fmt::Debug + 'static> JsLifetime<'js> for DebugPrinter<D> {
717            type Changed<'to> = DebugPrinter<D>;
718        }
719
720        impl<'js, D: std::fmt::Debug + 'static> JsClass<'js> for DebugPrinter<D> {
721            const NAME: &'static str = "DebugPrinter";
722
723            type Mutable = Readable;
724
725            fn prototype(ctx: &crate::Ctx<'js>) -> crate::Result<Option<Object<'js>>> {
726                let object = Object::new(ctx.clone())?;
727                object.set(
728                    "to_debug_string",
729                    Function::new(
730                        ctx.clone(),
731                        |this: This<Class<DebugPrinter<D>>>| -> crate::Result<String> {
732                            Ok(format!("{:?}", this.0.borrow().d))
733                        },
734                    ),
735                )?;
736                Ok(Some(object))
737            }
738
739            fn constructor(_ctx: &crate::Ctx<'js>) -> crate::Result<Option<Constructor<'js>>> {
740                Ok(None)
741            }
742        }
743
744        test_with(|ctx| {
745            let a = Class::instance(ctx.clone(), DebugPrinter { d: 42usize });
746            let b = Class::instance(
747                ctx.clone(),
748                DebugPrinter {
749                    d: "foo".to_string(),
750                },
751            );
752
753            ctx.globals().set("a", a).unwrap();
754            ctx.globals().set("b", b).unwrap();
755
756            assert_eq!(
757                ctx.eval::<String, _>(r#" a.to_debug_string() "#)
758                    .catch(&ctx)
759                    .unwrap(),
760                "42"
761            );
762            assert_eq!(
763                ctx.eval::<String, _>(r#" b.to_debug_string() "#)
764                    .catch(&ctx)
765                    .unwrap(),
766                "\"foo\""
767            );
768
769            if ctx
770                .globals()
771                .get::<_, Class<DebugPrinter<String>>>("a")
772                .is_ok()
773            {
774                panic!("Conversion should fail")
775            }
776            if ctx
777                .globals()
778                .get::<_, Class<DebugPrinter<usize>>>("b")
779                .is_ok()
780            {
781                panic!("Conversion should fail")
782            }
783
784            ctx.globals()
785                .get::<_, Class<DebugPrinter<usize>>>("a")
786                .unwrap();
787            ctx.globals()
788                .get::<_, Class<DebugPrinter<String>>>("b")
789                .unwrap();
790        })
791    }
792
793    #[test]
794    fn exotic() {
795        pub struct ExoticIterator {
796            curr_state: Arc<AtomicI32>,
797        }
798
799        impl<'js> Trace<'js> for ExoticIterator {
800            fn trace<'a>(&self, _tracer: Tracer<'a, 'js>) {}
801        }
802
803        unsafe impl<'js> JsLifetime<'js> for ExoticIterator {
804            type Changed<'to> = ExoticIterator;
805        }
806
807        impl<'js> JsClass<'js> for ExoticIterator {
808            const NAME: &'static str = "ExoticIterator";
809
810            type Mutable = Readable;
811
812            const KIND: ClassKind = ClassKind::Exotic;
813
814            fn prototype(ctx: &crate::Ctx<'js>) -> crate::Result<Option<crate::Object<'js>>> {
815                Ok(Some(crate::Object::new(ctx.clone())?))
816            }
817
818            fn constructor(
819                _ctx: &crate::Ctx<'js>,
820            ) -> crate::Result<Option<crate::value::Constructor<'js>>> {
821                Ok(None)
822            }
823
824            fn exotic_get_property(
825                this: &crate::class::JsCell<'js, Self>,
826                ctx: &crate::Ctx<'js>,
827                atom: crate::Atom<'js>,
828                _receiver: crate::Value<'js>,
829            ) -> crate::Result<crate::Value<'js>> {
830                println!("Get property [iter]: {}", atom.to_string()?);
831                if atom.to_string()? == "next" {
832                    let state = this.borrow().curr_state.clone();
833                    Ok(Function::new(ctx.clone(), move |ctx: crate::Ctx<'js>| {
834                        // A really awful iterator thats implemented as a handwritten state machine
835                        //
836                        // Do not use this in production
837                        if state.load(Ordering::SeqCst) <= 1 {
838                            state.store(2, Ordering::SeqCst);
839
840                            let val = crate::Object::from_iter_js(
841                                &ctx,
842                                [
843                                    ("done", false.into_js(&ctx)?),
844                                    ("value", vec!["hello", "1292"].into_js(&ctx)?),
845                                ],
846                            )?
847                            .into_value();
848
849                            Ok::<crate::Value<'_>, crate::Error>(val)
850                        } else if state.load(Ordering::SeqCst) == 2 {
851                            state.fetch_add(1, Ordering::SeqCst);
852
853                            let val = crate::Object::from_iter_js(
854                                &ctx,
855                                [
856                                    ("done", false.into_js(&ctx)?),
857                                    (
858                                        "value",
859                                        vec!["i".into_js(&ctx)?, 43.into_js(&ctx)?]
860                                            .into_js(&ctx)?,
861                                    ),
862                                ],
863                            )?
864                            .into_value();
865
866                            Ok(val)
867                        } else {
868                            state.fetch_add(1, Ordering::SeqCst);
869
870                            let val = crate::Object::from_iter_js(
871                                &ctx,
872                                [
873                                    ("done", true.into_js(&ctx)?),
874                                    ("value", crate::Value::new_undefined(ctx.clone())),
875                                ],
876                            )?
877                            .into_value();
878
879                            Ok(val)
880                        }
881                    })?
882                    .into_value())
883                } else {
884                    Ok(crate::Value::new_undefined(ctx.clone()))
885                }
886            }
887
888            fn exotic_has_property(
889                this: &super::JsCell<'js, Self>,
890                _ctx: &crate::Ctx<'js>,
891                atom: crate::Atom<'js>,
892            ) -> crate::Result<bool> {
893                let _ = this;
894                if atom.to_string()? == "next" {
895                    return Ok(true);
896                }
897
898                Ok(false)
899            }
900        }
901
902        #[derive(Clone)]
903        pub struct Exotic {
904            pub i: i32,
905        }
906
907        impl<'js> Trace<'js> for Exotic {
908            fn trace<'a>(&self, _tracer: Tracer<'a, 'js>) {}
909        }
910
911        unsafe impl<'js> JsLifetime<'js> for Exotic {
912            type Changed<'to> = Exotic;
913        }
914
915        impl<'js> JsClass<'js> for Exotic {
916            const NAME: &'static str = "Exotic";
917
918            type Mutable = Writable;
919
920            const KIND: ClassKind = ClassKind::Exotic;
921
922            fn prototype(ctx: &crate::Ctx<'js>) -> crate::Result<Option<crate::Object<'js>>> {
923                Ok(Some(crate::Object::new(ctx.clone())?))
924            }
925
926            fn constructor(
927                _ctx: &crate::Ctx<'js>,
928            ) -> crate::Result<Option<crate::value::Constructor<'js>>> {
929                Ok(None)
930            }
931
932            fn exotic_get_property(
933                this: &crate::class::JsCell<'js, Self>,
934                ctx: &crate::Ctx<'js>,
935                atom: crate::Atom<'js>,
936                _receiver: crate::Value<'js>,
937            ) -> crate::Result<crate::Value<'js>> {
938                let symbol_iterator = crate::Atom::from_predefined(
939                    ctx.clone(),
940                    crate::atom::PredefinedAtom::SymbolIterator,
941                );
942                println!("Get property: {}", atom.to_string()?);
943                if atom.to_string()? == "hello" {
944                    assert!(this.borrow().i == 42);
945                    Ok("world".into_js(ctx)?)
946                } else if atom.to_string()? == "toString" {
947                    Ok(Function::new(ctx.clone(), || {
948                        let f = "class Exotic { [native code] }";
949                        Ok::<&'static str, crate::Error>(f)
950                    })?
951                    .into_value())
952                } else if atom == symbol_iterator {
953                    println!("Getting iterator");
954                    let exotic = Class::<ExoticIterator>::instance(
955                        ctx.clone(),
956                        ExoticIterator {
957                            curr_state: Arc::default(),
958                        },
959                    )?;
960                    println!("Returning ExoticIterator");
961                    Ok(Function::new(ctx.clone(), move || {
962                        Ok::<crate::Value<'_>, crate::Error>(exotic.clone().into_value())
963                    })?
964                    .into_value())
965                } else {
966                    Ok(crate::Value::new_null(ctx.clone()))
967                }
968            }
969
970            fn exotic_set_property(
971                this: &super::JsCell<'js, Self>,
972                ctx: &crate::Ctx<'js>,
973                atom: crate::Atom<'js>,
974                _receiver: crate::Value<'js>,
975                _value: crate::Value<'js>,
976            ) -> crate::Result<bool> {
977                let _ = this;
978                if atom.to_string()? == "i" {
979                    let Some(new_i) = _value.as_int() else {
980                        let err_val = crate::String::from_str(ctx.clone(), "i must be an integer")?
981                            .into_value();
982                        return Err(ctx.throw(err_val));
983                    };
984                    this.borrow_mut().i = new_i;
985                    return Ok(true);
986                }
987                let err_val =
988                    crate::String::from_str(ctx.clone(), "Properties are read-only")?.into_value();
989                Err(ctx.throw(err_val))
990            }
991
992            fn exotic_has_property(
993                this: &super::JsCell<'js, Self>,
994                _ctx: &crate::Ctx<'js>,
995                atom: crate::Atom<'js>,
996            ) -> crate::Result<bool> {
997                let _ = this;
998                println!("Got atom: {}", atom.to_string()?);
999                if atom.to_string()? == "hello"
1000                    || atom.to_string()? == "i"
1001                    || atom.to_string()? == "toString"
1002                {
1003                    return Ok(true);
1004                }
1005
1006                Ok(false)
1007            }
1008
1009            fn exotic_delete_property(
1010                _this: &super::JsCell<'js, Self>,
1011                ctx: &crate::Ctx<'js>,
1012                _atom: crate::Atom<'js>,
1013            ) -> crate::Result<bool> {
1014                let err_val = crate::String::from_str(ctx.clone(), "Properties cannot be deleted")?
1015                    .into_value();
1016                Err(ctx.throw(err_val))
1017            }
1018
1019            fn exotic_get_own_property(
1020                this: &super::JsCell<'js, Self>,
1021                ctx: &crate::Ctx<'js>,
1022                atom: crate::Atom<'js>,
1023            ) -> crate::Result<Option<super::PropertyDescriptor<'js>>> {
1024                let name = atom.to_string()?;
1025                if name == "hello" || name == "i" {
1026                    let value = if name == "hello" {
1027                        "world".into_js(ctx)?
1028                    } else {
1029                        this.borrow().i.into_js(ctx)?
1030                    };
1031                    Ok(Some(super::PropertyDescriptor::new_value(
1032                        value, true, true, false,
1033                    )))
1034                } else {
1035                    Ok(None)
1036                }
1037            }
1038
1039            fn exotic_get_own_property_names(
1040                _this: &super::JsCell<'js, Self>,
1041                ctx: &crate::Ctx<'js>,
1042            ) -> crate::Result<Vec<super::PropertyName<'js>>> {
1043                Ok(vec![
1044                    super::PropertyName {
1045                        atom: crate::Atom::from_str(ctx.clone(), "hello")?,
1046                        is_enumerable: true,
1047                    },
1048                    super::PropertyName {
1049                        atom: crate::Atom::from_str(ctx.clone(), "i")?,
1050                        is_enumerable: true,
1051                    },
1052                ])
1053            }
1054        }
1055
1056        test_with(|ctx| {
1057            let exotic = Class::<Exotic>::instance(ctx.clone(), Exotic { i: 0 }).unwrap();
1058            ctx.globals().set("exotic", exotic).unwrap();
1059            ctx.globals()
1060                .set(
1061                    "assert",
1062                    Function::new(
1063                        ctx.clone(),
1064                        |ctx: crate::Ctx<'_>, cond: bool, msg: String| {
1065                            if !cond {
1066                                let err_val =
1067                                    crate::String::from_str(ctx.clone(), &msg)?.into_value();
1068                                return Err(ctx.throw(err_val));
1069                            }
1070                            Ok(())
1071                        },
1072                    ),
1073                )
1074                .unwrap();
1075
1076            let v = ctx
1077                .eval::<String, _>(
1078                    r"
1079                if(exotic.foo !== null) {
1080                    throw new Error('foo should be null');
1081                }
1082                try {
1083                    exotic.foo = 1
1084                } catch(e) {
1085                    if (e?.toString() !== 'Properties are read-only') {
1086                        throw new Error('wrong error message: ' + e?.toString());
1087                    }
1088                }
1089                if (exotic.foo !== null) {
1090                    throw new Error('foo should be null');
1091                }
1092                exotic.i = 42;
1093                if (exotic.hello === 42) {
1094                    throw new Error('i should be 42');
1095                }
1096                assert(exotic?.toString() === 'class Exotic { [native code] }', `exotic.toString() should be 'class Exotic { [native code] }' but is ${exotic?.toString()}`);
1097                assert('i' in exotic, 'i should be in exotic');
1098                assert('hello' in exotic, 'hello should be in exotic');
1099                assert(!('foo' in exotic), 'foo should not be in exotic');
1100
1101                try {
1102                    delete exotic.i;
1103                } catch(e) {
1104                    if (e?.toString() !== 'Properties cannot be deleted') {
1105                        throw new Error('wrong error message: ' + e?.toString());
1106                    }
1107                }
1108
1109                let resp = []
1110                for (let [objKey, value] of exotic) {
1111                    if (objKey !== 'i' && objKey !== 'hello') {
1112                        throw new Error('only i and hello should be enumerable, got ' + objKey);
1113                    }
1114                    resp.push(`${objKey}:${value}`);
1115                }
1116
1117                assert(resp.toString() === 'hello:1292,i:43', `${resp.toString()} with length ${resp.length} should be [] as properties are not enumerable`);
1118
1119                // Test Object.getOwnPropertyNames() (uses get_own_property_names)
1120                let ownNames = Object.getOwnPropertyNames(exotic);
1121                assert(ownNames.length === 2, `getOwnPropertyNames should return 2, got ${ownNames.length}`);
1122                assert(ownNames.includes('hello'), 'getOwnPropertyNames should include hello');
1123                assert(ownNames.includes('i'), 'getOwnPropertyNames should include i');
1124
1125                // Test Object.keys() (uses get_own_property_names + get_own_property)
1126                let keys = Object.keys(exotic);
1127                assert(keys.length === 2, `Object.keys should return 2 keys, got ${keys.length}`);
1128
1129                // Test Object.getOwnPropertyDescriptor() (uses get_own_property)
1130                let desc = Object.getOwnPropertyDescriptor(exotic, 'hello');
1131                assert(desc !== undefined, 'descriptor for hello should exist');
1132                assert(desc.value === 'world', `descriptor value should be world, got ${desc.value}`);
1133                assert(desc.configurable === true, 'hello should be configurable');
1134                assert(desc.enumerable === true, 'hello should be enumerable');
1135                assert(desc.writable === false, 'hello should not be writable');
1136
1137                // Non-existent property returns undefined descriptor
1138                assert(Object.getOwnPropertyDescriptor(exotic, 'nonexistent') === undefined, 'nonexistent should be undefined');
1139
1140                exotic.hello
1141            ",
1142                )
1143                .catch(&ctx)
1144                .unwrap();
1145
1146            assert_eq!(v, "world");
1147        })
1148    }
1149}