1use 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
24pub trait JsClass<'js>: Trace<'js> + JsLifetime<'js> + Sized {
26 const NAME: &'static str;
28
29 const CALLABLE: bool = false;
31
32 type Mutable: Mutability;
36
37 fn prototype(ctx: &Ctx<'js>) -> Result<Option<Object<'js>>> {
39 Object::new(ctx.clone()).map(Some)
40 }
41
42 fn constructor(ctx: &Ctx<'js>) -> Result<Option<Constructor<'js>>>;
44
45 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#[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 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 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 pub fn prototype(ctx: &Ctx<'js>) -> Result<Option<Object<'js>>> {
148 unsafe { ctx.get_opaque().get_or_insert_prototype::<C>(ctx) }
149 }
150
151 pub fn create_constructor(ctx: &Ctx<'js>) -> Result<Option<Constructor<'js>>> {
153 C::constructor(ctx)
154 }
155
156 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 #[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 #[inline]
172 pub fn get_cell<'a>(&self) -> &'a JsCell<'js, C> {
173 &self.get_class_cell().data
174 }
175
176 #[inline]
184 pub fn borrow<'a>(&'a self) -> Borrow<'a, 'js, C> {
185 self.get_cell().borrow()
186 }
187
188 #[inline]
197 pub fn borrow_mut<'a>(&'a self) -> BorrowMut<'a, 'js, C> {
198 self.get_cell().borrow_mut()
199 }
200
201 #[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 #[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 #[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 #[inline]
242 pub fn into_inner(self) -> Object<'js> {
243 self.0
244 }
245
246 #[inline]
248 pub fn as_inner(&self) -> &Object<'js> {
249 &self.0
250 }
251
252 #[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 #[inline]
267 pub fn into_value(self) -> Value<'js> {
268 self.0.into_value()
269 }
270
271 #[inline]
273 pub fn from_object(object: &Object<'js>) -> Option<Self> {
274 object.into_class().ok()
275 }
276}
277
278impl<'js> Object<'js> {
279 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 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 std::ptr::eq(v_table, VTable::get::<C>()) {
305 return true;
306 }
307
308 v_table.is_of_class::<C>()
309 }
310
311 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 pub fn as_class<C: JsClass<'js>>(&self) -> Option<&Class<'js, C>> {
322 if self.instance_of::<C>() {
323 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]
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}