From 6f10873bc252a0fe660d4c2ffb479f24a4fb42a7 Mon Sep 17 00:00:00 2001 From: Your Name Date: Fri, 29 Aug 2025 21:49:14 +0530 Subject: [PATCH 01/14] remove Send bound in non-parallel mode --- core/src/context/async.rs | 13 +++++++++---- core/src/context/async/future.rs | 12 ++++++++++-- 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/core/src/context/async.rs b/core/src/context/async.rs index 0fda4aef2..1c84fa5e2 100644 --- a/core/src/context/async.rs +++ b/core/src/context/async.rs @@ -3,9 +3,8 @@ use super::{ owner::{ContextOwner, DropContext}, ContextBuilder, Intrinsic, }; -use crate::{markers::ParallelSend, qjs, runtime::AsyncRuntime, Ctx, Error, Result}; -use alloc::boxed::Box; -use core::{future::Future, mem, pin::Pin, ptr::NonNull}; +use crate::{context::r#async::future::CallbackFuture, markers::ParallelSend, qjs, runtime::AsyncRuntime, Ctx, Error, Result}; +use core::{mem, ptr::NonNull}; mod future; @@ -81,9 +80,15 @@ macro_rules! async_with{ /// rquickjs objects are send so the future will never be send. /// Since we acquire a lock before running the future and nothing can escape the closure /// and future it is safe to recast the future as send. + #[cfg(not(feature = "parallel"))] + unsafe fn uplift<'a,'b,R>(f: core::pin::Pin<$crate::alloc::boxed::Box + 'a>>) -> core::pin::Pin<$crate::alloc::boxed::Box + 'b>>{ + core::mem::transmute(f) + } + #[cfg(feature = "parallel")] unsafe fn uplift<'a,'b,R>(f: core::pin::Pin<$crate::alloc::boxed::Box + 'a>>) -> core::pin::Pin<$crate::alloc::boxed::Box + 'b + Send>>{ core::mem::transmute(f) } + unsafe{ uplift(fut) } }) }; @@ -202,7 +207,7 @@ impl AsyncContext { /// future. pub fn async_with(&self, f: F) -> WithFuture where - F: for<'js> FnOnce(Ctx<'js>) -> Pin + 'js + Send>> + F: for<'js> FnOnce(Ctx<'js>) -> CallbackFuture<'js, R> + ParallelSend, R: ParallelSend, { diff --git a/core/src/context/async/future.rs b/core/src/context/async/future.rs index 7b5706f9e..01fa2576c 100644 --- a/core/src/context/async/future.rs +++ b/core/src/context/async/future.rs @@ -38,14 +38,22 @@ enum WithFutureState<'a, F, R> { closure: F, }, FutureCreated { + #[cfg(not(feature = "parallel"))] + future: Pin + 'a>>, + #[cfg(feature = "parallel")] future: Pin + 'a + Send>>, }, Done, } +#[cfg(not(feature = "parallel"))] +pub type CallbackFuture<'js, R> = Pin + 'js>>; +#[cfg(feature = "parallel")] +pub type CallbackFuture<'js, R> = Pin + 'js + Send>>; + impl<'a, F, R> WithFuture<'a, F, R> where - F: for<'js> FnOnce(Ctx<'js>) -> Pin + 'js + Send>> + ParallelSend, + F: for<'js> FnOnce(Ctx<'js>) -> CallbackFuture<'js, R> + ParallelSend, R: ParallelSend, { pub fn new(context: &'a AsyncContext, f: F) -> Self { @@ -59,7 +67,7 @@ where impl<'a, F, R> Future for WithFuture<'a, F, R> where - F: for<'js> FnOnce(Ctx<'js>) -> Pin + 'js + Send>> + ParallelSend, + F: for<'js> FnOnce(Ctx<'js>) -> CallbackFuture<'js, R> + ParallelSend, R: ParallelSend + 'static, { type Output = R; From 198bcba84875e22ac8ebcc00cd4875c042d414b7 Mon Sep 17 00:00:00 2001 From: Your Name Date: Sat, 30 Aug 2025 20:09:55 +0530 Subject: [PATCH 02/14] add support for exotic objects --- core/src/class.rs | 113 +++++++++++++++++++++++++++++++++++-- core/src/class/ffi.rs | 67 +++++++++++++++++++++- core/src/runtime/opaque.rs | 42 ++++++++++++-- 3 files changed, 213 insertions(+), 9 deletions(-) diff --git a/core/src/class.rs b/core/src/class.rs index 93dd73ba8..c706e7c96 100644 --- a/core/src/class.rs +++ b/core/src/class.rs @@ -1,10 +1,7 @@ //! JavaScript classes defined from Rust. use crate::{ - function::Params, - qjs::{self}, - value::Constructor, - Ctx, Error, FromJs, IntoJs, JsLifetime, Object, Result, Value, + function::Params, qjs::{self}, value::Constructor, Atom, Ctx, Error, FromJs, IntoJs, JsLifetime, Object, Result, Value }; use alloc::boxed::Box; use core::{hash::Hash, marker::PhantomData, mem, ops::Deref, ptr::NonNull}; @@ -30,6 +27,9 @@ pub trait JsClass<'js>: Trace<'js> + JsLifetime<'js> + Sized { /// Is this class a function. const CALLABLE: bool = false; + /// Is this class exotic (e.g. will exotic_* methods be called with it) + const EXOTIC: bool = false; + /// Can the type be mutated while a JavaScript value. /// /// This should either be [`Readable`] or [`Writable`]. @@ -49,6 +49,36 @@ pub trait JsClass<'js>: Trace<'js> + JsLifetime<'js> + Sized { let _ = this; Ok(Value::new_undefined(params.ctx().clone())) } + + /// The function which will be called if a get property is performed on an object with this class + fn exotic_get_property<'a>(this: &JsCell<'js, Self>, ctx: &Ctx<'js>, _atom: Atom<'js>, _obj: Value<'js>, _receiver: Value<'js>) -> Result> { + let _ = this; + Ok(Value::new_undefined(ctx.clone())) + } + + /// The function which will be called if a set property is performed on an object with this class + /// + /// Not yet implemented. + fn exotic_set_property<'a>(this: &JsCell<'js, Self>, _params: Params<'a, 'js>) -> Result { + let _ = this; + Ok(false) + } + + /// The function which will be called if a delete property is performed on an object with this class + /// + /// Not yet implemented. + fn exotic_delete_property<'a>(this: &JsCell<'js, Self>, _params: Params<'a, 'js>) -> Result { + let _ = this; + Ok(false) + } + + /// The function which will be called if has property or similar is called on an object with this class + /// + /// Not yet implemented. + fn exotic_has_property<'a>(this: &JsCell<'js, Self>, _params: Params<'a, 'js>) -> Result { + let _ = this; + Ok(false) + } } /// A object which is instance of a Rust class. @@ -97,6 +127,8 @@ impl<'js, C: JsClass<'js>> Class<'js, C> { let id = unsafe { if C::CALLABLE { ctx.get_opaque().get_callable_id() + } else if C::EXOTIC { + ctx.get_opaque().get_exotic_id() } else { ctx.get_opaque().get_class_id() } @@ -122,6 +154,8 @@ impl<'js, C: JsClass<'js>> Class<'js, C> { let id = unsafe { if C::CALLABLE { proto.ctx().get_opaque().get_callable_id() + } else if C::EXOTIC { + proto.ctx().get_opaque().get_exotic_id() } else { proto.ctx().get_opaque().get_class_id() } @@ -228,6 +262,8 @@ impl<'js, C: JsClass<'js>> Class<'js, C> { let id = unsafe { if C::CALLABLE { self.ctx.get_opaque().get_callable_id() + } else if C::EXOTIC { + self.ctx.get_opaque().get_exotic_id() } else { self.ctx.get_opaque().get_class_id() } @@ -282,6 +318,8 @@ impl<'js> Object<'js> { let id = unsafe { if C::CALLABLE { self.ctx.get_opaque().get_callable_id() + } else if C::EXOTIC { + self.ctx.get_opaque().get_exotic_id() } else { self.ctx.get_opaque().get_class_id() } @@ -674,4 +712,71 @@ mod test { .unwrap(); }) } + + #[test] + fn exotic() { + pub struct Exotic { + pub i: i32, + } + + impl<'js> Trace<'js> for Exotic { + fn trace<'a>(&self, _tracer: Tracer<'a, 'js>) {} + } + + unsafe impl<'js> JsLifetime<'js> for Exotic { + type Changed<'to> = Exotic; + } + + impl<'js> JsClass<'js> for Exotic { + const NAME: &'static str = "Exotic"; + + type Mutable = Writable; + + const EXOTIC: bool = true; + + fn prototype(ctx: &crate::Ctx<'js>) -> crate::Result>> { + Ok(Some(crate::Object::new(ctx.clone())?)) + } + + fn constructor( + _ctx: &crate::Ctx<'js>, + ) -> crate::Result>> { + Ok(None) + } + + fn exotic_get_property<'a>( + this: &crate::class::JsCell<'js, Self>, + ctx: &crate::Ctx<'js>, + atom: crate::Atom<'js>, + _obj: crate::Value<'js>, + _receiver: crate::Value<'js>, + ) -> crate::Result> { + if atom.to_string()? == "hello" { + assert!(this.borrow().i == 0); + Ok("world".into_js(ctx)?) + } else { + Ok(crate::Value::new_null(ctx.clone())) + } + } + } + + test_with(|ctx| { + let exotic = Class::::instance(ctx.clone(), Exotic { i: 0 }).unwrap(); + ctx.globals().set("exotic", exotic).unwrap(); + + let v = ctx + .eval::( + r" + if(exotic.foo !== null) { + throw new Error('foo should be null'); + } + exotic.hello + ", + ) + .catch(&ctx) + .unwrap(); + + assert_eq!(v, "world"); + }) + } } diff --git a/core/src/class/ffi.rs b/core/src/class/ffi.rs index 1c977bb1e..f95f434db 100644 --- a/core/src/class/ffi.rs +++ b/core/src/class/ffi.rs @@ -1,5 +1,5 @@ use super::{JsClass, Tracer}; -use crate::{class::JsCell, function::Params, qjs, runtime::opaque::Opaque, Value}; +use crate::{class::JsCell, function::Params, qjs, runtime::opaque::Opaque, Atom, Ctx, Value}; use alloc::boxed::Box; use core::{any::TypeId, panic::AssertUnwindSafe, ptr::NonNull}; @@ -11,6 +11,14 @@ pub(crate) unsafe extern "C" fn class_finalizer(rt: *mut qjs::JSRuntime, val: qj (ptr.as_ref().v_table.finalizer)(ptr); } +/// FFI finalizer, destroying the object once it is delete by the Gc. +pub(crate) unsafe extern "C" fn exotic_class_finalizer(rt: *mut qjs::JSRuntime, val: qjs::JSValue) { + let class_id = Opaque::from_runtime_ptr(rt).get_exotic_id(); + let ptr = qjs::JS_GetOpaque(val, class_id); + let ptr = NonNull::new(ptr).unwrap().cast::>(); + (ptr.as_ref().v_table.finalizer)(ptr); +} + /// FFI tracing function for non callable classes. pub(crate) unsafe extern "C" fn class_trace( rt: *mut qjs::JSRuntime, @@ -24,6 +32,19 @@ pub(crate) unsafe extern "C" fn class_trace( (ptr.as_ref().v_table.trace)(ptr, tracer) } +/// FFI tracing function for non callable exotic classes. +pub(crate) unsafe extern "C" fn exotic_class_trace( + rt: *mut qjs::JSRuntime, + val: qjs::JSValue, + mark_func: qjs::JS_MarkFunc, +) { + let class_id = Opaque::from_runtime_ptr(rt).get_exotic_id(); + let ptr = qjs::JS_GetOpaque(val, class_id); + let ptr = NonNull::new(ptr).unwrap().cast::>(); + let tracer = Tracer::from_ffi(rt, mark_func); + (ptr.as_ref().v_table.trace)(ptr, tracer) +} + /// FFI finalizer, destroying the object once it is delete by the Gc. pub(crate) unsafe extern "C" fn callable_finalizer(rt: *mut qjs::JSRuntime, val: qjs::JSValue) { let class_id = Opaque::from_runtime_ptr(rt).get_callable_id(); @@ -61,6 +82,20 @@ pub(crate) unsafe extern "C" fn call( (ptr.as_ref().v_table.call)(ptr, ctx, function, this, argc, argv, flags) } +/// FFI exotic get_own_property function for classes with exotic behavior. +pub(crate) unsafe extern "C" fn exotic_get_property( + ctx: *mut qjs::JSContext, + obj: qjs::JSValueConst, + atom: qjs::JSAtom, + receiver: qjs::JSValueConst, +) -> qjs::JSValue { + let rt = qjs::JS_GetRuntime(ctx); + let id = Opaque::from_runtime_ptr(rt).get_exotic_id(); + let ptr = qjs::JS_GetOpaque(obj, id); + let ptr = NonNull::new(ptr).unwrap().cast::>(); + (ptr.as_ref().v_table.get_property)(ptr, ctx, obj, atom, receiver) +} + pub(crate) type FinalizerFunc = unsafe fn(this: NonNull>); pub(crate) type TraceFunc = for<'a> unsafe fn(this: NonNull>, tracer: Tracer<'a, 'static>); @@ -74,6 +109,14 @@ pub(crate) type CallFunc = for<'a> unsafe fn( flags: qjs::c_int, ) -> qjs::JSValue; +pub(crate) type GetPropertyFunc = for<'a> unsafe fn( + this_ptr: NonNull>, + ctx: *mut qjs::JSContext, + obj: qjs::JSValueConst, + atom: qjs::JSAtom, + receiver: qjs::JSValueConst, +) -> qjs::JSValue; + pub(crate) type TypeIdFn = fn() -> TypeId; pub(crate) struct VTable { @@ -81,6 +124,7 @@ pub(crate) struct VTable { finalizer: FinalizerFunc, trace: TraceFunc, call: CallFunc, + get_property: GetPropertyFunc, } impl VTable { @@ -119,6 +163,26 @@ impl VTable { })) } + unsafe fn get_property_impl<'js, C: JsClass<'js>>( + this_ptr: NonNull>, + ctx: *mut qjs::JSContext, + obj: qjs::JSValueConst, + atom: qjs::JSAtom, + receiver: qjs::JSValueConst, + ) -> qjs::JSValue { + let this_ptr = this_ptr.cast::>>(); + let ctx = Ctx::from_ptr(ctx); + let atom = Atom::from_atom_val_dup(ctx.clone(), atom); + let obj = Value::from_js_value_const(ctx.clone(), obj); + let receiver = Value::from_js_value_const(ctx.clone(), receiver); + + ctx.handle_panic(AssertUnwindSafe(|| { + C::exotic_get_property(&this_ptr.as_ref().data, &ctx, atom, obj, receiver) + .map(Value::into_js_value) + .unwrap_or_else(|e| e.throw(&ctx)) + })) + } + pub fn get<'js, C: JsClass<'js>>() -> &'static VTable { trait HasVTable { const VTABLE: VTable; @@ -130,6 +194,7 @@ impl VTable { finalizer: VTable::finalizer_impl::<'js, C>, trace: VTable::trace_impl::, call: VTable::call_impl::, + get_property: VTable::get_property_impl::, }; } &::VTABLE diff --git a/core/src/runtime/opaque.rs b/core/src/runtime/opaque.rs index 4716b3e7e..a0df21ce9 100644 --- a/core/src/runtime/opaque.rs +++ b/core/src/runtime/opaque.rs @@ -11,8 +11,7 @@ use alloc::boxed::Box; use core::{ any::{Any, TypeId}, cell::{Cell, UnsafeCell}, - marker::PhantomData, - ptr, + marker::PhantomData, ptr, }; #[cfg(feature = "std")] @@ -48,6 +47,8 @@ pub(crate) struct Opaque<'js> { class_id: qjs::JSClassID, /// The class id for rust classes which can be called. callable_class_id: qjs::JSClassID, + /// The class id for exotic classes + exotic_class_id: qjs::JSClassID, prototypes: UnsafeCell>>>, @@ -56,11 +57,23 @@ pub(crate) struct Opaque<'js> { #[cfg(feature = "futures")] spawner: Option>, + exotic_methods: *mut qjs::JSClassExoticMethods, + _marker: PhantomData<&'js ()>, } impl<'js> Opaque<'js> { pub fn new() -> Self { + let exotic_methods = Box::into_raw(Box::new(qjs::JSClassExoticMethods { + get_own_property: None, + get_own_property_names: None, + delete_property: None, // TODO: Implement + define_own_property: None, // TODO: Implement + has_property: None, // TODO: Implement + set_property: None, // TODO: Implement + get_property: Some(crate::class::ffi::exotic_get_property), + })); + Opaque { panic: Cell::new(None), @@ -72,6 +85,7 @@ impl<'js> Opaque<'js> { class_id: qjs::JS_INVALID_CLASS_ID, callable_class_id: qjs::JS_INVALID_CLASS_ID, + exotic_class_id: qjs::JS_INVALID_CLASS_ID, prototypes: UnsafeCell::new(HashMap::new()), @@ -81,6 +95,8 @@ impl<'js> Opaque<'js> { #[cfg(feature = "futures")] spawner: None, + + exotic_methods, } } @@ -94,13 +110,14 @@ impl<'js> Opaque<'js> { pub unsafe fn initialize(&mut self, rt: *mut qjs::JSRuntime) -> Result<(), Error> { qjs::JS_NewClassID(rt, (&mut self.class_id) as *mut qjs::JSClassID); qjs::JS_NewClassID(rt, (&mut self.callable_class_id) as *mut qjs::JSClassID); + qjs::JS_NewClassID(rt, (&mut self.exotic_class_id) as *mut qjs::JSClassID); let class_def = qjs::JSClassDef { class_name: c"RustClass".as_ptr().cast(), finalizer: Some(class::ffi::class_finalizer), gc_mark: Some(class::ffi::class_trace), call: None, - exotic: ptr::null_mut(), + exotic: std::ptr::null_mut(), }; if 0 != qjs::JS_NewClass(rt, self.class_id, &class_def) { @@ -119,6 +136,18 @@ impl<'js> Opaque<'js> { return Err(Error::Unknown); } + let class_def = qjs::JSClassDef { + class_name: c"RustExotic".as_ptr().cast(), + finalizer: Some(class::ffi::exotic_class_finalizer), + gc_mark: Some(class::ffi::exotic_class_trace), + call: None, + exotic: self.exotic_methods, + }; + + if 0 != qjs::JS_NewClass(rt, self.exotic_class_id, &class_def) { + return Err(Error::Unknown); + } + Ok(()) } @@ -235,6 +264,10 @@ impl<'js> Opaque<'js> { self.callable_class_id } + pub fn get_exotic_id(&self) -> qjs::JSClassID { + self.exotic_class_id + } + pub fn get_or_insert_prototype>( &self, ctx: &Ctx<'js>, @@ -263,6 +296,7 @@ impl<'js> Opaque<'js> { self.prototypes.get_mut().clear(); #[cfg(feature = "futures")] self.spawner.take(); - self.userdata.clear() + self.userdata.clear(); + unsafe { drop(Box::from_raw(self.exotic_methods)) }; } } From 2e64066f3888a7a9d0526d553b0d427071ecbe61 Mon Sep 17 00:00:00 2001 From: Your Name Date: Sat, 30 Aug 2025 20:41:08 +0530 Subject: [PATCH 03/14] add set_property --- core/src/class.rs | 32 +++++++++++++++++++-- core/src/class/ffi.rs | 57 +++++++++++++++++++++++++++++++++++++- core/src/result.rs | 14 ++++++++++ core/src/runtime/opaque.rs | 2 +- 4 files changed, 101 insertions(+), 4 deletions(-) diff --git a/core/src/class.rs b/core/src/class.rs index c706e7c96..1ab6309ca 100644 --- a/core/src/class.rs +++ b/core/src/class.rs @@ -59,7 +59,7 @@ pub trait JsClass<'js>: Trace<'js> + JsLifetime<'js> + Sized { /// The function which will be called if a set property is performed on an object with this class /// /// Not yet implemented. - fn exotic_set_property<'a>(this: &JsCell<'js, Self>, _params: Params<'a, 'js>) -> Result { + fn exotic_set_property<'a>(this: &JsCell<'js, Self>, _ctx: &Ctx<'js>, _atom: Atom<'js>, _obj: Value<'js>, _receiver: Value<'js>, _value: Value<'js>) -> Result { let _ = this; Ok(false) } @@ -752,12 +752,26 @@ mod test { _receiver: crate::Value<'js>, ) -> crate::Result> { if atom.to_string()? == "hello" { - assert!(this.borrow().i == 0); + assert!(this.borrow().i == 42); Ok("world".into_js(ctx)?) } else { Ok(crate::Value::new_null(ctx.clone())) } } + + fn exotic_set_property<'a>(this: &super::JsCell<'js, Self>, ctx: &crate::Ctx<'js>, atom: crate::Atom<'js>, _obj: crate::Value<'js>, _receiver: crate::Value<'js>, _value: crate::Value<'js>) -> crate::Result { + let _ = this; + if atom.to_string()? == "i" { + let Some(new_i) = _value.as_int() else { + let err_val = crate::String::from_str(ctx.clone(), "i must be an integer")?.into_value(); + return Err(ctx.throw(err_val)); + }; + this.borrow_mut().i = new_i; + return Ok(true); + } + let err_val = crate::String::from_str(ctx.clone(), "Properties are read-only")?.into_value(); + Err(ctx.throw(err_val)) + } } test_with(|ctx| { @@ -770,6 +784,20 @@ mod test { if(exotic.foo !== null) { throw new Error('foo should be null'); } + try { + exotic.foo = 1 + } catch(e) { + if (e?.toString() !== 'Properties are read-only') { + throw new Error('wrong error message: ' + e?.toString()); + } + } + if (exotic.foo !== null) { + throw new Error('foo should be null'); + } + exotic.i = 42; + if (exotic.hello === 42) { + throw new Error('i should be 42'); + } exotic.hello ", ) diff --git a/core/src/class/ffi.rs b/core/src/class/ffi.rs index f95f434db..30859c1e9 100644 --- a/core/src/class/ffi.rs +++ b/core/src/class/ffi.rs @@ -82,7 +82,7 @@ pub(crate) unsafe extern "C" fn call( (ptr.as_ref().v_table.call)(ptr, ctx, function, this, argc, argv, flags) } -/// FFI exotic get_own_property function for classes with exotic behavior. +/// FFI exotic get_property function for classes with exotic behavior. pub(crate) unsafe extern "C" fn exotic_get_property( ctx: *mut qjs::JSContext, obj: qjs::JSValueConst, @@ -96,6 +96,22 @@ pub(crate) unsafe extern "C" fn exotic_get_property( (ptr.as_ref().v_table.get_property)(ptr, ctx, obj, atom, receiver) } +/// FFI exotic set_property function for classes with exotic behavior. +pub(crate) unsafe extern "C" fn exotic_set_property( + ctx: *mut qjs::JSContext, + obj: qjs::JSValueConst, + atom: qjs::JSAtom, + value: qjs::JSValue, + receiver: qjs::JSValueConst, + flags: qjs::c_int, +) -> qjs::c_int { + let rt = qjs::JS_GetRuntime(ctx); + let id = Opaque::from_runtime_ptr(rt).get_exotic_id(); + let ptr = qjs::JS_GetOpaque(obj, id); + let ptr = NonNull::new(ptr).unwrap().cast::>(); + (ptr.as_ref().v_table.set_property)(ptr, ctx, obj, atom, receiver, value, flags) +} + pub(crate) type FinalizerFunc = unsafe fn(this: NonNull>); pub(crate) type TraceFunc = for<'a> unsafe fn(this: NonNull>, tracer: Tracer<'a, 'static>); @@ -117,6 +133,16 @@ pub(crate) type GetPropertyFunc = for<'a> unsafe fn( receiver: qjs::JSValueConst, ) -> qjs::JSValue; +pub(crate) type SetPropertyFunc = for<'a> unsafe fn( + this_ptr: NonNull>, + ctx: *mut qjs::JSContext, + obj: qjs::JSValueConst, + atom: qjs::JSAtom, + receiver: qjs::JSValueConst, + value: qjs::JSValue, + flags: qjs::c_int, +) -> qjs::c_int; + pub(crate) type TypeIdFn = fn() -> TypeId; pub(crate) struct VTable { @@ -125,6 +151,7 @@ pub(crate) struct VTable { trace: TraceFunc, call: CallFunc, get_property: GetPropertyFunc, + set_property: SetPropertyFunc, } impl VTable { @@ -183,6 +210,33 @@ impl VTable { })) } + unsafe fn set_property_impl<'js, C: JsClass<'js>>( + this_ptr: NonNull>, + ctx: *mut qjs::JSContext, + obj: qjs::JSValueConst, + atom: qjs::JSAtom, + receiver: qjs::JSValueConst, + value: qjs::JSValue, + _flags: qjs::c_int, + ) -> qjs::c_int { + let this_ptr = this_ptr.cast::>>(); + let ctx = Ctx::from_ptr(ctx); + let atom = Atom::from_atom_val_dup(ctx.clone(), atom); + let obj = Value::from_js_value_const(ctx.clone(), obj); + let receiver = Value::from_js_value_const(ctx.clone(), receiver); + let value = Value::from_js_value(ctx.clone(), value); + + ctx.handle_panic_exotic(AssertUnwindSafe(|| { + match C::exotic_set_property(&this_ptr.as_ref().data, &ctx, atom, obj, receiver, value) { + Ok(v) => if v { 1 } else { 0 }, + Err(e) => { + e.throw(&ctx); + -1 + } + } + })) + } + pub fn get<'js, C: JsClass<'js>>() -> &'static VTable { trait HasVTable { const VTABLE: VTable; @@ -195,6 +249,7 @@ impl VTable { trace: VTable::trace_impl::, call: VTable::call_impl::, get_property: VTable::get_property_impl::, + set_property: VTable::set_property_impl::, }; } &::VTABLE diff --git a/core/src/result.rs b/core/src/result.rs index 1a2cf78d6..96ee2c3ae 100644 --- a/core/src/result.rs +++ b/core/src/result.rs @@ -689,6 +689,20 @@ impl<'js> Ctx<'js> { } } + pub(crate) fn handle_panic_exotic(&self, f: F) -> qjs::c_int + where + F: FnOnce() -> qjs::c_int + UnwindSafe, + { + match crate::util::catch_unwind(f) { + Ok(x) => x, + Err(e) => unsafe { + self.get_opaque().set_panic(e); + qjs::JS_Throw(self.as_ptr(), qjs::JS_MKVAL(qjs::JS_TAG_EXCEPTION, 0)); + -1 + }, + } + } + /// Handle possible exceptions in [`JSValue`]'s and turn them into errors /// Will return the [`JSValue`] if it is not an exception /// diff --git a/core/src/runtime/opaque.rs b/core/src/runtime/opaque.rs index a0df21ce9..66b437478 100644 --- a/core/src/runtime/opaque.rs +++ b/core/src/runtime/opaque.rs @@ -70,7 +70,7 @@ impl<'js> Opaque<'js> { delete_property: None, // TODO: Implement define_own_property: None, // TODO: Implement has_property: None, // TODO: Implement - set_property: None, // TODO: Implement + set_property: Some(crate::class::ffi::exotic_set_property), get_property: Some(crate::class::ffi::exotic_get_property), })); From 3e41a4d3d7fe756a40a7a08397662a08f24d4864 Mon Sep 17 00:00:00 2001 From: Your Name Date: Sat, 30 Aug 2025 21:40:05 +0530 Subject: [PATCH 04/14] add has/delete property --- core/src/class.rs | 50 ++++++++++++++++++---- core/src/class/ffi.rs | 88 ++++++++++++++++++++++++++++++++++++++ core/src/runtime/opaque.rs | 8 ++-- 3 files changed, 134 insertions(+), 12 deletions(-) diff --git a/core/src/class.rs b/core/src/class.rs index 1ab6309ca..3c18767a1 100644 --- a/core/src/class.rs +++ b/core/src/class.rs @@ -57,25 +57,19 @@ pub trait JsClass<'js>: Trace<'js> + JsLifetime<'js> + Sized { } /// The function which will be called if a set property is performed on an object with this class - /// - /// Not yet implemented. fn exotic_set_property<'a>(this: &JsCell<'js, Self>, _ctx: &Ctx<'js>, _atom: Atom<'js>, _obj: Value<'js>, _receiver: Value<'js>, _value: Value<'js>) -> Result { let _ = this; Ok(false) } /// The function which will be called if a delete property is performed on an object with this class - /// - /// Not yet implemented. - fn exotic_delete_property<'a>(this: &JsCell<'js, Self>, _params: Params<'a, 'js>) -> Result { + fn exotic_delete_property<'a>(this: &JsCell<'js, Self>, _ctx: &Ctx<'js>, _atom: Atom<'js>, _obj: Value<'js>) -> Result { let _ = this; Ok(false) } /// The function which will be called if has property or similar is called on an object with this class - /// - /// Not yet implemented. - fn exotic_has_property<'a>(this: &JsCell<'js, Self>, _params: Params<'a, 'js>) -> Result { + fn exotic_has_property<'a>(this: &JsCell<'js, Self>, _ctx: &Ctx<'js>, _atom: Atom<'js>, _obj: Value<'js>) -> Result { let _ = this; Ok(false) } @@ -754,6 +748,11 @@ mod test { if atom.to_string()? == "hello" { assert!(this.borrow().i == 42); Ok("world".into_js(ctx)?) + } else if atom.to_string()? == "toString" { + Ok(Function::new(ctx.clone(), || { + let f = "class Exotic { [native code] }"; + Ok::<&'static str, crate::Error>(f) + })?.into_value()) } else { Ok(crate::Value::new_null(ctx.clone())) } @@ -772,11 +771,33 @@ mod test { let err_val = crate::String::from_str(ctx.clone(), "Properties are read-only")?.into_value(); Err(ctx.throw(err_val)) } + + fn exotic_has_property<'a>(this: &super::JsCell<'js, Self>, _ctx: &crate::Ctx<'js>, atom: crate::Atom<'js>, _obj: crate::Value<'js>) -> crate::Result { + let _ = this; + println!("Got atom: {}", atom.to_string()?); + if atom.to_string()? == "hello" || atom.to_string()? == "i" || atom.to_string()? == "toString" { + return Ok(true); + } + + Ok(false) + } + + fn exotic_delete_property<'a>(_this: &super::JsCell<'js, Self>, ctx: &crate::Ctx<'js>, _atom: crate::Atom<'js>, _obj: crate::Value<'js>) -> crate::Result { + let err_val = crate::String::from_str(ctx.clone(), "Properties cannot be deleted")?.into_value(); + Err(ctx.throw(err_val)) + } } test_with(|ctx| { let exotic = Class::::instance(ctx.clone(), Exotic { i: 0 }).unwrap(); ctx.globals().set("exotic", exotic).unwrap(); + ctx.globals().set("assert", Function::new(ctx.clone(), |ctx: crate::Ctx<'_>, cond: bool, msg: String| { + if !cond { + let err_val = crate::String::from_str(ctx.clone(), &msg)?.into_value(); + return Err(ctx.throw(err_val)); + } + Ok(()) + })).unwrap(); let v = ctx .eval::( @@ -798,6 +819,19 @@ mod test { if (exotic.hello === 42) { throw new Error('i should be 42'); } + assert(exotic?.toString() === 'class Exotic { [native code] }', `exotic.toString() should be 'class Exotic { [native code] }' but is ${exotic?.toString()}`); + assert('i' in exotic, 'i should be in exotic'); + assert('hello' in exotic, 'hello should be in exotic'); + assert(!('foo' in exotic), 'foo should not be in exotic'); + + try { + delete exotic.i; + } catch(e) { + if (e?.toString() !== 'Properties cannot be deleted') { + throw new Error('wrong error message: ' + e?.toString()); + } + } + exotic.hello ", ) diff --git a/core/src/class/ffi.rs b/core/src/class/ffi.rs index 30859c1e9..0245f7c97 100644 --- a/core/src/class/ffi.rs +++ b/core/src/class/ffi.rs @@ -112,6 +112,32 @@ pub(crate) unsafe extern "C" fn exotic_set_property( (ptr.as_ref().v_table.set_property)(ptr, ctx, obj, atom, receiver, value, flags) } +/// FFI exotic has_property function for classes with exotic behavior. +pub(crate) unsafe extern "C" fn exotic_has_property( + ctx: *mut qjs::JSContext, + obj: qjs::JSValueConst, + atom: qjs::JSAtom, +) -> qjs::c_int { + let rt = qjs::JS_GetRuntime(ctx); + let id = Opaque::from_runtime_ptr(rt).get_exotic_id(); + let ptr = qjs::JS_GetOpaque(obj, id); + let ptr = NonNull::new(ptr).unwrap().cast::>(); + (ptr.as_ref().v_table.has_property)(ptr, ctx, obj, atom) +} + +/// FFI exotic delete_property function for classes with exotic behavior. +pub(crate) unsafe extern "C" fn exotic_delete_property( + ctx: *mut qjs::JSContext, + obj: qjs::JSValueConst, + prop: qjs::JSAtom, +) -> qjs::c_int { + let rt = qjs::JS_GetRuntime(ctx); + let id = Opaque::from_runtime_ptr(rt).get_exotic_id(); + let ptr = qjs::JS_GetOpaque(obj, id); + let ptr = NonNull::new(ptr).unwrap().cast::>(); + (ptr.as_ref().v_table.delete_property)(ptr, ctx, obj, prop) +} + pub(crate) type FinalizerFunc = unsafe fn(this: NonNull>); pub(crate) type TraceFunc = for<'a> unsafe fn(this: NonNull>, tracer: Tracer<'a, 'static>); @@ -143,6 +169,20 @@ pub(crate) type SetPropertyFunc = for<'a> unsafe fn( flags: qjs::c_int, ) -> qjs::c_int; +pub(crate) type HasPropertyFunc = for<'a> unsafe fn( + this_ptr: NonNull>, + ctx: *mut qjs::JSContext, + obj: qjs::JSValueConst, + atom: qjs::JSAtom, +) -> qjs::c_int; + +pub(crate) type DeletePropertyFunc = for<'a> unsafe fn( + this_ptr: NonNull>, + ctx: *mut qjs::JSContext, + obj: qjs::JSValueConst, + prop: qjs::JSAtom, +) -> qjs::c_int; + pub(crate) type TypeIdFn = fn() -> TypeId; pub(crate) struct VTable { @@ -152,6 +192,8 @@ pub(crate) struct VTable { call: CallFunc, get_property: GetPropertyFunc, set_property: SetPropertyFunc, + has_property: HasPropertyFunc, + delete_property: DeletePropertyFunc, } impl VTable { @@ -237,6 +279,50 @@ impl VTable { })) } + unsafe fn has_property_impl<'js, C: JsClass<'js>>( + this_ptr: NonNull>, + ctx: *mut qjs::JSContext, + obj: qjs::JSValueConst, + atom: qjs::JSAtom, + ) -> qjs::c_int { + let this_ptr = this_ptr.cast::>>(); + let ctx = Ctx::from_ptr(ctx); + let atom = Atom::from_atom_val_dup(ctx.clone(), atom); + let obj = Value::from_js_value_const(ctx.clone(), obj); + + ctx.handle_panic_exotic(AssertUnwindSafe(|| { + match C::exotic_has_property(&this_ptr.as_ref().data, &ctx, atom, obj) { + Ok(v) => if v { 1 } else { 0 }, + Err(e) => { + e.throw(&ctx); + -1 + } + } + })) + } + + unsafe fn delete_property_impl<'js, C: JsClass<'js>>( + this_ptr: NonNull>, + ctx: *mut qjs::JSContext, + obj: qjs::JSValueConst, + atom: qjs::JSAtom, + ) -> qjs::c_int { + let this_ptr = this_ptr.cast::>>(); + let ctx = Ctx::from_ptr(ctx); + let atom = Atom::from_atom_val_dup(ctx.clone(), atom); + let obj = Value::from_js_value_const(ctx.clone(), obj); + + ctx.handle_panic_exotic(AssertUnwindSafe(|| { + match C::exotic_delete_property(&this_ptr.as_ref().data, &ctx, atom, obj) { + Ok(v) => if v { 1 } else { 0 }, + Err(e) => { + e.throw(&ctx); + -1 + } + } + })) + } + pub fn get<'js, C: JsClass<'js>>() -> &'static VTable { trait HasVTable { const VTABLE: VTable; @@ -250,6 +336,8 @@ impl VTable { call: VTable::call_impl::, get_property: VTable::get_property_impl::, set_property: VTable::set_property_impl::, + has_property: VTable::has_property_impl::, + delete_property: VTable::delete_property_impl::, }; } &::VTABLE diff --git a/core/src/runtime/opaque.rs b/core/src/runtime/opaque.rs index 66b437478..68c4aa9f3 100644 --- a/core/src/runtime/opaque.rs +++ b/core/src/runtime/opaque.rs @@ -65,11 +65,11 @@ pub(crate) struct Opaque<'js> { impl<'js> Opaque<'js> { pub fn new() -> Self { let exotic_methods = Box::into_raw(Box::new(qjs::JSClassExoticMethods { - get_own_property: None, - get_own_property_names: None, - delete_property: None, // TODO: Implement + get_own_property: None, // TODO: Implement + get_own_property_names: None, // TODO: Implement + delete_property: Some(crate::class::ffi::exotic_delete_property), define_own_property: None, // TODO: Implement - has_property: None, // TODO: Implement + has_property: Some(crate::class::ffi::exotic_has_property), set_property: Some(crate::class::ffi::exotic_set_property), get_property: Some(crate::class::ffi::exotic_get_property), })); From 871de054174a1d66c7730bd612850817c56566fb Mon Sep 17 00:00:00 2001 From: Your Name Date: Sat, 30 Aug 2025 21:41:42 +0530 Subject: [PATCH 05/14] cargo fmt --- core/src/class.rs | 101 ++++++++++++++++++++++++------- core/src/class/ffi.rs | 27 +++++++-- core/src/context/async.rs | 8 ++- core/src/context/async/future.rs | 2 +- core/src/runtime/opaque.rs | 5 +- 5 files changed, 112 insertions(+), 31 deletions(-) diff --git a/core/src/class.rs b/core/src/class.rs index 3c18767a1..5217eee0d 100644 --- a/core/src/class.rs +++ b/core/src/class.rs @@ -1,7 +1,10 @@ //! JavaScript classes defined from Rust. use crate::{ - function::Params, qjs::{self}, value::Constructor, Atom, Ctx, Error, FromJs, IntoJs, JsLifetime, Object, Result, Value + function::Params, + qjs::{self}, + value::Constructor, + Atom, Ctx, Error, FromJs, IntoJs, JsLifetime, Object, Result, Value, }; use alloc::boxed::Box; use core::{hash::Hash, marker::PhantomData, mem, ops::Deref, ptr::NonNull}; @@ -51,25 +54,48 @@ pub trait JsClass<'js>: Trace<'js> + JsLifetime<'js> + Sized { } /// The function which will be called if a get property is performed on an object with this class - fn exotic_get_property<'a>(this: &JsCell<'js, Self>, ctx: &Ctx<'js>, _atom: Atom<'js>, _obj: Value<'js>, _receiver: Value<'js>) -> Result> { + fn exotic_get_property<'a>( + this: &JsCell<'js, Self>, + ctx: &Ctx<'js>, + _atom: Atom<'js>, + _obj: Value<'js>, + _receiver: Value<'js>, + ) -> Result> { let _ = this; Ok(Value::new_undefined(ctx.clone())) } /// The function which will be called if a set property is performed on an object with this class - fn exotic_set_property<'a>(this: &JsCell<'js, Self>, _ctx: &Ctx<'js>, _atom: Atom<'js>, _obj: Value<'js>, _receiver: Value<'js>, _value: Value<'js>) -> Result { + fn exotic_set_property<'a>( + this: &JsCell<'js, Self>, + _ctx: &Ctx<'js>, + _atom: Atom<'js>, + _obj: Value<'js>, + _receiver: Value<'js>, + _value: Value<'js>, + ) -> Result { let _ = this; Ok(false) } /// The function which will be called if a delete property is performed on an object with this class - fn exotic_delete_property<'a>(this: &JsCell<'js, Self>, _ctx: &Ctx<'js>, _atom: Atom<'js>, _obj: Value<'js>) -> Result { + fn exotic_delete_property<'a>( + this: &JsCell<'js, Self>, + _ctx: &Ctx<'js>, + _atom: Atom<'js>, + _obj: Value<'js>, + ) -> Result { let _ = this; Ok(false) } /// The function which will be called if has property or similar is called on an object with this class - fn exotic_has_property<'a>(this: &JsCell<'js, Self>, _ctx: &Ctx<'js>, _atom: Atom<'js>, _obj: Value<'js>) -> Result { + fn exotic_has_property<'a>( + this: &JsCell<'js, Self>, + _ctx: &Ctx<'js>, + _atom: Atom<'js>, + _obj: Value<'js>, + ) -> Result { let _ = this; Ok(false) } @@ -752,38 +778,62 @@ mod test { Ok(Function::new(ctx.clone(), || { let f = "class Exotic { [native code] }"; Ok::<&'static str, crate::Error>(f) - })?.into_value()) + })? + .into_value()) } else { Ok(crate::Value::new_null(ctx.clone())) } } - fn exotic_set_property<'a>(this: &super::JsCell<'js, Self>, ctx: &crate::Ctx<'js>, atom: crate::Atom<'js>, _obj: crate::Value<'js>, _receiver: crate::Value<'js>, _value: crate::Value<'js>) -> crate::Result { + fn exotic_set_property<'a>( + this: &super::JsCell<'js, Self>, + ctx: &crate::Ctx<'js>, + atom: crate::Atom<'js>, + _obj: crate::Value<'js>, + _receiver: crate::Value<'js>, + _value: crate::Value<'js>, + ) -> crate::Result { let _ = this; if atom.to_string()? == "i" { let Some(new_i) = _value.as_int() else { - let err_val = crate::String::from_str(ctx.clone(), "i must be an integer")?.into_value(); + let err_val = crate::String::from_str(ctx.clone(), "i must be an integer")? + .into_value(); return Err(ctx.throw(err_val)); }; this.borrow_mut().i = new_i; return Ok(true); } - let err_val = crate::String::from_str(ctx.clone(), "Properties are read-only")?.into_value(); + let err_val = + crate::String::from_str(ctx.clone(), "Properties are read-only")?.into_value(); Err(ctx.throw(err_val)) } - fn exotic_has_property<'a>(this: &super::JsCell<'js, Self>, _ctx: &crate::Ctx<'js>, atom: crate::Atom<'js>, _obj: crate::Value<'js>) -> crate::Result { + fn exotic_has_property<'a>( + this: &super::JsCell<'js, Self>, + _ctx: &crate::Ctx<'js>, + atom: crate::Atom<'js>, + _obj: crate::Value<'js>, + ) -> crate::Result { let _ = this; println!("Got atom: {}", atom.to_string()?); - if atom.to_string()? == "hello" || atom.to_string()? == "i" || atom.to_string()? == "toString" { + if atom.to_string()? == "hello" + || atom.to_string()? == "i" + || atom.to_string()? == "toString" + { return Ok(true); } - + Ok(false) } - fn exotic_delete_property<'a>(_this: &super::JsCell<'js, Self>, ctx: &crate::Ctx<'js>, _atom: crate::Atom<'js>, _obj: crate::Value<'js>) -> crate::Result { - let err_val = crate::String::from_str(ctx.clone(), "Properties cannot be deleted")?.into_value(); + fn exotic_delete_property<'a>( + _this: &super::JsCell<'js, Self>, + ctx: &crate::Ctx<'js>, + _atom: crate::Atom<'js>, + _obj: crate::Value<'js>, + ) -> crate::Result { + let err_val = crate::String::from_str(ctx.clone(), "Properties cannot be deleted")? + .into_value(); Err(ctx.throw(err_val)) } } @@ -791,13 +841,22 @@ mod test { test_with(|ctx| { let exotic = Class::::instance(ctx.clone(), Exotic { i: 0 }).unwrap(); ctx.globals().set("exotic", exotic).unwrap(); - ctx.globals().set("assert", Function::new(ctx.clone(), |ctx: crate::Ctx<'_>, cond: bool, msg: String| { - if !cond { - let err_val = crate::String::from_str(ctx.clone(), &msg)?.into_value(); - return Err(ctx.throw(err_val)); - } - Ok(()) - })).unwrap(); + ctx.globals() + .set( + "assert", + Function::new( + ctx.clone(), + |ctx: crate::Ctx<'_>, cond: bool, msg: String| { + if !cond { + let err_val = + crate::String::from_str(ctx.clone(), &msg)?.into_value(); + return Err(ctx.throw(err_val)); + } + Ok(()) + }, + ), + ) + .unwrap(); let v = ctx .eval::( diff --git a/core/src/class/ffi.rs b/core/src/class/ffi.rs index 0245f7c97..815560720 100644 --- a/core/src/class/ffi.rs +++ b/core/src/class/ffi.rs @@ -269,8 +269,15 @@ impl VTable { let value = Value::from_js_value(ctx.clone(), value); ctx.handle_panic_exotic(AssertUnwindSafe(|| { - match C::exotic_set_property(&this_ptr.as_ref().data, &ctx, atom, obj, receiver, value) { - Ok(v) => if v { 1 } else { 0 }, + match C::exotic_set_property(&this_ptr.as_ref().data, &ctx, atom, obj, receiver, value) + { + Ok(v) => { + if v { + 1 + } else { + 0 + } + } Err(e) => { e.throw(&ctx); -1 @@ -292,7 +299,13 @@ impl VTable { ctx.handle_panic_exotic(AssertUnwindSafe(|| { match C::exotic_has_property(&this_ptr.as_ref().data, &ctx, atom, obj) { - Ok(v) => if v { 1 } else { 0 }, + Ok(v) => { + if v { + 1 + } else { + 0 + } + } Err(e) => { e.throw(&ctx); -1 @@ -314,7 +327,13 @@ impl VTable { ctx.handle_panic_exotic(AssertUnwindSafe(|| { match C::exotic_delete_property(&this_ptr.as_ref().data, &ctx, atom, obj) { - Ok(v) => if v { 1 } else { 0 }, + Ok(v) => { + if v { + 1 + } else { + 0 + } + } Err(e) => { e.throw(&ctx); -1 diff --git a/core/src/context/async.rs b/core/src/context/async.rs index 1c84fa5e2..e02a2522f 100644 --- a/core/src/context/async.rs +++ b/core/src/context/async.rs @@ -3,7 +3,10 @@ use super::{ owner::{ContextOwner, DropContext}, ContextBuilder, Intrinsic, }; -use crate::{context::r#async::future::CallbackFuture, markers::ParallelSend, qjs, runtime::AsyncRuntime, Ctx, Error, Result}; +use crate::{ + context::r#async::future::CallbackFuture, markers::ParallelSend, qjs, runtime::AsyncRuntime, + Ctx, Error, Result, +}; use core::{mem, ptr::NonNull}; mod future; @@ -207,8 +210,7 @@ impl AsyncContext { /// future. pub fn async_with(&self, f: F) -> WithFuture where - F: for<'js> FnOnce(Ctx<'js>) -> CallbackFuture<'js, R> - + ParallelSend, + F: for<'js> FnOnce(Ctx<'js>) -> CallbackFuture<'js, R> + ParallelSend, R: ParallelSend, { WithFuture::new(self, f) diff --git a/core/src/context/async/future.rs b/core/src/context/async/future.rs index 01fa2576c..9f71498ea 100644 --- a/core/src/context/async/future.rs +++ b/core/src/context/async/future.rs @@ -49,7 +49,7 @@ enum WithFutureState<'a, F, R> { #[cfg(not(feature = "parallel"))] pub type CallbackFuture<'js, R> = Pin + 'js>>; #[cfg(feature = "parallel")] -pub type CallbackFuture<'js, R> = Pin + 'js + Send>>; +pub type CallbackFuture<'js, R> = Pin + 'js + Send>>; impl<'a, F, R> WithFuture<'a, F, R> where diff --git a/core/src/runtime/opaque.rs b/core/src/runtime/opaque.rs index 68c4aa9f3..fc932c534 100644 --- a/core/src/runtime/opaque.rs +++ b/core/src/runtime/opaque.rs @@ -11,7 +11,8 @@ use alloc::boxed::Box; use core::{ any::{Any, TypeId}, cell::{Cell, UnsafeCell}, - marker::PhantomData, ptr, + marker::PhantomData, + ptr, }; #[cfg(feature = "std")] @@ -65,7 +66,7 @@ pub(crate) struct Opaque<'js> { impl<'js> Opaque<'js> { pub fn new() -> Self { let exotic_methods = Box::into_raw(Box::new(qjs::JSClassExoticMethods { - get_own_property: None, // TODO: Implement + get_own_property: None, // TODO: Implement get_own_property_names: None, // TODO: Implement delete_property: Some(crate::class::ffi::exotic_delete_property), define_own_property: None, // TODO: Implement From 5aa6b0d3bf8328c2deb1f2e24992de6344b53fac Mon Sep 17 00:00:00 2001 From: Your Name Date: Sat, 30 Aug 2025 22:26:55 +0530 Subject: [PATCH 06/14] add iterator test case to exotic --- core/src/class.rs | 142 +++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 141 insertions(+), 1 deletion(-) diff --git a/core/src/class.rs b/core/src/class.rs index 5217eee0d..5310a637d 100644 --- a/core/src/class.rs +++ b/core/src/class.rs @@ -402,6 +402,7 @@ impl<'js, C: JsClass<'js>> IntoJs<'js> for Class<'js, C> { #[cfg(test)] mod test { + use core::sync::atomic::AtomicI32; use std::sync::{ atomic::{AtomicBool, Ordering}, Arc, @@ -412,7 +413,8 @@ mod test { function::This, test_with, value::Constructor, - CatchResultExt, Class, Context, FromJs, Function, IntoJs, JsLifetime, Object, Runtime, + CatchResultExt, Class, Context, FromIteratorJs, FromJs, Function, IntoJs, JsLifetime, + Object, Runtime, }; /// Test circular references. @@ -735,6 +737,116 @@ mod test { #[test] fn exotic() { + pub struct ExoticIterator { + curr_state: Arc, + } + + impl<'js> Trace<'js> for ExoticIterator { + fn trace<'a>(&self, _tracer: Tracer<'a, 'js>) {} + } + + unsafe impl<'js> JsLifetime<'js> for ExoticIterator { + type Changed<'to> = ExoticIterator; + } + + impl<'js> JsClass<'js> for ExoticIterator { + const NAME: &'static str = "ExoticIterator"; + + type Mutable = Readable; + + const EXOTIC: bool = true; + + fn prototype(ctx: &crate::Ctx<'js>) -> crate::Result>> { + Ok(Some(crate::Object::new(ctx.clone())?)) + } + + fn constructor( + _ctx: &crate::Ctx<'js>, + ) -> crate::Result>> { + Ok(None) + } + + fn exotic_get_property<'a>( + this: &crate::class::JsCell<'js, Self>, + ctx: &crate::Ctx<'js>, + atom: crate::Atom<'js>, + _obj: crate::Value<'js>, + _receiver: crate::Value<'js>, + ) -> crate::Result> { + println!("Get property [iter]: {}", atom.to_string()?); + if atom.to_string()? == "next" { + let state = this.borrow().curr_state.clone(); + Ok(Function::new(ctx.clone(), move |ctx: crate::Ctx<'js>| { + // A really awful iterator thats implemented as a handwritten state machine + // + // Do not use this in production + if state.load(Ordering::SeqCst) <= 1 { + state.store(2, Ordering::SeqCst); + + let val = crate::Object::from_iter_js( + &ctx, + [ + ("done", false.into_js(&ctx)?), + ("value", vec!["hello", "1292"].into_js(&ctx)?), + ], + )? + .into_value(); + + return Ok::, crate::Error>(val); + } else if state.load(Ordering::SeqCst) == 2 { + state.fetch_add(1, Ordering::SeqCst); + + let val = crate::Object::from_iter_js( + &ctx, + [ + ("done", false.into_js(&ctx)?), + ( + "value", + vec!["i".into_js(&ctx)?, 43.into_js(&ctx)?] + .into_js(&ctx)?, + ), + ], + )? + .into_value(); + + return Ok(val); + } else { + state.fetch_add(1, Ordering::SeqCst); + + let val = crate::Object::from_iter_js( + &ctx, + [ + ("done", true.into_js(&ctx)?), + ("value", crate::Value::new_undefined(ctx.clone())), + ], + )? + .into_value(); + + return Ok(val); + } + })? + .into_value()) + } else { + Ok(crate::Value::new_undefined(ctx.clone())) + } + } + + fn exotic_has_property<'a>( + this: &super::JsCell<'js, Self>, + _ctx: &crate::Ctx<'js>, + atom: crate::Atom<'js>, + _obj: crate::Value<'js>, + ) -> crate::Result { + let _ = this; + if atom.to_string()? == "next" { + return Ok(true); + } + + Ok(false) + } + } + + #[derive(Clone)] pub struct Exotic { pub i: i32, } @@ -771,6 +883,11 @@ mod test { _obj: crate::Value<'js>, _receiver: crate::Value<'js>, ) -> crate::Result> { + let symbol_iterator = crate::Atom::from_predefined( + ctx.clone(), + crate::atom::PredefinedAtom::SymbolIterator, + ); + println!("Get property: {}", atom.to_string()?); if atom.to_string()? == "hello" { assert!(this.borrow().i == 42); Ok("world".into_js(ctx)?) @@ -780,6 +897,19 @@ mod test { Ok::<&'static str, crate::Error>(f) })? .into_value()) + } else if atom == symbol_iterator { + println!("Getting iterator"); + let exotic = Class::::instance( + ctx.clone(), + ExoticIterator { + curr_state: Arc::default(), + }, + )?; + println!("Returning ExoticIterator"); + Ok(Function::new(ctx.clone(), move || { + Ok::, crate::Error>(exotic.clone().into_value()) + })? + .into_value()) } else { Ok(crate::Value::new_null(ctx.clone())) } @@ -891,6 +1021,16 @@ mod test { } } + let resp = [] + for (let [objKey, value] of exotic) { + if (objKey !== 'i' && objKey !== 'hello') { + throw new Error('only i and hello should be enumerable, got ' + objKey); + } + resp.push(`${objKey}:${value}`); + } + + assert(resp.toString() === 'hello:1292,i:43', `${resp.toString()} with length ${resp.length} should be [] as properties are not enumerable`); + exotic.hello ", ) From deda179bba7e8350d35f5dd098eeffa237c5c480 Mon Sep 17 00:00:00 2001 From: Your Name Date: Tue, 7 Oct 2025 17:04:00 -0400 Subject: [PATCH 07/14] fix --- core/src/class.rs | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/core/src/class.rs b/core/src/class.rs index 5310a637d..3c0154ff8 100644 --- a/core/src/class.rs +++ b/core/src/class.rs @@ -54,7 +54,7 @@ pub trait JsClass<'js>: Trace<'js> + JsLifetime<'js> + Sized { } /// The function which will be called if a get property is performed on an object with this class - fn exotic_get_property<'a>( + fn exotic_get_property( this: &JsCell<'js, Self>, ctx: &Ctx<'js>, _atom: Atom<'js>, @@ -66,7 +66,7 @@ pub trait JsClass<'js>: Trace<'js> + JsLifetime<'js> + Sized { } /// The function which will be called if a set property is performed on an object with this class - fn exotic_set_property<'a>( + fn exotic_set_property( this: &JsCell<'js, Self>, _ctx: &Ctx<'js>, _atom: Atom<'js>, @@ -79,7 +79,7 @@ pub trait JsClass<'js>: Trace<'js> + JsLifetime<'js> + Sized { } /// The function which will be called if a delete property is performed on an object with this class - fn exotic_delete_property<'a>( + fn exotic_delete_property( this: &JsCell<'js, Self>, _ctx: &Ctx<'js>, _atom: Atom<'js>, @@ -90,7 +90,7 @@ pub trait JsClass<'js>: Trace<'js> + JsLifetime<'js> + Sized { } /// The function which will be called if has property or similar is called on an object with this class - fn exotic_has_property<'a>( + fn exotic_has_property( this: &JsCell<'js, Self>, _ctx: &Ctx<'js>, _atom: Atom<'js>, @@ -766,7 +766,7 @@ mod test { Ok(None) } - fn exotic_get_property<'a>( + fn exotic_get_property( this: &crate::class::JsCell<'js, Self>, ctx: &crate::Ctx<'js>, atom: crate::Atom<'js>, @@ -831,7 +831,7 @@ mod test { } } - fn exotic_has_property<'a>( + fn exotic_has_property( this: &super::JsCell<'js, Self>, _ctx: &crate::Ctx<'js>, atom: crate::Atom<'js>, @@ -876,7 +876,7 @@ mod test { Ok(None) } - fn exotic_get_property<'a>( + fn exotic_get_property( this: &crate::class::JsCell<'js, Self>, ctx: &crate::Ctx<'js>, atom: crate::Atom<'js>, @@ -915,7 +915,7 @@ mod test { } } - fn exotic_set_property<'a>( + fn exotic_set_property( this: &super::JsCell<'js, Self>, ctx: &crate::Ctx<'js>, atom: crate::Atom<'js>, @@ -938,7 +938,7 @@ mod test { Err(ctx.throw(err_val)) } - fn exotic_has_property<'a>( + fn exotic_has_property( this: &super::JsCell<'js, Self>, _ctx: &crate::Ctx<'js>, atom: crate::Atom<'js>, @@ -956,7 +956,7 @@ mod test { Ok(false) } - fn exotic_delete_property<'a>( + fn exotic_delete_property( _this: &super::JsCell<'js, Self>, ctx: &crate::Ctx<'js>, _atom: crate::Atom<'js>, From 34fbfb91b8c77da5cc322ed288b9bfcf7e9500e0 Mon Sep 17 00:00:00 2001 From: Emile Fugulin Date: Wed, 3 Dec 2025 18:36:10 -0500 Subject: [PATCH 08/14] Add holder for exotic methods ffi --- core/src/runtime.rs | 1 + core/src/runtime/exotic.rs | 30 ++++++++++++++++++++++++++++++ core/src/runtime/opaque.rs | 20 +++++--------------- 3 files changed, 36 insertions(+), 15 deletions(-) create mode 100644 core/src/runtime/exotic.rs diff --git a/core/src/runtime.rs b/core/src/runtime.rs index dabe4dacf..dd09a0613 100644 --- a/core/src/runtime.rs +++ b/core/src/runtime.rs @@ -1,6 +1,7 @@ //! QuickJS runtime related types. mod base; +mod exotic; pub(crate) mod opaque; pub(crate) mod raw; mod userdata; diff --git a/core/src/runtime/exotic.rs b/core/src/runtime/exotic.rs new file mode 100644 index 000000000..919d7829b --- /dev/null +++ b/core/src/runtime/exotic.rs @@ -0,0 +1,30 @@ +use crate::qjs; +use alloc::boxed::Box; + +#[derive(Debug)] +#[repr(transparent)] +pub(crate) struct ExoticMethodsHolder(*mut qjs::JSClassExoticMethods); + +impl ExoticMethodsHolder { + pub fn new() -> Self { + Self(Box::into_raw(Box::new(qjs::JSClassExoticMethods { + get_own_property: None, // TODO: Implement + get_own_property_names: None, // TODO: Implement + delete_property: Some(crate::class::ffi::exotic_delete_property), + define_own_property: None, // TODO: Implement + has_property: Some(crate::class::ffi::exotic_has_property), + set_property: Some(crate::class::ffi::exotic_set_property), + get_property: Some(crate::class::ffi::exotic_get_property), + }))) + } + + pub(crate) fn as_ptr(&self) -> *mut qjs::JSClassExoticMethods { + self.0 + } +} + +impl Drop for ExoticMethodsHolder { + fn drop(&mut self) { + let _ = unsafe { Box::from_raw(self.0) }; + } +} diff --git a/core/src/runtime/opaque.rs b/core/src/runtime/opaque.rs index fc932c534..d14f83b64 100644 --- a/core/src/runtime/opaque.rs +++ b/core/src/runtime/opaque.rs @@ -4,6 +4,7 @@ use crate::{ }; use super::{ + exotic::ExoticMethodsHolder, userdata::{UserDataGuard, UserDataMap}, InterruptHandler, PromiseHook, PromiseHookType, RejectionTracker, UserDataError, }; @@ -58,23 +59,13 @@ pub(crate) struct Opaque<'js> { #[cfg(feature = "futures")] spawner: Option>, - exotic_methods: *mut qjs::JSClassExoticMethods, + exotic_methods: ExoticMethodsHolder, _marker: PhantomData<&'js ()>, } impl<'js> Opaque<'js> { pub fn new() -> Self { - let exotic_methods = Box::into_raw(Box::new(qjs::JSClassExoticMethods { - get_own_property: None, // TODO: Implement - get_own_property_names: None, // TODO: Implement - delete_property: Some(crate::class::ffi::exotic_delete_property), - define_own_property: None, // TODO: Implement - has_property: Some(crate::class::ffi::exotic_has_property), - set_property: Some(crate::class::ffi::exotic_set_property), - get_property: Some(crate::class::ffi::exotic_get_property), - })); - Opaque { panic: Cell::new(None), @@ -97,7 +88,7 @@ impl<'js> Opaque<'js> { #[cfg(feature = "futures")] spawner: None, - exotic_methods, + exotic_methods: ExoticMethodsHolder::new(), } } @@ -118,7 +109,7 @@ impl<'js> Opaque<'js> { finalizer: Some(class::ffi::class_finalizer), gc_mark: Some(class::ffi::class_trace), call: None, - exotic: std::ptr::null_mut(), + exotic: ptr::null_mut(), }; if 0 != qjs::JS_NewClass(rt, self.class_id, &class_def) { @@ -142,7 +133,7 @@ impl<'js> Opaque<'js> { finalizer: Some(class::ffi::exotic_class_finalizer), gc_mark: Some(class::ffi::exotic_class_trace), call: None, - exotic: self.exotic_methods, + exotic: self.exotic_methods.as_ptr(), }; if 0 != qjs::JS_NewClass(rt, self.exotic_class_id, &class_def) { @@ -298,6 +289,5 @@ impl<'js> Opaque<'js> { #[cfg(feature = "futures")] self.spawner.take(); self.userdata.clear(); - unsafe { drop(Box::from_raw(self.exotic_methods)) }; } } From 0891ef85f69791aa3d69732621eac30287ae7e7e Mon Sep 17 00:00:00 2001 From: Emile Fugulin Date: Wed, 3 Dec 2025 18:36:48 -0500 Subject: [PATCH 09/14] Warn user when trying to use CALLABLE and EXOTIC at the same time --- core/src/class.rs | 62 ++++++++++++++++++---------------------------- core/src/result.rs | 11 ++++++++ 2 files changed, 35 insertions(+), 38 deletions(-) diff --git a/core/src/class.rs b/core/src/class.rs index 3c0154ff8..3ba298943 100644 --- a/core/src/class.rs +++ b/core/src/class.rs @@ -7,6 +7,7 @@ use crate::{ Atom, Ctx, Error, FromJs, IntoJs, JsLifetime, Object, Result, Value, }; use alloc::boxed::Box; +use alloc::string::ToString as _; use core::{hash::Hash, marker::PhantomData, mem, ops::Deref, ptr::NonNull}; mod cell; @@ -144,15 +145,7 @@ impl<'js, C: JsClass<'js>> Deref for Class<'js, C> { impl<'js, C: JsClass<'js>> Class<'js, C> { /// Create a class from a Rust object. pub fn instance(ctx: Ctx<'js>, value: C) -> Result> { - let id = unsafe { - if C::CALLABLE { - ctx.get_opaque().get_callable_id() - } else if C::EXOTIC { - ctx.get_opaque().get_exotic_id() - } else { - ctx.get_opaque().get_class_id() - } - }; + let id = unsafe { class_id::(&ctx)? }; let prototype = Self::prototype(&ctx)?; @@ -171,15 +164,7 @@ impl<'js, C: JsClass<'js>> Class<'js, C> { /// Create a class from a Rust object with a given prototype. pub fn instance_proto(value: C, proto: Object<'js>) -> Result> { - let id = unsafe { - if C::CALLABLE { - proto.ctx().get_opaque().get_callable_id() - } else if C::EXOTIC { - proto.ctx().get_opaque().get_exotic_id() - } else { - proto.ctx().get_opaque().get_class_id() - } - }; + let id = unsafe { class_id::(proto.ctx())? }; let val = unsafe { proto.ctx.handle_exception(qjs::JS_NewObjectProtoClass( @@ -279,15 +264,7 @@ impl<'js, C: JsClass<'js>> Class<'js, C> { /// returns a pointer to the class object. #[inline] pub(crate) fn get_class_ptr(&self) -> NonNull>> { - let id = unsafe { - if C::CALLABLE { - self.ctx.get_opaque().get_callable_id() - } else if C::EXOTIC { - self.ctx.get_opaque().get_exotic_id() - } else { - self.ctx.get_opaque().get_class_id() - } - }; + let id = unsafe { class_id::(&self.ctx).expect("invalid class") }; let ptr = unsafe { qjs::JS_GetOpaque2(self.0.ctx.as_ptr(), self.0 .0.as_js_value(), id) }; @@ -335,14 +312,8 @@ impl<'js, C: JsClass<'js>> Class<'js, C> { impl<'js> Object<'js> { /// Returns if the object is of a certain Rust class. pub fn instance_of>(&self) -> bool { - let id = unsafe { - if C::CALLABLE { - self.ctx.get_opaque().get_callable_id() - } else if C::EXOTIC { - self.ctx.get_opaque().get_exotic_id() - } else { - self.ctx.get_opaque().get_class_id() - } + let Ok(id) = (unsafe { class_id::(&self.ctx) }) else { + return false; }; // This checks if the class is of the right class id. @@ -400,6 +371,21 @@ impl<'js, C: JsClass<'js>> IntoJs<'js> for Class<'js, C> { } } +unsafe fn class_id<'js, C: JsClass<'js>>(ctx: &Ctx<'js>) -> Result { + if C::CALLABLE && C::EXOTIC { + Err(Error::InvalidClass { + class: C::NAME, + message: "a class cannot be both callable and exotic".to_string(), + }) + } else if C::CALLABLE { + Ok(ctx.get_opaque().get_callable_id()) + } else if C::EXOTIC { + Ok(ctx.get_opaque().get_exotic_id()) + } else { + Ok(ctx.get_opaque().get_class_id()) + } +} + #[cfg(test)] mod test { use core::sync::atomic::AtomicI32; @@ -792,7 +778,7 @@ mod test { )? .into_value(); - return Ok::, crate::Error>(val); + Ok::, crate::Error>(val) } else if state.load(Ordering::SeqCst) == 2 { state.fetch_add(1, Ordering::SeqCst); @@ -809,7 +795,7 @@ mod test { )? .into_value(); - return Ok(val); + Ok(val) } else { state.fetch_add(1, Ordering::SeqCst); @@ -822,7 +808,7 @@ mod test { )? .into_value(); - return Ok(val); + Ok(val) } })? .into_value()) diff --git a/core/src/result.rs b/core/src/result.rs index 96ee2c3ae..c76ee725e 100644 --- a/core/src/result.rs +++ b/core/src/result.rs @@ -68,6 +68,11 @@ pub enum Error { DuplicateExports, /// Tried to export a entry which was not previously declared. InvalidExport, + /// Tried to use or create an invalid class. + InvalidClass { + class: &'static str, + message: StdString, + }, /// Found a string with a internal null byte while converting /// to C string. InvalidString(NulError), @@ -365,6 +370,12 @@ impl Display for Error { Error::InvalidExport => { "Tried to export a value which was not previously declared".fmt(f)? } + Error::InvalidClass { class, message } => { + "Invalid class '".fmt(f)?; + class.fmt(f)?; + "': ".fmt(f)?; + message.fmt(f)?; + } Error::InvalidString(error) => { "String contained internal null bytes: ".fmt(f)?; error.fmt(f)?; From 6f3baed2887b1a9f6ba825374e6c7703ae868eb4 Mon Sep 17 00:00:00 2001 From: Emile Fugulin Date: Wed, 3 Dec 2025 20:37:34 -0500 Subject: [PATCH 10/14] Remove unused obj We already have this --- core/src/class.rs | 10 ---------- core/src/class/ffi.rs | 21 ++++++++------------- 2 files changed, 8 insertions(+), 23 deletions(-) diff --git a/core/src/class.rs b/core/src/class.rs index 3ba298943..77951a3c8 100644 --- a/core/src/class.rs +++ b/core/src/class.rs @@ -59,7 +59,6 @@ pub trait JsClass<'js>: Trace<'js> + JsLifetime<'js> + Sized { this: &JsCell<'js, Self>, ctx: &Ctx<'js>, _atom: Atom<'js>, - _obj: Value<'js>, _receiver: Value<'js>, ) -> Result> { let _ = this; @@ -71,7 +70,6 @@ pub trait JsClass<'js>: Trace<'js> + JsLifetime<'js> + Sized { this: &JsCell<'js, Self>, _ctx: &Ctx<'js>, _atom: Atom<'js>, - _obj: Value<'js>, _receiver: Value<'js>, _value: Value<'js>, ) -> Result { @@ -84,7 +82,6 @@ pub trait JsClass<'js>: Trace<'js> + JsLifetime<'js> + Sized { this: &JsCell<'js, Self>, _ctx: &Ctx<'js>, _atom: Atom<'js>, - _obj: Value<'js>, ) -> Result { let _ = this; Ok(false) @@ -95,7 +92,6 @@ pub trait JsClass<'js>: Trace<'js> + JsLifetime<'js> + Sized { this: &JsCell<'js, Self>, _ctx: &Ctx<'js>, _atom: Atom<'js>, - _obj: Value<'js>, ) -> Result { let _ = this; Ok(false) @@ -756,7 +752,6 @@ mod test { this: &crate::class::JsCell<'js, Self>, ctx: &crate::Ctx<'js>, atom: crate::Atom<'js>, - _obj: crate::Value<'js>, _receiver: crate::Value<'js>, ) -> crate::Result> { println!("Get property [iter]: {}", atom.to_string()?); @@ -821,7 +816,6 @@ mod test { this: &super::JsCell<'js, Self>, _ctx: &crate::Ctx<'js>, atom: crate::Atom<'js>, - _obj: crate::Value<'js>, ) -> crate::Result { let _ = this; if atom.to_string()? == "next" { @@ -866,7 +860,6 @@ mod test { this: &crate::class::JsCell<'js, Self>, ctx: &crate::Ctx<'js>, atom: crate::Atom<'js>, - _obj: crate::Value<'js>, _receiver: crate::Value<'js>, ) -> crate::Result> { let symbol_iterator = crate::Atom::from_predefined( @@ -905,7 +898,6 @@ mod test { this: &super::JsCell<'js, Self>, ctx: &crate::Ctx<'js>, atom: crate::Atom<'js>, - _obj: crate::Value<'js>, _receiver: crate::Value<'js>, _value: crate::Value<'js>, ) -> crate::Result { @@ -928,7 +920,6 @@ mod test { this: &super::JsCell<'js, Self>, _ctx: &crate::Ctx<'js>, atom: crate::Atom<'js>, - _obj: crate::Value<'js>, ) -> crate::Result { let _ = this; println!("Got atom: {}", atom.to_string()?); @@ -946,7 +937,6 @@ mod test { _this: &super::JsCell<'js, Self>, ctx: &crate::Ctx<'js>, _atom: crate::Atom<'js>, - _obj: crate::Value<'js>, ) -> crate::Result { let err_val = crate::String::from_str(ctx.clone(), "Properties cannot be deleted")? .into_value(); diff --git a/core/src/class/ffi.rs b/core/src/class/ffi.rs index 815560720..79b5196f8 100644 --- a/core/src/class/ffi.rs +++ b/core/src/class/ffi.rs @@ -235,18 +235,17 @@ impl VTable { unsafe fn get_property_impl<'js, C: JsClass<'js>>( this_ptr: NonNull>, ctx: *mut qjs::JSContext, - obj: qjs::JSValueConst, + _obj: qjs::JSValueConst, atom: qjs::JSAtom, receiver: qjs::JSValueConst, ) -> qjs::JSValue { let this_ptr = this_ptr.cast::>>(); let ctx = Ctx::from_ptr(ctx); let atom = Atom::from_atom_val_dup(ctx.clone(), atom); - let obj = Value::from_js_value_const(ctx.clone(), obj); let receiver = Value::from_js_value_const(ctx.clone(), receiver); ctx.handle_panic(AssertUnwindSafe(|| { - C::exotic_get_property(&this_ptr.as_ref().data, &ctx, atom, obj, receiver) + C::exotic_get_property(&this_ptr.as_ref().data, &ctx, atom, receiver) .map(Value::into_js_value) .unwrap_or_else(|e| e.throw(&ctx)) })) @@ -255,7 +254,7 @@ impl VTable { unsafe fn set_property_impl<'js, C: JsClass<'js>>( this_ptr: NonNull>, ctx: *mut qjs::JSContext, - obj: qjs::JSValueConst, + _obj: qjs::JSValueConst, atom: qjs::JSAtom, receiver: qjs::JSValueConst, value: qjs::JSValue, @@ -264,13 +263,11 @@ impl VTable { let this_ptr = this_ptr.cast::>>(); let ctx = Ctx::from_ptr(ctx); let atom = Atom::from_atom_val_dup(ctx.clone(), atom); - let obj = Value::from_js_value_const(ctx.clone(), obj); let receiver = Value::from_js_value_const(ctx.clone(), receiver); let value = Value::from_js_value(ctx.clone(), value); ctx.handle_panic_exotic(AssertUnwindSafe(|| { - match C::exotic_set_property(&this_ptr.as_ref().data, &ctx, atom, obj, receiver, value) - { + match C::exotic_set_property(&this_ptr.as_ref().data, &ctx, atom, receiver, value) { Ok(v) => { if v { 1 @@ -289,16 +286,15 @@ impl VTable { unsafe fn has_property_impl<'js, C: JsClass<'js>>( this_ptr: NonNull>, ctx: *mut qjs::JSContext, - obj: qjs::JSValueConst, + _obj: qjs::JSValueConst, atom: qjs::JSAtom, ) -> qjs::c_int { let this_ptr = this_ptr.cast::>>(); let ctx = Ctx::from_ptr(ctx); let atom = Atom::from_atom_val_dup(ctx.clone(), atom); - let obj = Value::from_js_value_const(ctx.clone(), obj); ctx.handle_panic_exotic(AssertUnwindSafe(|| { - match C::exotic_has_property(&this_ptr.as_ref().data, &ctx, atom, obj) { + match C::exotic_has_property(&this_ptr.as_ref().data, &ctx, atom) { Ok(v) => { if v { 1 @@ -317,16 +313,15 @@ impl VTable { unsafe fn delete_property_impl<'js, C: JsClass<'js>>( this_ptr: NonNull>, ctx: *mut qjs::JSContext, - obj: qjs::JSValueConst, + _obj: qjs::JSValueConst, atom: qjs::JSAtom, ) -> qjs::c_int { let this_ptr = this_ptr.cast::>>(); let ctx = Ctx::from_ptr(ctx); let atom = Atom::from_atom_val_dup(ctx.clone(), atom); - let obj = Value::from_js_value_const(ctx.clone(), obj); ctx.handle_panic_exotic(AssertUnwindSafe(|| { - match C::exotic_delete_property(&this_ptr.as_ref().data, &ctx, atom, obj) { + match C::exotic_delete_property(&this_ptr.as_ref().data, &ctx, atom) { Ok(v) => { if v { 1 From dddf8ba3d70f4999305eee316ad6df871c8b2672 Mon Sep 17 00:00:00 2001 From: Emile Fugulin Date: Wed, 3 Dec 2025 20:43:50 -0500 Subject: [PATCH 11/14] Add exotic macro --- macro/src/class.rs | 58 ++++++ macro/src/common.rs | 3 + macro/src/exotic.rs | 410 ++++++++++++++++++++++++++++++++++++++++++ macro/src/function.rs | 10 +- macro/src/lib.rs | 74 ++++++++ src/lib.rs | 2 +- 6 files changed, 551 insertions(+), 6 deletions(-) create mode 100644 macro/src/exotic.rs diff --git a/macro/src/class.rs b/macro/src/class.rs index d2a6c1da0..30291043a 100644 --- a/macro/src/class.rs +++ b/macro/src/class.rs @@ -17,6 +17,7 @@ use crate::{ #[derive(Debug, Default, Clone)] pub(crate) struct ClassConfig { pub frozen: bool, + pub exotic: bool, pub crate_: Option, pub rename: Option, pub rename_all: Option, @@ -24,6 +25,7 @@ pub(crate) struct ClassConfig { pub(crate) enum ClassOption { Frozen(FlagOption), + Exotic(FlagOption), Crate(ValueOption), Rename(ValueOption), RenameAll(ValueOption), @@ -33,6 +35,8 @@ impl Parse for ClassOption { fn parse(input: ParseStream) -> syn::Result { if input.peek(kw::frozen) { input.parse().map(Self::Frozen) + } else if input.peek(kw::exotic) { + input.parse().map(Self::Exotic) } else if input.peek(Token![crate]) { input.parse().map(Self::Crate) } else if input.peek(kw::rename) { @@ -51,6 +55,9 @@ impl ClassConfig { ClassOption::Frozen(ref x) => { self.frozen = x.is_true(); } + ClassOption::Exotic(ref x) => { + self.exotic = x.is_true(); + } ClassOption::Crate(ref x) => { self.crate_ = Some(x.value.value()); } @@ -345,6 +352,53 @@ impl Class { let mutability = self.mutability(); let props = self.expand_props(&crate_name); let reexpand = self.reexpand(); + let exotic_const = if self.config().exotic { + quote! { const EXOTIC: bool = true; } + } else { + TokenStream::new() + }; + + let exotic_methods = if self.config().exotic { + let exotic_module = format_ident!("__impl_exotic_{}__", self.ident()); + quote! { + fn exotic_get_property( + this: &#crate_name::class::JsCell<'js, Self>, + ctx: &#crate_name::Ctx<'js>, + atom: #crate_name::Atom<'js>, + receiver: #crate_name::Value<'js>, + ) -> #crate_name::Result<#crate_name::Value<'js>> { + #exotic_module::ExoticImpl::exotic_get_property(this, ctx, atom, receiver) + } + + fn exotic_set_property( + this: &#crate_name::class::JsCell<'js, Self>, + ctx: &#crate_name::Ctx<'js>, + atom: #crate_name::Atom<'js>, + receiver: #crate_name::Value<'js>, + value: #crate_name::Value<'js>, + ) -> #crate_name::Result { + #exotic_module::ExoticImpl::exotic_set_property(this, ctx, atom, receiver, value) + } + + fn exotic_delete_property( + this: &#crate_name::class::JsCell<'js, Self>, + ctx: &#crate_name::Ctx<'js>, + atom: #crate_name::Atom<'js>, + ) -> #crate_name::Result { + #exotic_module::ExoticImpl::exotic_delete_property(this, ctx, atom) + } + + fn exotic_has_property( + this: &#crate_name::class::JsCell<'js, Self>, + ctx: &#crate_name::Ctx<'js>, + atom: #crate_name::Atom<'js>, + ) -> #crate_name::Result { + #exotic_module::ExoticImpl::exotic_has_property(this, ctx, atom) + } + } + } else { + TokenStream::new() + }; let res = quote! { #reexpand @@ -358,6 +412,8 @@ impl Class { type Mutable = #crate_name::class::#mutability; + #exotic_const + fn prototype(ctx: &#crate_name::Ctx<'js>) -> #crate_name::Result>>{ use #crate_name::class::impl_::MethodImplementor; @@ -374,6 +430,8 @@ impl Class { let implementor = #crate_name::class::impl_::ConstructorCreate::::new(); (&implementor).create_constructor(ctx) } + + #exotic_methods } impl #generics_with_lifetimes #crate_name::IntoJs<'js> for #class_name #generics{ diff --git a/macro/src/common.rs b/macro/src/common.rs index 7e18624f3..ab472e9d6 100644 --- a/macro/src/common.rs +++ b/macro/src/common.rs @@ -139,4 +139,7 @@ pub(crate) mod kw { syn::custom_keyword!(prefix); syn::custom_keyword!(declare); syn::custom_keyword!(evaluate); + syn::custom_keyword!(exotic); + syn::custom_keyword!(delete); + syn::custom_keyword!(has); } diff --git a/macro/src/exotic.rs b/macro/src/exotic.rs new file mode 100644 index 000000000..b27187930 --- /dev/null +++ b/macro/src/exotic.rs @@ -0,0 +1,410 @@ +use proc_macro2::{Ident, Span, TokenStream}; +use quote::{format_ident, quote}; +use syn::{ + parse::{Parse, ParseStream}, + spanned::Spanned, + Attribute, Block, Error, ImplItemFn, ItemImpl, Result, ReturnType, Signature, Type, Visibility, +}; + +use crate::{ + attrs::{take_attributes, FlagOption, OptionList}, + common::{crate_ident, kw}, + function::JsFunction, +}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum ExoticMethodKind { + Get, + Set, + Delete, + Has, +} + +impl ExoticMethodKind { + fn trait_method_name(&self) -> &'static str { + match self { + Self::Get => "exotic_get_property", + Self::Set => "exotic_set_property", + Self::Delete => "exotic_delete_property", + Self::Has => "exotic_has_property", + } + } +} + +#[derive(Default)] +struct ExoticMethodConfig { + kind: Option, +} + +enum ExoticMethodOption { + Get(FlagOption), + Set(FlagOption), + Delete(FlagOption), + Has(FlagOption), +} + +impl Parse for ExoticMethodOption { + fn parse(input: ParseStream) -> syn::Result { + if input.peek(kw::get) { + input.parse().map(Self::Get) + } else if input.peek(kw::set) { + input.parse().map(Self::Set) + } else if input.peek(kw::delete) { + input.parse().map(Self::Delete) + } else if input.peek(kw::has) { + input.parse().map(Self::Has) + } else { + Err(syn::Error::new( + input.span(), + "invalid exotic method attribute, expected one of: get, set, delete, has", + )) + } + } +} + +impl ExoticMethodConfig { + fn apply(&mut self, option: &ExoticMethodOption, span: Span) -> Result<()> { + let (new_kind, option_name) = match option { + ExoticMethodOption::Get(x) if x.is_true() => (ExoticMethodKind::Get, "get"), + ExoticMethodOption::Set(x) if x.is_true() => (ExoticMethodKind::Set, "set"), + ExoticMethodOption::Delete(x) if x.is_true() => (ExoticMethodKind::Delete, "delete"), + ExoticMethodOption::Has(x) if x.is_true() => (ExoticMethodKind::Has, "has"), + _ => return Ok(()), // Flag is false, ignore + }; + + if let Some(existing_kind) = self.kind { + let error = Error::new( + span, + format!( + "exotic method cannot have multiple attributes (found '{}' but already have '{}')", + option_name, + match existing_kind { + ExoticMethodKind::Get => "get", + ExoticMethodKind::Set => "set", + ExoticMethodKind::Delete => "delete", + ExoticMethodKind::Has => "has", + } + ), + ); + return Err(error); + } + + self.kind = Some(new_kind); + Ok(()) + } +} + +struct ExoticMethod { + kind: ExoticMethodKind, + function: JsFunction, + attrs: Vec, + vis: Visibility, + sig: Signature, + block: Block, + has_ctx: bool, + returns_result: bool, +} + +impl ExoticMethod { + fn parse(func: ImplItemFn, self_ty: &Type) -> Result> { + let ImplItemFn { + mut attrs, + vis, + sig, + block, + .. + } = func; + + let mut config = ExoticMethodConfig::default(); + + take_attributes(&mut attrs, |attr| { + if !attr.path().is_ident("qjs") { + return Ok(false); + } + + let attr_span = attr.span(); + let options = attr.parse_args::>()?; + for option in options.0.iter() { + config.apply(option, attr_span)?; + } + Ok(true) + })?; + + // If no exotic attributes, this is not an exotic method + let Some(kind) = config.kind else { + return Ok(None); + }; + + let function = JsFunction::new(vis.clone(), &sig, Some(self_ty))?; + + if function.params.params.is_empty() || !function.params.params[0].is_this { + return Err(Error::new( + sig.span(), + "Exotic methods must have a self receiver", + )); + } + + let has_ctx = if function.params.params.len() > 1 { + // Check if the second parameter is a Ctx + // We need to look at the original signature for this + sig.inputs.iter().nth(1).is_some_and(|arg| { + if let syn::FnArg::Typed(pat_type) = arg { + if let Type::Path(type_path) = &*pat_type.ty { + type_path + .path + .segments + .last() + .is_some_and(|seg| seg.ident == "Ctx") + } else { + false + } + } else { + false + } + }) + } else { + false + }; + + // Check return type + let returns_result = match &sig.output { + ReturnType::Default => false, + ReturnType::Type(_, ty) => { + if let Type::Path(type_path) = &**ty { + type_path + .path + .segments + .first() + .is_some_and(|seg| seg.ident == "Result") + } else { + false + } + } + }; + + Ok(Some(ExoticMethod { + kind, + function, + attrs, + vis, + sig, + block, + has_ctx, + returns_result, + })) + } + + fn expand_wrapper(&self, crate_name: &Ident, self_ty: &Type) -> TokenStream { + let method_name = &self.function.name; + let trait_method = format_ident!("{}", self.kind.trait_method_name()); + + // Use JsFunction's params to determine if we need borrow or borrow_mut + let borrow_call = if let Some(first_param) = self.function.params.params.first() { + match first_param.kind { + crate::function::ParamKind::BorrowMut => quote! { this.borrow_mut() }, + _ => quote! { this.borrow() }, + } + } else { + quote! { this.borrow() } + }; + + let (params, args, return_type, result_conversion) = match self.kind { + ExoticMethodKind::Get => { + let params = quote! { ctx: &#crate_name::Ctx<'js>, atom: #crate_name::Atom<'js>, _receiver: #crate_name::Value<'js> }; + let args = if self.has_ctx { + quote! { ctx, atom } + } else { + quote! { atom } + }; + let conversion = if self.returns_result { + quote! { result.and_then(|r| #crate_name::IntoJs::into_js(r, ctx)) } + } else { + quote! { #crate_name::IntoJs::into_js(result, ctx) } + }; + (params, args, quote! { #crate_name::Value<'js> }, conversion) + } + ExoticMethodKind::Set => { + let params = quote! { ctx: &#crate_name::Ctx<'js>, atom: #crate_name::Atom<'js>, _receiver: #crate_name::Value<'js>, value: #crate_name::Value<'js> }; + let args = if self.has_ctx { + quote! { ctx, atom, value } + } else { + quote! { atom, value } + }; + let conversion = if self.returns_result { + quote! { result } + } else { + quote! { Ok(result) } + }; + (params, args, quote! { bool }, conversion) + } + ExoticMethodKind::Delete | ExoticMethodKind::Has => { + let params = quote! { ctx: &#crate_name::Ctx<'js>, atom: #crate_name::Atom<'js> }; + let args = if self.has_ctx { + quote! { ctx, atom } + } else { + quote! { atom } + }; + let conversion = if self.returns_result { + quote! { result } + } else { + quote! { Ok(result) } + }; + (params, args, quote! { bool }, conversion) + } + }; + + quote! { + pub fn #trait_method<'js>( + this: &#crate_name::class::JsCell<'js, #self_ty>, + #params + ) -> #crate_name::Result<#return_type> { + let result = #borrow_call.#method_name(#args); + #result_conversion + } + } + } + + fn expand_impl(&self) -> TokenStream { + let attrs = &self.attrs; + let vis = &self.vis; + let sig = &self.sig; + let block = &self.block; + + quote! { + #(#attrs)* #vis #sig #block + } + } +} + +fn get_class_name(ty: &Type) -> String { + match ty { + Type::Path(x) => x.path.segments.first().unwrap().ident.to_string(), + Type::Paren(x) => get_class_name(&x.elem), + _ => "Unknown".to_string(), + } +} + +pub(crate) fn expand(item: ItemImpl) -> Result { + let ItemImpl { + attrs, + generics, + self_ty, + items, + .. + } = item; + + let crate_name = format_ident!("{}", crate_ident()?); + let class_name = get_class_name(&self_ty); + let module_name = format_ident!("__impl_exotic_{}__", class_name); + + let mut methods = Vec::new(); + let mut user_impls = Vec::new(); + + for item in items { + if let syn::ImplItem::Fn(func) = item { + if let Some(method) = ExoticMethod::parse(func, &self_ty)? { + user_impls.push(method.expand_impl()); + methods.push(method); + } + } + } + + // Generate wrappers for user-provided methods + let user_wrappers: Vec<_> = methods + .iter() + .map(|m| m.expand_wrapper(&crate_name, &self_ty)) + .collect(); + + // Generate default implementations for missing methods + let has_get = methods.iter().any(|m| m.kind == ExoticMethodKind::Get); + let has_set = methods.iter().any(|m| m.kind == ExoticMethodKind::Set); + let has_delete = methods.iter().any(|m| m.kind == ExoticMethodKind::Delete); + let has_has = methods.iter().any(|m| m.kind == ExoticMethodKind::Has); + + let default_get = if !has_get { + quote! { + pub fn exotic_get_property<'js>( + this: &#crate_name::class::JsCell<'js, #self_ty>, + ctx: &#crate_name::Ctx<'js>, + _atom: #crate_name::Atom<'js>, + _receiver: #crate_name::Value<'js>, + ) -> #crate_name::Result<#crate_name::Value<'js>> { + let _ = this; + Ok(#crate_name::Value::new_undefined(ctx.clone())) + } + } + } else { + TokenStream::new() + }; + + let default_set = if !has_set { + quote! { + pub fn exotic_set_property<'js>( + this: &#crate_name::class::JsCell<'js, #self_ty>, + _ctx: &#crate_name::Ctx<'js>, + _atom: #crate_name::Atom<'js>, + _receiver: #crate_name::Value<'js>, + _value: #crate_name::Value<'js>, + ) -> #crate_name::Result { + let _ = this; + Ok(false) + } + } + } else { + TokenStream::new() + }; + + let default_delete = if !has_delete { + quote! { + pub fn exotic_delete_property<'js>( + this: &#crate_name::class::JsCell<'js, #self_ty>, + _ctx: &#crate_name::Ctx<'js>, + _atom: #crate_name::Atom<'js>, + ) -> #crate_name::Result { + let _ = this; + Ok(false) + } + } + } else { + TokenStream::new() + }; + + let default_has = if !has_has { + quote! { + pub fn exotic_has_property<'js>( + this: &#crate_name::class::JsCell<'js, #self_ty>, + _ctx: &#crate_name::Ctx<'js>, + _atom: #crate_name::Atom<'js>, + ) -> #crate_name::Result { + let _ = this; + Ok(false) + } + } + } else { + TokenStream::new() + }; + + let res = quote! { + #(#attrs)* + impl #generics #self_ty { + #(#user_impls)* + } + + #[allow(non_snake_case)] + mod #module_name { + pub use super::*; + + pub(crate) struct ExoticImpl; + + impl ExoticImpl { + #(#user_wrappers)* + #default_get + #default_set + #default_delete + #default_has + } + } + }; + + Ok(res) +} diff --git a/macro/src/function.rs b/macro/src/function.rs index d7fdacf85..10c66690a 100644 --- a/macro/src/function.rs +++ b/macro/src/function.rs @@ -289,7 +289,7 @@ impl JsParams { } #[derive(Clone, Copy, Debug)] -pub(crate) enum ParamKind { +pub enum ParamKind { Value, Borrow, BorrowMut, @@ -297,10 +297,10 @@ pub(crate) enum ParamKind { #[derive(Debug, Clone)] pub(crate) struct JsParam { - kind: ParamKind, - number: usize, - tokens: TokenStream, - is_this: bool, + pub kind: ParamKind, + pub number: usize, + pub tokens: TokenStream, + pub is_this: bool, } impl JsParam { diff --git a/macro/src/lib.rs b/macro/src/lib.rs index 37d66e530..919a74d82 100644 --- a/macro/src/lib.rs +++ b/macro/src/lib.rs @@ -23,6 +23,7 @@ mod attrs; mod class; mod common; mod embed; +mod exotic; mod fields; mod function; mod js_lifetime; @@ -46,6 +47,7 @@ mod trace; /// | `rename` | String | Changes the name of the implemented class on the JavaScript side. | /// | `rename_all` | Casing | Converts the case of all the fields of this struct which have implement accessors. Can be one of `lowercase`, `UPPERCASE`, `camelCase`, `PascalCase`,`snake_case`, or `SCREAMING_SNAKE` | /// | `frozen` | Flag | Changes the class implementation to only allow borrowing immutably. Trying to borrow mutably will result in an error. | +/// | `exotic` | Flag | Changes the class implementation to support exotic methods. Must be used in combination with the macro [`macro@exotic`]. | /// /// # Field options /// @@ -308,6 +310,78 @@ pub fn methods(attr: TokenStream1, item: TokenStream1) -> TokenStream1 { } } +/// An attribute for implementing exotic methods for a class. +/// +/// This attribute can be added to a impl block which implements methods for a type which uses the +/// [`macro@class`] attribute to derive [`JsClass`](rquickjs_core::class::JsClass) with the `exotic` +/// +/// # Limitations +/// Due to limitations in the Rust type system this attribute can be used on only one impl block +/// per type. +/// +/// # Item options +/// +/// Each item of the impl block must be tagged with an attribute to specify the exotic method it implements. +/// These attributes are all in the form of `#[qjs(option)]`. +/// +/// | **Option** | **Value** | **Description** | +/// |------------|-----------|-----------------| +/// | `get` | Flag | Makes this method the [[Get]] exotic method | +/// | `set` | Flag | Makes this method the [[Set]] exotic method | +/// | `delete` | Flag | Makes this method the [[Delete]] exotic method | +/// | `has` | Flag | Makes this method the [[HasProperty]] exotic method | +/// +/// # Example +/// ``` +/// use rquickjs::{class::Trace, JsLifetime, Context, Runtime}; +/// +/// #[derive(Trace, JsLifetime)] +/// #[rquickjs::class(exotic)] +/// pub struct TestClass { +/// value: u32, +/// } +/// +/// #[rquickjs::exotic] +/// impl TestClass { +/// #[qjs(get)] +/// pub fn value(&self, atom: Atom<'_>) -> Option { +/// if atom.to_string().unwrap() == "value" { +/// Some(self.value) +/// } else { +/// None +/// } +/// } +/// } +/// +/// fn main() { +/// let rt = Runtime::new().unwrap(); +/// let ctx = Context::full(&rt).unwrap(); +/// +/// ctx.with(|ctx| { +/// let cls = Class::instance(ctx.clone(), TestClass { value: 42 }).unwrap(); +/// let value = ctx.eval::(r#"my_class.value"#)?; +/// println!("value: {}", value); +/// assert_eq!(value, 42); +/// }) +/// } +/// ``` +#[proc_macro_attribute] +pub fn exotic(_attr: TokenStream1, item: TokenStream1) -> TokenStream1 { + let item = parse_macro_input!(item as Item); + match item { + Item::Impl(item) => match exotic::expand(item) { + Ok(x) => x.into(), + Err(e) => e.into_compile_error().into(), + }, + item => Error::new( + item.span(), + "#[exotic] macro can only be used on impl blocks", + ) + .into_compile_error() + .into(), + } +} + /// An attribute which generates code for exporting a module to Rust. /// /// Any supported item inside the module which is marked as `pub` will be exported as a JavaScript value. diff --git a/src/lib.rs b/src/lib.rs index 57f6d63aa..28329e6a9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -126,7 +126,7 @@ pub use rquickjs_core::*; #[cfg_attr(feature = "doc-cfg", doc(cfg(feature = "macro")))] #[cfg(feature = "macro")] -pub use rquickjs_macro::{class, embed, function, methods, module, JsLifetime}; +pub use rquickjs_macro::{class, embed, exotic, function, methods, module, JsLifetime}; pub mod class { //! JavaScript classes defined from Rust. From fb4927e0afc69633610b6c13be016f978fa11ec1 Mon Sep 17 00:00:00 2001 From: Emile Fugulin Date: Wed, 3 Dec 2025 20:45:08 -0500 Subject: [PATCH 12/14] Add test and example for macro exotic --- Cargo.toml | 1 + examples/exotic/Cargo.toml | 10 +++++ examples/exotic/src/main.rs | 84 +++++++++++++++++++++++++++++++++++++ tests/macros/pass_exotic.rs | 84 +++++++++++++++++++++++++++++++++++++ 4 files changed, 179 insertions(+) create mode 100644 examples/exotic/Cargo.toml create mode 100644 examples/exotic/src/main.rs create mode 100644 tests/macros/pass_exotic.rs diff --git a/Cargo.toml b/Cargo.toml index f233f1278..7dfb717f1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,6 +20,7 @@ members = [ "examples/native-module", "examples/module-loader", "examples/rquickjs-cli", + "examples/exotic", ] [workspace.dependencies] diff --git a/examples/exotic/Cargo.toml b/examples/exotic/Cargo.toml new file mode 100644 index 000000000..a089d13c0 --- /dev/null +++ b/examples/exotic/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "rquickjs-exotic" +version = "0.10.0" +authors = ["K. "] +edition = "2018" +publish = false + +[dependencies.rquickjs] +path = "../.." +features = ["macro"] diff --git a/examples/exotic/src/main.rs b/examples/exotic/src/main.rs new file mode 100644 index 000000000..271f2ea3f --- /dev/null +++ b/examples/exotic/src/main.rs @@ -0,0 +1,84 @@ +use rquickjs::{class::Trace, Atom, Context, JsLifetime, Result, Runtime, Value}; + +#[derive(Trace, JsLifetime)] +#[rquickjs::class(exotic)] +pub struct TestClass { + value: i32, +} + +impl TestClass { + pub fn new(value: i32) -> Self { + Self { value } + } +} + +#[rquickjs::exotic] +impl TestClass { + #[qjs(get)] + pub fn value(&self, atom: Atom<'_>) -> Option { + if atom.to_string().unwrap() == "value" { + Some(self.value) + } else { + None + } + } + + #[qjs(set)] + pub fn set_value(&mut self, atom: Atom<'_>, value: Value<'_>) -> bool { + if atom.to_string().unwrap() == "value" { + self.value = value.as_int().unwrap(); + true + } else { + false + } + } + + #[qjs(has)] + pub fn has_value(&self, atom: Atom<'_>) -> bool { + atom.to_string().unwrap() == "value" + } + + #[qjs(delete)] + pub fn delete_value(&mut self, atom: Atom<'_>) -> bool { + if atom.to_string().unwrap() == "value" { + self.value = 0; + true + } else { + false + } + } +} + +fn main() -> Result<()> { + let rt = Runtime::new()?; + let ctx = Context::full(&rt)?; + + ctx.with(|ctx| -> Result<()> { + let global = ctx.globals(); + + let my_class = TestClass::new(42); + global.set("my_class", my_class)?; + + let value = ctx.eval::(r#"my_class.value"#)?; + println!("value: {}", value); + + let value = ctx.eval::, _>(r#"my_class.other"#)?; + println!("value: {:?}", value); + + let value = ctx.eval::(r#"my_class.value = 43; my_class.value"#)?; + println!("value: {}", value); + + let value = ctx.eval::(r#"delete my_class.value; my_class.value"#)?; + println!("value: {}", value); + + let value = ctx.eval::(r#""value" in my_class"#)?; + println!("value: {}", value); + + let value = ctx.eval::(r#""other" in my_class"#)?; + println!("value: {}", value); + + Ok(()) + })?; + + Ok(()) +} diff --git a/tests/macros/pass_exotic.rs b/tests/macros/pass_exotic.rs new file mode 100644 index 000000000..ed315d20a --- /dev/null +++ b/tests/macros/pass_exotic.rs @@ -0,0 +1,84 @@ +use rquickjs::{class::Trace, Atom, Context, JsLifetime, Result, Runtime, Value}; + +#[derive(Trace, JsLifetime)] +#[rquickjs::class(exotic)] +pub struct TestClass { + value: i32, +} + +impl TestClass { + pub fn new(value: i32) -> Self { + Self { value } + } +} + +#[rquickjs::exotic] +impl TestClass { + #[qjs(get)] + pub fn value(&self, atom: Atom<'_>) -> Option { + if atom.to_string().unwrap() == "value" { + Some(self.value) + } else { + None + } + } + + #[qjs(set)] + pub fn set_value(&mut self, atom: Atom<'_>, value: Value<'_>) -> bool { + if atom.to_string().unwrap() == "value" { + self.value = value.as_int().unwrap(); + true + } else { + false + } + } + + #[qjs(has)] + pub fn has_value(&self, atom: Atom<'_>) -> bool { + atom.to_string().unwrap() == "value" + } + + #[qjs(delete)] + pub fn delete_value(&mut self, atom: Atom<'_>) -> bool { + if atom.to_string().unwrap() == "value" { + self.value = 0; + true + } else { + false + } + } +} + +fn main() -> Result<()> { + let rt = Runtime::new()?; + let ctx = Context::full(&rt)?; + + ctx.with(|ctx| -> Result<()> { + let global = ctx.globals(); + + let my_class = TestClass::new(42); + global.set("my_class", my_class)?; + + let value = ctx.eval::(r#"my_class.value"#)?; + assert_eq!(value, 42); + + let value = ctx.eval::, _>(r#"my_class.other"#)?; + assert!(value.is_none()); + + let value = ctx.eval::(r#"my_class.value = 43; my_class.value"#)?; + assert_eq!(value, 43); + + let value = ctx.eval::(r#"delete my_class.value; my_class.value"#)?; + assert_eq!(value, 0); + + let value = ctx.eval::(r#""value" in my_class"#)?; + assert!(value); + + let value = ctx.eval::(r#""other" in my_class"#)?; + assert!(!value); + + Ok(()) + })?; + + Ok(()) +} From a80b139044fb3428b9bd17f96621788e1edbc231 Mon Sep 17 00:00:00 2001 From: Emile Fugulin Date: Sat, 6 Dec 2025 22:34:27 -0500 Subject: [PATCH 13/14] Fix example --- macro/src/lib.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/macro/src/lib.rs b/macro/src/lib.rs index 919a74d82..d453f7387 100644 --- a/macro/src/lib.rs +++ b/macro/src/lib.rs @@ -333,7 +333,7 @@ pub fn methods(attr: TokenStream1, item: TokenStream1) -> TokenStream1 { /// /// # Example /// ``` -/// use rquickjs::{class::Trace, JsLifetime, Context, Runtime}; +/// use rquickjs::{class::Trace, JsLifetime, Context, Runtime, Atom, Class}; /// /// #[derive(Trace, JsLifetime)] /// #[rquickjs::class(exotic)] @@ -359,7 +359,7 @@ pub fn methods(attr: TokenStream1, item: TokenStream1) -> TokenStream1 { /// /// ctx.with(|ctx| { /// let cls = Class::instance(ctx.clone(), TestClass { value: 42 }).unwrap(); -/// let value = ctx.eval::(r#"my_class.value"#)?; +/// let value = ctx.eval::(r#"my_class.value"#).unwrap(); /// println!("value: {}", value); /// assert_eq!(value, 42); /// }) From cf6f794826d84be77513c7ae6c9f05c429b70edc Mon Sep 17 00:00:00 2001 From: Emile Fugulin Date: Sat, 6 Dec 2025 22:40:39 -0500 Subject: [PATCH 14/14] Fix example 2 --- macro/src/lib.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/macro/src/lib.rs b/macro/src/lib.rs index d453f7387..a78841f13 100644 --- a/macro/src/lib.rs +++ b/macro/src/lib.rs @@ -359,6 +359,7 @@ pub fn methods(attr: TokenStream1, item: TokenStream1) -> TokenStream1 { /// /// ctx.with(|ctx| { /// let cls = Class::instance(ctx.clone(), TestClass { value: 42 }).unwrap(); +/// ctx.globals().set("my_class", cls.clone()).unwrap(); /// let value = ctx.eval::(r#"my_class.value"#).unwrap(); /// println!("value: {}", value); /// assert_eq!(value, 42);