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