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>(&'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>(&'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            // `JS_GetOpaque2` throws a `TypeError` internally whenever the class id
400            // doesn't match, leaving it pending on the runtime even though a
401            // negative result here is an ordinary, expected outcome. Clear it so a
402            // failed `instance_of` can't be mistaken for a real exception by code
403            // that later checks `Ctx::has_exception`/`Ctx::catch` for something
404            // unrelated (e.g. a pending `Promise`).
405            self.ctx.catch();
406            return false;
407        };
408
409        let v_table = unsafe { x.cast::<ClassCell<()>>().as_ref().v_table };
410
411        // If the pointer is equal it must be of the right type, as the inclusion of a call to
412        // generate a TypeId means that each type must have a unique v table.
413        // however if it is not equal then it can still be the right type if the v_table is
414        // duplicated, which is possible when compilation with multiple code-gen units.
415        //
416        // Doing check avoids a lookup and an dynamic function call in some cases.
417        if core::ptr::eq(v_table, VTable::get::<C>()) {
418            return true;
419        }
420
421        v_table.is_of_class::<C>()
422    }
423
424    /// Turn the object into the class if it is an instance of that class.
425    pub fn into_class<C: JsClass<'js>>(&self) -> core::result::Result<Class<'js, C>, &Self> {
426        if self.instance_of::<C>() {
427            Ok(Class(self.clone(), PhantomData))
428        } else {
429            Err(self)
430        }
431    }
432
433    /// Turn the object into the class if it is an instance of that class.
434    pub fn as_class<C: JsClass<'js>>(&self) -> Option<&Class<'js, C>> {
435        if self.instance_of::<C>() {
436            // SAFETY:
437            // Safe because class is a transparent wrapper
438            unsafe { Some(mem::transmute::<&Object<'js>, &Class<'js, C>>(self)) }
439        } else {
440            None
441        }
442    }
443}
444
445impl<'js, C: JsClass<'js>> FromJs<'js> for Class<'js, C> {
446    fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> Result<Self> {
447        Self::from_value(&value)
448    }
449}
450
451impl<'js, C: JsClass<'js>> IntoJs<'js> for Class<'js, C> {
452    fn into_js(self, _ctx: &Ctx<'js>) -> Result<Value<'js>> {
453        Ok(self.0 .0)
454    }
455}
456
457unsafe fn class_id<'js, C: JsClass<'js>>(ctx: &Ctx<'js>) -> Result<qjs::JSClassID> {
458    match C::KIND {
459        ClassKind::Plain => Ok(ctx.get_opaque().get_class_id()),
460        ClassKind::Callable => Ok(ctx.get_opaque().get_callable_id()),
461        ClassKind::Exotic => Ok(ctx.get_opaque().get_exotic_id()),
462    }
463}
464
465#[cfg(test)]
466mod test {
467    use core::sync::atomic::AtomicI32;
468    use std::sync::{
469        atomic::{AtomicBool, Ordering},
470        Arc,
471    };
472
473    use crate::{
474        class::{ClassKind, JsClass, Readable, Trace, Tracer, Writable},
475        function::This,
476        test_with,
477        value::Constructor,
478        CatchResultExt, Class, Context, FromIteratorJs, FromJs, Function, IntoJs, JsLifetime,
479        Object, Runtime,
480    };
481
482    /// Test circular references.
483    #[test]
484    fn trace() {
485        pub struct Container<'js> {
486            inner: Vec<Class<'js, Container<'js>>>,
487            test: Arc<AtomicBool>,
488        }
489
490        impl<'js> Drop for Container<'js> {
491            fn drop(&mut self) {
492                self.test.store(true, Ordering::SeqCst);
493            }
494        }
495
496        impl<'js> Trace<'js> for Container<'js> {
497            fn trace<'a>(&self, tracer: Tracer<'a, 'js>) {
498                self.inner.iter().for_each(|x| x.trace(tracer))
499            }
500        }
501
502        unsafe impl<'js> JsLifetime<'js> for Container<'js> {
503            type Changed<'to> = Container<'to>;
504        }
505
506        impl<'js> JsClass<'js> for Container<'js> {
507            const NAME: &'static str = "Container";
508
509            type Mutable = Writable;
510
511            fn prototype(ctx: &crate::Ctx<'js>) -> crate::Result<Option<crate::Object<'js>>> {
512                Ok(Some(Object::new(ctx.clone())?))
513            }
514
515            fn constructor(
516                _ctx: &crate::Ctx<'js>,
517            ) -> crate::Result<Option<crate::value::Constructor<'js>>> {
518                Ok(None)
519            }
520        }
521
522        let rt = Runtime::new().unwrap();
523        let ctx = Context::full(&rt).unwrap();
524
525        let drop_test = Arc::new(AtomicBool::new(false));
526
527        ctx.with(|ctx| {
528            let cls = Class::instance(
529                ctx.clone(),
530                Container {
531                    inner: Vec::new(),
532                    test: drop_test.clone(),
533                },
534            )
535            .unwrap();
536
537            assert!(cls.instance_of::<Container>());
538
539            let cls_clone = cls.clone();
540            cls.borrow_mut().inner.push(cls_clone);
541        });
542        rt.run_gc();
543        assert!(drop_test.load(Ordering::SeqCst));
544        ctx.with(|ctx| {
545            let cls = Class::instance(
546                ctx.clone(),
547                Container {
548                    inner: Vec::new(),
549                    test: drop_test.clone(),
550                },
551            )
552            .unwrap();
553            let cls_clone = cls.clone();
554            cls.borrow_mut().inner.push(cls_clone);
555            ctx.globals().set("t", cls).unwrap();
556        });
557    }
558
559    #[derive(Clone, Copy)]
560    pub struct Vec3 {
561        x: f32,
562        y: f32,
563        z: f32,
564    }
565
566    impl Vec3 {
567        pub fn new(x: f32, y: f32, z: f32) -> Self {
568            Vec3 { x, y, z }
569        }
570
571        pub fn add(self, v: Vec3) -> Self {
572            Vec3 {
573                x: self.x + v.x,
574                y: self.y + v.y,
575                z: self.z + v.z,
576            }
577        }
578    }
579
580    impl<'js> Trace<'js> for Vec3 {
581        fn trace<'a>(&self, _tracer: Tracer<'a, 'js>) {}
582    }
583
584    impl<'js> FromJs<'js> for Vec3 {
585        fn from_js(ctx: &crate::Ctx<'js>, value: crate::Value<'js>) -> crate::Result<Self> {
586            Ok(*Class::<Vec3>::from_js(ctx, value)?.try_borrow()?)
587        }
588    }
589
590    impl<'js> IntoJs<'js> for Vec3 {
591        fn into_js(self, ctx: &crate::Ctx<'js>) -> crate::Result<crate::Value<'js>> {
592            Class::instance(ctx.clone(), self).into_js(ctx)
593        }
594    }
595
596    unsafe impl<'js> JsLifetime<'js> for Vec3 {
597        type Changed<'to> = Vec3;
598    }
599
600    impl<'js> JsClass<'js> for Vec3 {
601        const NAME: &'static str = "Vec3";
602
603        type Mutable = Writable;
604
605        fn prototype(ctx: &crate::Ctx<'js>) -> crate::Result<Option<crate::Object<'js>>> {
606            let proto = Object::new(ctx.clone())?;
607            let func = Function::new(ctx.clone(), |this: This<Vec3>, other: Vec3| this.add(other))?
608                .with_name("add")?;
609
610            proto.set("add", func)?;
611            Ok(Some(proto))
612        }
613
614        fn constructor(
615            ctx: &crate::Ctx<'js>,
616        ) -> crate::Result<Option<crate::value::Constructor<'js>>> {
617            let constr =
618                Constructor::new_class::<Vec3, _, _>(ctx.clone(), |x: f32, y: f32, z: f32| {
619                    Vec3::new(x, y, z)
620                })?;
621
622            Ok(Some(constr))
623        }
624    }
625
626    #[test]
627    fn constructor() {
628        test_with(|ctx| {
629            Class::<Vec3>::define(&ctx.globals()).unwrap();
630
631            let v = ctx
632                .eval::<Vec3, _>(
633                    r"
634                let a = new Vec3(1,2,3);
635                let b = new Vec3(4,2,8);
636                a.add(b)
637            ",
638                )
639                .catch(&ctx)
640                .unwrap();
641
642            approx::assert_abs_diff_eq!(v.x, 5.0);
643            approx::assert_abs_diff_eq!(v.y, 4.0);
644            approx::assert_abs_diff_eq!(v.z, 11.0);
645
646            let name: String = ctx.eval("new Vec3(1,2,3).constructor.name").unwrap();
647            assert_eq!(name, Vec3::NAME);
648        })
649    }
650
651    #[test]
652    fn extend_class() {
653        test_with(|ctx| {
654            Class::<Vec3>::define(&ctx.globals()).unwrap();
655
656            let v = ctx
657                .eval::<Vec3, _>(
658                    r"
659                    class Vec4 extends Vec3 {
660                        w = 0;
661                        constructor(x,y,z,w){
662                            super(x,y,z);
663                            this.w
664                        }
665                    }
666
667                    new Vec4(1,2,3,4);
668                ",
669                )
670                .catch(&ctx)
671                .unwrap();
672
673            approx::assert_abs_diff_eq!(v.x, 1.0);
674            approx::assert_abs_diff_eq!(v.y, 2.0);
675            approx::assert_abs_diff_eq!(v.z, 3.0);
676        })
677    }
678
679    #[test]
680    fn get_prototype() {
681        pub struct X;
682
683        impl<'js> Trace<'js> for X {
684            fn trace<'a>(&self, _tracer: Tracer<'a, 'js>) {}
685        }
686
687        unsafe impl<'js> JsLifetime<'js> for X {
688            type Changed<'to> = X;
689        }
690
691        impl<'js> JsClass<'js> for X {
692            const NAME: &'static str = "X";
693
694            type Mutable = Readable;
695
696            fn prototype(ctx: &crate::Ctx<'js>) -> crate::Result<Option<Object<'js>>> {
697                let object = Object::new(ctx.clone())?;
698                object.set("foo", "bar")?;
699                Ok(Some(object))
700            }
701
702            fn constructor(_ctx: &crate::Ctx<'js>) -> crate::Result<Option<Constructor<'js>>> {
703                Ok(None)
704            }
705        }
706
707        test_with(|ctx| {
708            let proto = Class::<X>::prototype(&ctx).unwrap().unwrap();
709            assert_eq!(proto.get::<_, String>("foo").unwrap(), "bar")
710        })
711    }
712
713    #[test]
714    fn instance_of_non_class_object_does_not_leave_pending_exception() {
715        test_with(|ctx| {
716            // A plain object isn't backed by any Rust class, so the underlying
717            // class-id check inside `instance_of` genuinely fails here (as
718            // opposed to checking one Rust class against another, where the
719            // class ids match and only a later, exception-free v-table
720            // comparison tells the classes apart).
721            let plain = Object::new(ctx.clone()).unwrap();
722
723            assert!(!plain.instance_of::<Vec3>());
724
725            // A negative `instance_of` result is an ordinary outcome, not an
726            // error: it must not leave a pending exception on the runtime for
727            // unrelated code to trip over later.
728            assert!(!ctx.has_exception());
729
730            // The context must still be perfectly usable afterwards.
731            let sum: i32 = ctx.eval("1 + 1").unwrap();
732            assert_eq!(sum, 2);
733        })
734    }
735
736    #[test]
737    fn generic_types() {
738        pub struct DebugPrinter<D: std::fmt::Debug> {
739            d: D,
740        }
741
742        impl<'js, D: std::fmt::Debug> Trace<'js> for DebugPrinter<D> {
743            fn trace<'a>(&self, _tracer: Tracer<'a, 'js>) {}
744        }
745
746        unsafe impl<'js, D: std::fmt::Debug + 'static> JsLifetime<'js> for DebugPrinter<D> {
747            type Changed<'to> = DebugPrinter<D>;
748        }
749
750        impl<'js, D: std::fmt::Debug + 'static> JsClass<'js> for DebugPrinter<D> {
751            const NAME: &'static str = "DebugPrinter";
752
753            type Mutable = Readable;
754
755            fn prototype(ctx: &crate::Ctx<'js>) -> crate::Result<Option<Object<'js>>> {
756                let object = Object::new(ctx.clone())?;
757                object.set(
758                    "to_debug_string",
759                    Function::new(
760                        ctx.clone(),
761                        |this: This<Class<DebugPrinter<D>>>| -> crate::Result<String> {
762                            Ok(format!("{:?}", this.0.borrow().d))
763                        },
764                    ),
765                )?;
766                Ok(Some(object))
767            }
768
769            fn constructor(_ctx: &crate::Ctx<'js>) -> crate::Result<Option<Constructor<'js>>> {
770                Ok(None)
771            }
772        }
773
774        test_with(|ctx| {
775            let a = Class::instance(ctx.clone(), DebugPrinter { d: 42usize });
776            let b = Class::instance(
777                ctx.clone(),
778                DebugPrinter {
779                    d: "foo".to_string(),
780                },
781            );
782
783            ctx.globals().set("a", a).unwrap();
784            ctx.globals().set("b", b).unwrap();
785
786            assert_eq!(
787                ctx.eval::<String, _>(r#" a.to_debug_string() "#)
788                    .catch(&ctx)
789                    .unwrap(),
790                "42"
791            );
792            assert_eq!(
793                ctx.eval::<String, _>(r#" b.to_debug_string() "#)
794                    .catch(&ctx)
795                    .unwrap(),
796                "\"foo\""
797            );
798
799            if ctx
800                .globals()
801                .get::<_, Class<DebugPrinter<String>>>("a")
802                .is_ok()
803            {
804                panic!("Conversion should fail")
805            }
806            if ctx
807                .globals()
808                .get::<_, Class<DebugPrinter<usize>>>("b")
809                .is_ok()
810            {
811                panic!("Conversion should fail")
812            }
813
814            ctx.globals()
815                .get::<_, Class<DebugPrinter<usize>>>("a")
816                .unwrap();
817            ctx.globals()
818                .get::<_, Class<DebugPrinter<String>>>("b")
819                .unwrap();
820        })
821    }
822
823    #[test]
824    fn exotic() {
825        pub struct ExoticIterator {
826            curr_state: Arc<AtomicI32>,
827        }
828
829        impl<'js> Trace<'js> for ExoticIterator {
830            fn trace<'a>(&self, _tracer: Tracer<'a, 'js>) {}
831        }
832
833        unsafe impl<'js> JsLifetime<'js> for ExoticIterator {
834            type Changed<'to> = ExoticIterator;
835        }
836
837        impl<'js> JsClass<'js> for ExoticIterator {
838            const NAME: &'static str = "ExoticIterator";
839
840            type Mutable = Readable;
841
842            const KIND: ClassKind = ClassKind::Exotic;
843
844            fn prototype(ctx: &crate::Ctx<'js>) -> crate::Result<Option<crate::Object<'js>>> {
845                Ok(Some(crate::Object::new(ctx.clone())?))
846            }
847
848            fn constructor(
849                _ctx: &crate::Ctx<'js>,
850            ) -> crate::Result<Option<crate::value::Constructor<'js>>> {
851                Ok(None)
852            }
853
854            fn exotic_get_property(
855                this: &crate::class::JsCell<'js, Self>,
856                ctx: &crate::Ctx<'js>,
857                atom: crate::Atom<'js>,
858                _receiver: crate::Value<'js>,
859            ) -> crate::Result<crate::Value<'js>> {
860                println!("Get property [iter]: {}", atom.to_string()?);
861                if atom.to_string()? == "next" {
862                    let state = this.borrow().curr_state.clone();
863                    Ok(Function::new(ctx.clone(), move |ctx: crate::Ctx<'js>| {
864                        // A really awful iterator thats implemented as a handwritten state machine
865                        //
866                        // Do not use this in production
867                        if state.load(Ordering::SeqCst) <= 1 {
868                            state.store(2, Ordering::SeqCst);
869
870                            let val = crate::Object::from_iter_js(
871                                &ctx,
872                                [
873                                    ("done", false.into_js(&ctx)?),
874                                    ("value", vec!["hello", "1292"].into_js(&ctx)?),
875                                ],
876                            )?
877                            .into_value();
878
879                            Ok::<crate::Value<'_>, crate::Error>(val)
880                        } else if state.load(Ordering::SeqCst) == 2 {
881                            state.fetch_add(1, Ordering::SeqCst);
882
883                            let val = crate::Object::from_iter_js(
884                                &ctx,
885                                [
886                                    ("done", false.into_js(&ctx)?),
887                                    (
888                                        "value",
889                                        vec!["i".into_js(&ctx)?, 43.into_js(&ctx)?]
890                                            .into_js(&ctx)?,
891                                    ),
892                                ],
893                            )?
894                            .into_value();
895
896                            Ok(val)
897                        } else {
898                            state.fetch_add(1, Ordering::SeqCst);
899
900                            let val = crate::Object::from_iter_js(
901                                &ctx,
902                                [
903                                    ("done", true.into_js(&ctx)?),
904                                    ("value", crate::Value::new_undefined(ctx.clone())),
905                                ],
906                            )?
907                            .into_value();
908
909                            Ok(val)
910                        }
911                    })?
912                    .into_value())
913                } else {
914                    Ok(crate::Value::new_undefined(ctx.clone()))
915                }
916            }
917
918            fn exotic_has_property(
919                this: &super::JsCell<'js, Self>,
920                _ctx: &crate::Ctx<'js>,
921                atom: crate::Atom<'js>,
922            ) -> crate::Result<bool> {
923                let _ = this;
924                Ok(atom.to_string()? == "next")
925            }
926        }
927
928        #[derive(Clone)]
929        pub struct Exotic {
930            pub i: i32,
931        }
932
933        impl<'js> Trace<'js> for Exotic {
934            fn trace<'a>(&self, _tracer: Tracer<'a, 'js>) {}
935        }
936
937        unsafe impl<'js> JsLifetime<'js> for Exotic {
938            type Changed<'to> = Exotic;
939        }
940
941        impl<'js> JsClass<'js> for Exotic {
942            const NAME: &'static str = "Exotic";
943
944            type Mutable = Writable;
945
946            const KIND: ClassKind = ClassKind::Exotic;
947
948            fn prototype(ctx: &crate::Ctx<'js>) -> crate::Result<Option<crate::Object<'js>>> {
949                Ok(Some(crate::Object::new(ctx.clone())?))
950            }
951
952            fn constructor(
953                _ctx: &crate::Ctx<'js>,
954            ) -> crate::Result<Option<crate::value::Constructor<'js>>> {
955                Ok(None)
956            }
957
958            fn exotic_get_property(
959                this: &crate::class::JsCell<'js, Self>,
960                ctx: &crate::Ctx<'js>,
961                atom: crate::Atom<'js>,
962                _receiver: crate::Value<'js>,
963            ) -> crate::Result<crate::Value<'js>> {
964                let symbol_iterator = crate::Atom::from_predefined(
965                    ctx.clone(),
966                    crate::atom::PredefinedAtom::SymbolIterator,
967                );
968                println!("Get property: {}", atom.to_string()?);
969                if atom.to_string()? == "hello" {
970                    assert!(this.borrow().i == 42);
971                    Ok("world".into_js(ctx)?)
972                } else if atom.to_string()? == "toString" {
973                    Ok(Function::new(ctx.clone(), || {
974                        let f = "class Exotic { [native code] }";
975                        Ok::<&'static str, crate::Error>(f)
976                    })?
977                    .into_value())
978                } else if atom == symbol_iterator {
979                    println!("Getting iterator");
980                    let exotic = Class::<ExoticIterator>::instance(
981                        ctx.clone(),
982                        ExoticIterator {
983                            curr_state: Arc::default(),
984                        },
985                    )?;
986                    println!("Returning ExoticIterator");
987                    Ok(Function::new(ctx.clone(), move || {
988                        Ok::<crate::Value<'_>, crate::Error>(exotic.clone().into_value())
989                    })?
990                    .into_value())
991                } else {
992                    Ok(crate::Value::new_null(ctx.clone()))
993                }
994            }
995
996            fn exotic_set_property(
997                this: &super::JsCell<'js, Self>,
998                ctx: &crate::Ctx<'js>,
999                atom: crate::Atom<'js>,
1000                _receiver: crate::Value<'js>,
1001                _value: crate::Value<'js>,
1002            ) -> crate::Result<bool> {
1003                let _ = this;
1004                if atom.to_string()? == "i" {
1005                    let Some(new_i) = _value.as_int() else {
1006                        let err_val = crate::String::from_str(ctx.clone(), "i must be an integer")?
1007                            .into_value();
1008                        return Err(ctx.throw(err_val));
1009                    };
1010                    this.borrow_mut().i = new_i;
1011                    return Ok(true);
1012                }
1013                let err_val =
1014                    crate::String::from_str(ctx.clone(), "Properties are read-only")?.into_value();
1015                Err(ctx.throw(err_val))
1016            }
1017
1018            fn exotic_has_property(
1019                this: &super::JsCell<'js, Self>,
1020                _ctx: &crate::Ctx<'js>,
1021                atom: crate::Atom<'js>,
1022            ) -> crate::Result<bool> {
1023                let _ = this;
1024                println!("Got atom: {}", atom.to_string()?);
1025                Ok(atom.to_string()? == "hello"
1026                    || atom.to_string()? == "i"
1027                    || atom.to_string()? == "toString")
1028            }
1029
1030            fn exotic_delete_property(
1031                _this: &super::JsCell<'js, Self>,
1032                ctx: &crate::Ctx<'js>,
1033                _atom: crate::Atom<'js>,
1034            ) -> crate::Result<bool> {
1035                let err_val = crate::String::from_str(ctx.clone(), "Properties cannot be deleted")?
1036                    .into_value();
1037                Err(ctx.throw(err_val))
1038            }
1039
1040            fn exotic_get_own_property(
1041                this: &super::JsCell<'js, Self>,
1042                ctx: &crate::Ctx<'js>,
1043                atom: crate::Atom<'js>,
1044            ) -> crate::Result<Option<super::PropertyDescriptor<'js>>> {
1045                let name = atom.to_string()?;
1046                if name == "hello" || name == "i" {
1047                    let value = if name == "hello" {
1048                        "world".into_js(ctx)?
1049                    } else {
1050                        this.borrow().i.into_js(ctx)?
1051                    };
1052                    Ok(Some(super::PropertyDescriptor::new_value(
1053                        value, true, true, false,
1054                    )))
1055                } else {
1056                    Ok(None)
1057                }
1058            }
1059
1060            fn exotic_get_own_property_names(
1061                _this: &super::JsCell<'js, Self>,
1062                ctx: &crate::Ctx<'js>,
1063            ) -> crate::Result<Vec<super::PropertyName<'js>>> {
1064                Ok(vec![
1065                    super::PropertyName {
1066                        atom: crate::Atom::from_str(ctx.clone(), "hello")?,
1067                        is_enumerable: true,
1068                    },
1069                    super::PropertyName {
1070                        atom: crate::Atom::from_str(ctx.clone(), "i")?,
1071                        is_enumerable: true,
1072                    },
1073                ])
1074            }
1075        }
1076
1077        test_with(|ctx| {
1078            let exotic = Class::<Exotic>::instance(ctx.clone(), Exotic { i: 0 }).unwrap();
1079            ctx.globals().set("exotic", exotic).unwrap();
1080            ctx.globals()
1081                .set(
1082                    "assert",
1083                    Function::new(
1084                        ctx.clone(),
1085                        |ctx: crate::Ctx<'_>, cond: bool, msg: String| {
1086                            if !cond {
1087                                let err_val =
1088                                    crate::String::from_str(ctx.clone(), &msg)?.into_value();
1089                                return Err(ctx.throw(err_val));
1090                            }
1091                            Ok(())
1092                        },
1093                    ),
1094                )
1095                .unwrap();
1096
1097            let v = ctx
1098                .eval::<String, _>(
1099                    r"
1100                if(exotic.foo !== null) {
1101                    throw new Error('foo should be null');
1102                }
1103                try {
1104                    exotic.foo = 1
1105                } catch(e) {
1106                    if (e?.toString() !== 'Properties are read-only') {
1107                        throw new Error('wrong error message: ' + e?.toString());
1108                    }
1109                }
1110                if (exotic.foo !== null) {
1111                    throw new Error('foo should be null');
1112                }
1113                exotic.i = 42;
1114                if (exotic.hello === 42) {
1115                    throw new Error('i should be 42');
1116                }
1117                assert(exotic?.toString() === 'class Exotic { [native code] }', `exotic.toString() should be 'class Exotic { [native code] }' but is ${exotic?.toString()}`);
1118                assert('i' in exotic, 'i should be in exotic');
1119                assert('hello' in exotic, 'hello should be in exotic');
1120                assert(!('foo' in exotic), 'foo should not be in exotic');
1121
1122                try {
1123                    delete exotic.i;
1124                } catch(e) {
1125                    if (e?.toString() !== 'Properties cannot be deleted') {
1126                        throw new Error('wrong error message: ' + e?.toString());
1127                    }
1128                }
1129
1130                let resp = []
1131                for (let [objKey, value] of exotic) {
1132                    if (objKey !== 'i' && objKey !== 'hello') {
1133                        throw new Error('only i and hello should be enumerable, got ' + objKey);
1134                    }
1135                    resp.push(`${objKey}:${value}`);
1136                }
1137
1138                assert(resp.toString() === 'hello:1292,i:43', `${resp.toString()} with length ${resp.length} should be [] as properties are not enumerable`);
1139
1140                // Test Object.getOwnPropertyNames() (uses get_own_property_names)
1141                let ownNames = Object.getOwnPropertyNames(exotic);
1142                assert(ownNames.length === 2, `getOwnPropertyNames should return 2, got ${ownNames.length}`);
1143                assert(ownNames.includes('hello'), 'getOwnPropertyNames should include hello');
1144                assert(ownNames.includes('i'), 'getOwnPropertyNames should include i');
1145
1146                // Test Object.keys() (uses get_own_property_names + get_own_property)
1147                let keys = Object.keys(exotic);
1148                assert(keys.length === 2, `Object.keys should return 2 keys, got ${keys.length}`);
1149
1150                // Test Object.getOwnPropertyDescriptor() (uses get_own_property)
1151                let desc = Object.getOwnPropertyDescriptor(exotic, 'hello');
1152                assert(desc !== undefined, 'descriptor for hello should exist');
1153                assert(desc.value === 'world', `descriptor value should be world, got ${desc.value}`);
1154                assert(desc.configurable === true, 'hello should be configurable');
1155                assert(desc.enumerable === true, 'hello should be enumerable');
1156                assert(desc.writable === false, 'hello should not be writable');
1157
1158                // Non-existent property returns undefined descriptor
1159                assert(Object.getOwnPropertyDescriptor(exotic, 'nonexistent') === undefined, 'nonexistent should be undefined');
1160
1161                exotic.hello
1162            ",
1163                )
1164                .catch(&ctx)
1165                .unwrap();
1166
1167            assert_eq!(v, "world");
1168        })
1169    }
1170}