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