rquickjs_core/
class.rs

1//! JavaScript classes defined from Rust.
2
3use crate::{
4    function::Params,
5    qjs::{self},
6    value::Constructor,
7    Ctx, Error, FromJs, IntoJs, JsLifetime, Object, Result, Value,
8};
9use alloc::boxed::Box;
10use core::{hash::Hash, marker::PhantomData, mem, ops::Deref, ptr::NonNull};
11
12mod cell;
13mod trace;
14
15pub(crate) mod ffi;
16
17pub use cell::{
18    Borrow, BorrowMut, JsCell, Mutability, OwnedBorrow, OwnedBorrowMut, Readable, Writable,
19};
20use ffi::{ClassCell, VTable};
21pub use trace::{Trace, Tracer};
22#[doc(hidden)]
23pub mod impl_;
24
25/// The trait which allows Rust types to be used from JavaScript.
26pub trait JsClass<'js>: Trace<'js> + JsLifetime<'js> + Sized {
27    /// The name the constructor has in JavaScript
28    const NAME: &'static str;
29
30    /// Is this class a function.
31    const CALLABLE: bool = false;
32
33    /// Can the type be mutated while a JavaScript value.
34    ///
35    /// This should either be [`Readable`] or [`Writable`].
36    type Mutable: Mutability;
37
38    /// Returns the class prototype,
39    fn prototype(ctx: &Ctx<'js>) -> Result<Option<Object<'js>>> {
40        Object::new(ctx.clone()).map(Some)
41    }
42
43    /// Returns a predefined constructor for this specific class type if there is one.
44    fn constructor(ctx: &Ctx<'js>) -> Result<Option<Constructor<'js>>>;
45
46    /// The function which will be called if [`Self::CALLABLE`] is true and an an object with this
47    /// class is called as if it is a function.
48    fn call<'a>(this: &JsCell<'js, Self>, params: Params<'a, 'js>) -> Result<Value<'js>> {
49        let _ = this;
50        Ok(Value::new_undefined(params.ctx().clone()))
51    }
52}
53
54/// A object which is instance of a Rust class.
55#[repr(transparent)]
56pub struct Class<'js, C: JsClass<'js>>(pub(crate) Object<'js>, PhantomData<C>);
57
58impl<'js, C: JsClass<'js>> Clone for Class<'js, C> {
59    fn clone(&self) -> Self {
60        Class(self.0.clone(), PhantomData)
61    }
62}
63
64impl<'js, C: JsClass<'js>> PartialEq for Class<'js, C> {
65    fn eq(&self, other: &Self) -> bool {
66        self.0 == other.0
67    }
68}
69
70impl<'js, C: JsClass<'js>> Eq for Class<'js, C> {}
71
72impl<'js, C: JsClass<'js>> Hash for Class<'js, C> {
73    fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
74        self.0.hash(state)
75    }
76}
77
78unsafe impl<'js, C> JsLifetime<'js> for Class<'js, C>
79where
80    C: JsClass<'js> + JsLifetime<'js>,
81    for<'to> C::Changed<'to>: JsClass<'to>,
82{
83    type Changed<'to> = Class<'to, C::Changed<'to>>;
84}
85
86impl<'js, C: JsClass<'js>> Deref for Class<'js, C> {
87    type Target = Object<'js>;
88
89    fn deref(&self) -> &Self::Target {
90        &self.0
91    }
92}
93
94impl<'js, C: JsClass<'js>> Class<'js, C> {
95    /// Create a class from a Rust object.
96    pub fn instance(ctx: Ctx<'js>, value: C) -> Result<Class<'js, C>> {
97        let id = unsafe {
98            if C::CALLABLE {
99                ctx.get_opaque().get_callable_id()
100            } else {
101                ctx.get_opaque().get_class_id()
102            }
103        };
104
105        let prototype = Self::prototype(&ctx)?;
106
107        let prototype = prototype.map(|x| x.as_js_value()).unwrap_or(qjs::JS_NULL);
108        let val = unsafe {
109            ctx.handle_exception(qjs::JS_NewObjectProtoClass(ctx.as_ptr(), prototype, id))?
110        };
111
112        let ptr = Box::into_raw(Box::new(ClassCell::new(value)));
113        unsafe { qjs::JS_SetOpaque(val, ptr.cast()) };
114        Ok(Self(
115            unsafe { Object::from_js_value(ctx, val) },
116            PhantomData,
117        ))
118    }
119
120    /// Create a class from a Rust object with a given prototype.
121    pub fn instance_proto(value: C, proto: Object<'js>) -> Result<Class<'js, C>> {
122        let id = unsafe {
123            if C::CALLABLE {
124                proto.ctx().get_opaque().get_callable_id()
125            } else {
126                proto.ctx().get_opaque().get_class_id()
127            }
128        };
129
130        let val = unsafe {
131            proto.ctx.handle_exception(qjs::JS_NewObjectProtoClass(
132                proto.ctx().as_ptr(),
133                proto.0.as_js_value(),
134                id,
135            ))?
136        };
137        let ptr = Box::into_raw(Box::new(ClassCell::new(value)));
138        unsafe { qjs::JS_SetOpaque(val, ptr.cast()) };
139        Ok(Self(
140            unsafe { Object::from_js_value(proto.ctx.clone(), val) },
141            PhantomData,
142        ))
143    }
144
145    /// Returns the prototype for the class.
146    ///
147    /// Returns `None` if the class is not yet registered or if the class doesn't have a prototype.
148    pub fn prototype(ctx: &Ctx<'js>) -> Result<Option<Object<'js>>> {
149        unsafe { ctx.get_opaque().get_or_insert_prototype::<C>(ctx) }
150    }
151
152    /// Create a constructor for the current class using its definition.
153    pub fn create_constructor(ctx: &Ctx<'js>) -> Result<Option<Constructor<'js>>> {
154        C::constructor(ctx)
155    }
156
157    /// Defines the predefined constructor of this class, if there is one, onto the given object.
158    pub fn define(object: &Object<'js>) -> Result<()> {
159        if let Some(constructor) = Self::create_constructor(object.ctx())? {
160            object.set(C::NAME, constructor)?;
161        }
162        Ok(())
163    }
164
165    /// Returns a reference to the underlying object contained in a cell.
166    #[inline]
167    pub(crate) fn get_class_cell<'a>(&self) -> &'a ClassCell<JsCell<'js, C>> {
168        unsafe { self.get_class_ptr().as_ref() }
169    }
170
171    /// Returns a reference to the underlying object contained in a cell.
172    #[inline]
173    pub fn get_cell<'a>(&self) -> &'a JsCell<'js, C> {
174        &self.get_class_cell().data
175    }
176
177    /// Borrow the Rust class type.
178    ///
179    /// JavaScript classes behave similar to [`Rc`](std::rc::Rc) in Rust, you can essentially think
180    /// of a class object as a `Rc<RefCell<C>>` and with similar borrowing functionality.
181    ///
182    /// # Panic
183    /// This function panics if the class is already borrowed mutably.
184    #[inline]
185    pub fn borrow<'a>(&'a self) -> Borrow<'a, 'js, C> {
186        self.get_cell().borrow()
187    }
188
189    /// Borrow the Rust class type mutably.
190    ///
191    /// JavaScript classes behave similar to [`Rc`](std::rc::Rc) in Rust, you can essentially think
192    /// of a class object as a `Rc<RefCell<C>>` and with similar borrowing functionality.
193    ///
194    /// # Panic
195    /// This function panics if the class is already borrowed mutably or immutably, or the Class
196    /// can't be borrowed mutably.
197    #[inline]
198    pub fn borrow_mut<'a>(&'a self) -> BorrowMut<'a, 'js, C> {
199        self.get_cell().borrow_mut()
200    }
201
202    /// Try to borrow the Rust class type.
203    ///
204    /// JavaScript classes behave similar to [`Rc`](std::rc::Rc) in Rust, you can essentially think
205    /// of a class object as a `Rc<RefCell<C>>` and with similar borrowing functionality.
206    ///
207    /// This returns an error when the class is already borrowed mutably.
208    #[inline]
209    pub fn try_borrow<'a>(&'a self) -> Result<Borrow<'a, 'js, C>> {
210        self.get_cell().try_borrow().map_err(Error::ClassBorrow)
211    }
212
213    /// Try to borrow the Rust class type mutably.
214    ///
215    /// JavaScript classes behave similar to [`Rc`](std::rc::Rc) in Rust, you can essentially think
216    /// of a class object as a `Rc<RefCell<C>>` and with similar borrowing functionality.
217    ///
218    /// This returns an error when the class is already borrowed mutably, immutably or the class
219    /// can't be borrowed mutably.
220    #[inline]
221    pub fn try_borrow_mut<'a>(&'a self) -> Result<BorrowMut<'a, 'js, C>> {
222        self.get_cell().try_borrow_mut().map_err(Error::ClassBorrow)
223    }
224
225    /// returns a pointer to the class object.
226    #[inline]
227    pub(crate) fn get_class_ptr(&self) -> NonNull<ClassCell<JsCell<'js, C>>> {
228        let id = unsafe {
229            if C::CALLABLE {
230                self.ctx.get_opaque().get_callable_id()
231            } else {
232                self.ctx.get_opaque().get_class_id()
233            }
234        };
235
236        let ptr = unsafe { qjs::JS_GetOpaque2(self.0.ctx.as_ptr(), self.0 .0.as_js_value(), id) };
237
238        NonNull::new(ptr.cast()).expect("invalid class object, object didn't have opaque value")
239    }
240
241    /// Turns the class back into a generic object.
242    #[inline]
243    pub fn into_inner(self) -> Object<'js> {
244        self.0
245    }
246
247    /// Turns the class back into a generic object.
248    #[inline]
249    pub fn as_inner(&self) -> &Object<'js> {
250        &self.0
251    }
252
253    /// Convert from value.
254    #[inline]
255    pub fn from_value(value: &Value<'js>) -> Result<Self> {
256        if let Some(cls) = value.as_object().and_then(Self::from_object) {
257            return Ok(cls);
258        }
259        Err(Error::FromJs {
260            from: value.type_name(),
261            to: C::NAME,
262            message: None,
263        })
264    }
265
266    /// Turn the class into a value.
267    #[inline]
268    pub fn into_value(self) -> Value<'js> {
269        self.0.into_value()
270    }
271
272    /// Converts a generic object into a class if the object is of the right class.
273    #[inline]
274    pub fn from_object(object: &Object<'js>) -> Option<Self> {
275        object.into_class().ok()
276    }
277}
278
279impl<'js> Object<'js> {
280    /// Returns if the object is of a certain Rust class.
281    pub fn instance_of<C: JsClass<'js>>(&self) -> bool {
282        let id = unsafe {
283            if C::CALLABLE {
284                self.ctx.get_opaque().get_callable_id()
285            } else {
286                self.ctx.get_opaque().get_class_id()
287            }
288        };
289
290        // This checks if the class is of the right class id.
291        let Some(x) = NonNull::new(unsafe {
292            qjs::JS_GetOpaque2(self.0.ctx.as_ptr(), self.0.as_js_value(), id)
293        }) else {
294            return false;
295        };
296
297        let v_table = unsafe { x.cast::<ClassCell<()>>().as_ref().v_table };
298
299        // If the pointer is equal it must be of the right type, as the inclusion of a call to
300        // generate a TypeId means that each type must have a unique v table.
301        // however if it is not equal then it can still be the right type if the v_table is
302        // duplicated, which is possible when compilation with multiple code-gen units.
303        //
304        // Doing check avoids a lookup and an dynamic function call in some cases.
305        if core::ptr::eq(v_table, VTable::get::<C>()) {
306            return true;
307        }
308
309        v_table.is_of_class::<C>()
310    }
311
312    /// Turn the object into the class if it is an instance of that class.
313    pub fn into_class<C: JsClass<'js>>(&self) -> core::result::Result<Class<'js, C>, &Self> {
314        if self.instance_of::<C>() {
315            Ok(Class(self.clone(), PhantomData))
316        } else {
317            Err(self)
318        }
319    }
320
321    /// Turn the object into the class if it is an instance of that class.
322    pub fn as_class<C: JsClass<'js>>(&self) -> Option<&Class<'js, C>> {
323        if self.instance_of::<C>() {
324            // SAFETY:
325            // Safe because class is a transparent wrapper
326            unsafe { Some(mem::transmute::<&Object<'js>, &Class<'js, C>>(self)) }
327        } else {
328            None
329        }
330    }
331}
332
333impl<'js, C: JsClass<'js>> FromJs<'js> for Class<'js, C> {
334    fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> Result<Self> {
335        Self::from_value(&value)
336    }
337}
338
339impl<'js, C: JsClass<'js>> IntoJs<'js> for Class<'js, C> {
340    fn into_js(self, _ctx: &Ctx<'js>) -> Result<Value<'js>> {
341        Ok(self.0 .0)
342    }
343}
344
345#[cfg(test)]
346mod test {
347    use std::sync::{
348        atomic::{AtomicBool, Ordering},
349        Arc,
350    };
351
352    use crate::{
353        class::{JsClass, Readable, Trace, Tracer, Writable},
354        function::This,
355        test_with,
356        value::Constructor,
357        CatchResultExt, Class, Context, FromJs, Function, IntoJs, JsLifetime, Object, Runtime,
358    };
359
360    /// Test circular references.
361    #[test]
362    fn trace() {
363        pub struct Container<'js> {
364            inner: Vec<Class<'js, Container<'js>>>,
365            test: Arc<AtomicBool>,
366        }
367
368        impl<'js> Drop for Container<'js> {
369            fn drop(&mut self) {
370                self.test.store(true, Ordering::SeqCst);
371            }
372        }
373
374        impl<'js> Trace<'js> for Container<'js> {
375            fn trace<'a>(&self, tracer: Tracer<'a, 'js>) {
376                self.inner.iter().for_each(|x| x.trace(tracer))
377            }
378        }
379
380        unsafe impl<'js> JsLifetime<'js> for Container<'js> {
381            type Changed<'to> = Container<'to>;
382        }
383
384        impl<'js> JsClass<'js> for Container<'js> {
385            const NAME: &'static str = "Container";
386
387            type Mutable = Writable;
388
389            fn prototype(ctx: &crate::Ctx<'js>) -> crate::Result<Option<crate::Object<'js>>> {
390                Ok(Some(Object::new(ctx.clone())?))
391            }
392
393            fn constructor(
394                _ctx: &crate::Ctx<'js>,
395            ) -> crate::Result<Option<crate::value::Constructor<'js>>> {
396                Ok(None)
397            }
398        }
399
400        let rt = Runtime::new().unwrap();
401        let ctx = Context::full(&rt).unwrap();
402
403        let drop_test = Arc::new(AtomicBool::new(false));
404
405        ctx.with(|ctx| {
406            let cls = Class::instance(
407                ctx.clone(),
408                Container {
409                    inner: Vec::new(),
410                    test: drop_test.clone(),
411                },
412            )
413            .unwrap();
414
415            assert!(cls.instance_of::<Container>());
416
417            let cls_clone = cls.clone();
418            cls.borrow_mut().inner.push(cls_clone);
419        });
420        rt.run_gc();
421        assert!(drop_test.load(Ordering::SeqCst));
422        ctx.with(|ctx| {
423            let cls = Class::instance(
424                ctx.clone(),
425                Container {
426                    inner: Vec::new(),
427                    test: drop_test.clone(),
428                },
429            )
430            .unwrap();
431            let cls_clone = cls.clone();
432            cls.borrow_mut().inner.push(cls_clone);
433            ctx.globals().set("t", cls).unwrap();
434        });
435    }
436
437    #[derive(Clone, Copy)]
438    pub struct Vec3 {
439        x: f32,
440        y: f32,
441        z: f32,
442    }
443
444    impl Vec3 {
445        pub fn new(x: f32, y: f32, z: f32) -> Self {
446            Vec3 { x, y, z }
447        }
448
449        pub fn add(self, v: Vec3) -> Self {
450            Vec3 {
451                x: self.x + v.x,
452                y: self.y + v.y,
453                z: self.z + v.z,
454            }
455        }
456    }
457
458    impl<'js> Trace<'js> for Vec3 {
459        fn trace<'a>(&self, _tracer: Tracer<'a, 'js>) {}
460    }
461
462    impl<'js> FromJs<'js> for Vec3 {
463        fn from_js(ctx: &crate::Ctx<'js>, value: crate::Value<'js>) -> crate::Result<Self> {
464            Ok(*Class::<Vec3>::from_js(ctx, value)?.try_borrow()?)
465        }
466    }
467
468    impl<'js> IntoJs<'js> for Vec3 {
469        fn into_js(self, ctx: &crate::Ctx<'js>) -> crate::Result<crate::Value<'js>> {
470            Class::instance(ctx.clone(), self).into_js(ctx)
471        }
472    }
473
474    unsafe impl<'js> JsLifetime<'js> for Vec3 {
475        type Changed<'to> = Vec3;
476    }
477
478    impl<'js> JsClass<'js> for Vec3 {
479        const NAME: &'static str = "Vec3";
480
481        type Mutable = Writable;
482
483        fn prototype(ctx: &crate::Ctx<'js>) -> crate::Result<Option<crate::Object<'js>>> {
484            let proto = Object::new(ctx.clone())?;
485            let func = Function::new(ctx.clone(), |this: This<Vec3>, other: Vec3| this.add(other))?
486                .with_name("add")?;
487
488            proto.set("add", func)?;
489            Ok(Some(proto))
490        }
491
492        fn constructor(
493            ctx: &crate::Ctx<'js>,
494        ) -> crate::Result<Option<crate::value::Constructor<'js>>> {
495            let constr =
496                Constructor::new_class::<Vec3, _, _>(ctx.clone(), |x: f32, y: f32, z: f32| {
497                    Vec3::new(x, y, z)
498                })?;
499
500            Ok(Some(constr))
501        }
502    }
503
504    #[test]
505    fn constructor() {
506        test_with(|ctx| {
507            Class::<Vec3>::define(&ctx.globals()).unwrap();
508
509            let v = ctx
510                .eval::<Vec3, _>(
511                    r"
512                let a = new Vec3(1,2,3);
513                let b = new Vec3(4,2,8);
514                a.add(b)
515            ",
516                )
517                .catch(&ctx)
518                .unwrap();
519
520            approx::assert_abs_diff_eq!(v.x, 5.0);
521            approx::assert_abs_diff_eq!(v.y, 4.0);
522            approx::assert_abs_diff_eq!(v.z, 11.0);
523
524            let name: String = ctx.eval("new Vec3(1,2,3).constructor.name").unwrap();
525            assert_eq!(name, Vec3::NAME);
526        })
527    }
528
529    #[test]
530    fn extend_class() {
531        test_with(|ctx| {
532            Class::<Vec3>::define(&ctx.globals()).unwrap();
533
534            let v = ctx
535                .eval::<Vec3, _>(
536                    r"
537                    class Vec4 extends Vec3 {
538                        w = 0;
539                        constructor(x,y,z,w){
540                            super(x,y,z);
541                            this.w
542                        }
543                    }
544
545                    new Vec4(1,2,3,4);
546                ",
547                )
548                .catch(&ctx)
549                .unwrap();
550
551            approx::assert_abs_diff_eq!(v.x, 1.0);
552            approx::assert_abs_diff_eq!(v.y, 2.0);
553            approx::assert_abs_diff_eq!(v.z, 3.0);
554        })
555    }
556
557    #[test]
558    fn get_prototype() {
559        pub struct X;
560
561        impl<'js> Trace<'js> for X {
562            fn trace<'a>(&self, _tracer: Tracer<'a, 'js>) {}
563        }
564
565        unsafe impl<'js> JsLifetime<'js> for X {
566            type Changed<'to> = X;
567        }
568
569        impl<'js> JsClass<'js> for X {
570            const NAME: &'static str = "X";
571
572            type Mutable = Readable;
573
574            fn prototype(ctx: &crate::Ctx<'js>) -> crate::Result<Option<Object<'js>>> {
575                let object = Object::new(ctx.clone())?;
576                object.set("foo", "bar")?;
577                Ok(Some(object))
578            }
579
580            fn constructor(_ctx: &crate::Ctx<'js>) -> crate::Result<Option<Constructor<'js>>> {
581                Ok(None)
582            }
583        }
584
585        test_with(|ctx| {
586            let proto = Class::<X>::prototype(&ctx).unwrap().unwrap();
587            assert_eq!(proto.get::<_, String>("foo").unwrap(), "bar")
588        })
589    }
590
591    #[test]
592    fn generic_types() {
593        pub struct DebugPrinter<D: std::fmt::Debug> {
594            d: D,
595        }
596
597        impl<'js, D: std::fmt::Debug> Trace<'js> for DebugPrinter<D> {
598            fn trace<'a>(&self, _tracer: Tracer<'a, 'js>) {}
599        }
600
601        unsafe impl<'js, D: std::fmt::Debug + 'static> JsLifetime<'js> for DebugPrinter<D> {
602            type Changed<'to> = DebugPrinter<D>;
603        }
604
605        impl<'js, D: std::fmt::Debug + 'static> JsClass<'js> for DebugPrinter<D> {
606            const NAME: &'static str = "DebugPrinter";
607
608            type Mutable = Readable;
609
610            fn prototype(ctx: &crate::Ctx<'js>) -> crate::Result<Option<Object<'js>>> {
611                let object = Object::new(ctx.clone())?;
612                object.set(
613                    "to_debug_string",
614                    Function::new(
615                        ctx.clone(),
616                        |this: This<Class<DebugPrinter<D>>>| -> crate::Result<String> {
617                            Ok(format!("{:?}", &this.0.borrow().d))
618                        },
619                    ),
620                )?;
621                Ok(Some(object))
622            }
623
624            fn constructor(_ctx: &crate::Ctx<'js>) -> crate::Result<Option<Constructor<'js>>> {
625                Ok(None)
626            }
627        }
628
629        test_with(|ctx| {
630            let a = Class::instance(ctx.clone(), DebugPrinter { d: 42usize });
631            let b = Class::instance(
632                ctx.clone(),
633                DebugPrinter {
634                    d: "foo".to_string(),
635                },
636            );
637
638            ctx.globals().set("a", a).unwrap();
639            ctx.globals().set("b", b).unwrap();
640
641            assert_eq!(
642                ctx.eval::<String, _>(r#" a.to_debug_string() "#)
643                    .catch(&ctx)
644                    .unwrap(),
645                "42"
646            );
647            assert_eq!(
648                ctx.eval::<String, _>(r#" b.to_debug_string() "#)
649                    .catch(&ctx)
650                    .unwrap(),
651                "\"foo\""
652            );
653
654            if ctx
655                .globals()
656                .get::<_, Class<DebugPrinter<String>>>("a")
657                .is_ok()
658            {
659                panic!("Conversion should fail")
660            }
661            if ctx
662                .globals()
663                .get::<_, Class<DebugPrinter<usize>>>("b")
664                .is_ok()
665            {
666                panic!("Conversion should fail")
667            }
668
669            ctx.globals()
670                .get::<_, Class<DebugPrinter<usize>>>("a")
671                .unwrap();
672            ctx.globals()
673                .get::<_, Class<DebugPrinter<String>>>("b")
674                .unwrap();
675        })
676    }
677}