1use 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
25pub trait JsClass<'js>: Trace<'js> + JsLifetime<'js> + Sized {
27 const NAME: &'static str;
29
30 const CALLABLE: bool = false;
32
33 type Mutable: Mutability;
37
38 fn prototype(ctx: &Ctx<'js>) -> Result<Option<Object<'js>>> {
40 Object::new(ctx.clone()).map(Some)
41 }
42
43 fn constructor(ctx: &Ctx<'js>) -> Result<Option<Constructor<'js>>>;
45
46 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#[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 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 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 pub fn prototype(ctx: &Ctx<'js>) -> Result<Option<Object<'js>>> {
149 unsafe { ctx.get_opaque().get_or_insert_prototype::<C>(ctx) }
150 }
151
152 pub fn create_constructor(ctx: &Ctx<'js>) -> Result<Option<Constructor<'js>>> {
154 C::constructor(ctx)
155 }
156
157 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 #[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 #[inline]
173 pub fn get_cell<'a>(&self) -> &'a JsCell<'js, C> {
174 &self.get_class_cell().data
175 }
176
177 #[inline]
185 pub fn borrow<'a>(&'a self) -> Borrow<'a, 'js, C> {
186 self.get_cell().borrow()
187 }
188
189 #[inline]
198 pub fn borrow_mut<'a>(&'a self) -> BorrowMut<'a, 'js, C> {
199 self.get_cell().borrow_mut()
200 }
201
202 #[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 #[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 #[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 #[inline]
243 pub fn into_inner(self) -> Object<'js> {
244 self.0
245 }
246
247 #[inline]
249 pub fn as_inner(&self) -> &Object<'js> {
250 &self.0
251 }
252
253 #[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 #[inline]
268 pub fn into_value(self) -> Value<'js> {
269 self.0.into_value()
270 }
271
272 #[inline]
274 pub fn from_object(object: &Object<'js>) -> Option<Self> {
275 object.into_class().ok()
276 }
277}
278
279impl<'js> Object<'js> {
280 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 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 core::ptr::eq(v_table, VTable::get::<C>()) {
306 return true;
307 }
308
309 v_table.is_of_class::<C>()
310 }
311
312 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 pub fn as_class<C: JsClass<'js>>(&self) -> Option<&Class<'js, C>> {
323 if self.instance_of::<C>() {
324 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]
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}