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