diff --git a/CHANGELOG.md b/CHANGELOG.md index 081f5bd0..8222b2bb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +### Added + +- Add inheritance support for Rust classes #[#587](https://github.com/DelSkayn/rquickjs/pull/587) + + ## [0.11.0] - 2025-12-16 ### Added @@ -17,6 +22,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Add JsIterator to iterate over Javascript Iterator #[#564](https://github.com/DelSkayn/rquickjs/pull/564) - Add more trait implementations like AsRef for CString #[#558](https://github.com/DelSkayn/rquickjs/pull/558) + ### Changed - Bump MSRV to 1.85 #[#531](https://github.com/DelSkayn/rquickjs/pull/531) diff --git a/core/src/class.rs b/core/src/class.rs index 93dd73ba..0e20396a 100644 --- a/core/src/class.rs +++ b/core/src/class.rs @@ -12,7 +12,8 @@ use core::{hash::Hash, marker::PhantomData, mem, ops::Deref, ptr::NonNull}; mod cell; mod trace; -pub(crate) mod ffi; +#[doc(hidden)] +pub mod ffi; pub use cell::{ Borrow, BorrowMut, JsCell, Mutability, OwnedBorrow, OwnedBorrowMut, Readable, Writable, @@ -21,6 +22,7 @@ use ffi::{ClassCell, VTable}; pub use trace::{Trace, Tracer}; #[doc(hidden)] pub mod impl_; +pub mod inherits; /// The trait which allows Rust types to be used from JavaScript. pub trait JsClass<'js>: Trace<'js> + JsLifetime<'js> + Sized { @@ -35,6 +37,11 @@ pub trait JsClass<'js>: Trace<'js> + JsLifetime<'js> + Sized { /// This should either be [`Readable`] or [`Writable`]. type Mutable: Mutability; + /// Returns the parent class vtable if this class extends another class. + fn parent_vtable() -> Option<&'static VTable> { + None + } + /// Returns the class prototype, fn prototype(ctx: &Ctx<'js>) -> Result>> { Object::new(ctx.clone()).map(Some) @@ -149,9 +156,18 @@ impl<'js, C: JsClass<'js>> Class<'js, C> { unsafe { ctx.get_opaque().get_or_insert_prototype::(ctx) } } - /// Create a constructor for the current class using its definition. + /// Returns the constructor for the current class using its definition. + /// + /// Returns `None` if the class is not yet registered or if the class doesn't have a constructor. + pub fn constructor(ctx: &Ctx<'js>) -> Result>> { + unsafe { ctx.get_opaque().get_or_insert_constructor::(ctx) } + } + + /// Returns the constructor for the current class using its definition. + /// + /// Returns `None` if the class is not yet registered or if the class doesn't have a constructor. pub fn create_constructor(ctx: &Ctx<'js>) -> Result>> { - C::constructor(ctx) + Self::constructor(ctx) } /// Defines the predefined constructor of this class, if there is one, onto the given object. diff --git a/core/src/class/cell.rs b/core/src/class/cell.rs index 3c2422ab..86cc79c2 100644 --- a/core/src/class/cell.rs +++ b/core/src/class/cell.rs @@ -102,6 +102,7 @@ unsafe impl Mutability for Readable { /// When a class has `Writable` as it Mutable type you can borrow it both mutability and immutable. pub enum Writable {} +#[repr(C)] pub struct WritableCell { count: Cell, value: UnsafeCell, @@ -203,6 +204,7 @@ unsafe impl Mutability for Writable { /// A cell type for Rust classes passed to JavaScript. /// /// Implements [`RefCell`](std::cell::RefCell)-like borrow checking. +#[repr(transparent)] pub struct JsCell<'js, T: JsClass<'js>> { pub(crate) cell: ::Cell, } diff --git a/core/src/class/ffi.rs b/core/src/class/ffi.rs index 1c977bb1..7ebfc103 100644 --- a/core/src/class/ffi.rs +++ b/core/src/class/ffi.rs @@ -75,12 +75,15 @@ pub(crate) type CallFunc = for<'a> unsafe fn( ) -> qjs::JSValue; pub(crate) type TypeIdFn = fn() -> TypeId; +pub(crate) type ParentFn = fn() -> Option<&'static VTable>; -pub(crate) struct VTable { +#[doc(hidden)] +pub struct VTable { id_fn: TypeIdFn, finalizer: FinalizerFunc, trace: TraceFunc, call: CallFunc, + parent: ParentFn, } impl VTable { @@ -130,6 +133,7 @@ impl VTable { finalizer: VTable::finalizer_impl::<'js, C>, trace: VTable::trace_impl::, call: VTable::call_impl::, + parent: C::parent_vtable, }; } &::VTABLE @@ -140,7 +144,23 @@ impl VTable { } pub fn is_of_class<'js, C: JsClass<'js>>(&self) -> bool { - (self.id_fn)() == TypeId::of::>() + let target_id = TypeId::of::>(); + + // Check this class first + if (self.id_fn)() == target_id { + return true; + } + + // Traverse the parent chain + let mut current = (self.parent)(); + while let Some(parent_vtable) = current { + if (parent_vtable.id_fn)() == target_id { + return true; + } + current = (parent_vtable.parent)(); + } + + false } } diff --git a/core/src/class/inherits.rs b/core/src/class/inherits.rs new file mode 100644 index 00000000..47efc5f1 --- /dev/null +++ b/core/src/class/inherits.rs @@ -0,0 +1,13 @@ +use crate::class::JsClass; + +/// Trait for classes that have a parent class. +pub trait HasParent<'js> +where + Self: JsClass<'js>, +{ + /// Since the JSCell has different memory layout for different mutabilities, + /// the parent class must have the same mutability as the child class. + type Parent: JsClass<'js, Mutable = >::Mutable>; + + fn as_parent(&self) -> &Self::Parent; +} diff --git a/core/src/runtime/opaque.rs b/core/src/runtime/opaque.rs index 4716b3e7..cfe8eb92 100644 --- a/core/src/runtime/opaque.rs +++ b/core/src/runtime/opaque.rs @@ -1,5 +1,6 @@ use crate::{ class::{self, ffi::VTable, JsClass}, + function::Constructor, qjs, Ctx, Error, JsLifetime, Object, Value, }; @@ -50,6 +51,7 @@ pub(crate) struct Opaque<'js> { callable_class_id: qjs::JSClassID, prototypes: UnsafeCell>>>, + constructors: UnsafeCell>>>, userdata: UserDataMap, @@ -74,6 +76,7 @@ impl<'js> Opaque<'js> { callable_class_id: qjs::JS_INVALID_CLASS_ID, prototypes: UnsafeCell::new(HashMap::new()), + constructors: UnsafeCell::new(HashMap::new()), userdata: UserDataMap::default(), @@ -252,6 +255,23 @@ impl<'js> Opaque<'js> { } } + pub fn get_or_insert_constructor>( + &self, + ctx: &Ctx<'js>, + ) -> Result>, Error> { + unsafe { + let vtable = VTable::get::(); + let id = vtable.id(); + match (*self.constructors.get()).entry(id) { + Entry::Occupied(x) => Ok(x.get().clone()), + Entry::Vacant(x) => { + let constructor = C::constructor(ctx)?; + Ok(x.insert(constructor).clone()) + } + } + } + } + /// Cleans up all the internal state. /// /// Called before dropping the runtime to ensure that we drop everything before freeing the @@ -261,6 +281,7 @@ impl<'js> Opaque<'js> { self.interrupt_handler.get_mut().take(); self.panic.take(); self.prototypes.get_mut().clear(); + self.constructors.get_mut().clear(); #[cfg(feature = "futures")] self.spawner.take(); self.userdata.clear() diff --git a/macro/src/class.rs b/macro/src/class.rs index d2a6c1da..e7a26986 100644 --- a/macro/src/class.rs +++ b/macro/src/class.rs @@ -20,6 +20,7 @@ pub(crate) struct ClassConfig { pub crate_: Option, pub rename: Option, pub rename_all: Option, + pub extends: Option>, } pub(crate) enum ClassOption { @@ -27,6 +28,7 @@ pub(crate) enum ClassOption { Crate(ValueOption), Rename(ValueOption), RenameAll(ValueOption), + Extends(ValueOption>), } impl Parse for ClassOption { @@ -39,6 +41,8 @@ impl Parse for ClassOption { input.parse().map(Self::Rename) } else if input.peek(kw::rename_all) { input.parse().map(Self::RenameAll) + } else if input.peek(kw::extends) { + input.parse().map(Self::Extends) } else { Err(syn::Error::new(input.span(), "invalid class attribute")) } @@ -60,6 +64,9 @@ impl ClassConfig { ClassOption::RenameAll(ref x) => { self.rename_all = Some(x.value); } + ClassOption::Extends(ref x) => { + self.extends = Some(x.value.clone()); + } } } @@ -281,7 +288,7 @@ impl Class { } } - // Aeexpand the original definition with the attributes removed.. + // Reexpand the original definition with the attributes removed. pub fn reexpand(&self) -> TokenStream { match self { Class::Enum { @@ -299,6 +306,7 @@ impl Class { } } Class::Struct { + config, attrs, vis, struct_token, @@ -325,8 +333,16 @@ impl Class { Fields::Unit => TokenStream::new(), }; + // Add #[repr(C)] when extends is specified for memory layout compatibility + let repr_c = if config.extends.is_some() { + quote! { #[repr(C)] } + } else { + TokenStream::new() + }; + quote! { #(#attrs)* + #repr_c #vis #struct_token #ident #generics #fields } } @@ -346,6 +362,83 @@ impl Class { let props = self.expand_props(&crate_name); let reexpand = self.reexpand(); + // Generate prototype and constructor setup based on whether we have a parent class + let (parent_proto_impl, parent_constructor_impl) = if let Some(ref parent_ty) = + self.config().extends + { + let proto_setup = quote! { + // Set up prototype chain: our prototype's prototype is the parent's prototype + if let Some(parent_proto) = #crate_name::class::Class::<#parent_ty>::prototype(ctx)? { + proto.set_prototype(Some(&parent_proto))?; + } + }; + + let constructor_setup = quote! { + // Set up constructor chain for static properties/methods + // The child constructor's __proto__ should be the parent constructor + if let Some(ref parent_constructor) = #crate_name::class::Class::<#parent_ty>::create_constructor(ctx)? { + if let Some(ref child_constructor) = constructor { + let parent_func: &#crate_name::Object = parent_constructor.as_inner(); + child_constructor.set_prototype(Some(parent_func))?; + } + } + }; + + (proto_setup, constructor_setup) + } else { + (TokenStream::new(), TokenStream::new()) + }; + + // Generate parent_vtable_fn and HasParent implementation if extends is specified + let (parent_vtable_fn, inheritance_impls) = if let Some(ref parent_ty) = + self.config().extends + { + // Determine the first field accessor + let first_field_accessor = match &self { + Class::Struct { fields, .. } => match fields { + Fields::Named(fields) => { + let field_name = fields.first().and_then(|f| f.ident.as_ref()); + if let Some(name) = field_name { + quote! { &self.#name } + } else { + quote! { &self.0 } + } + } + Fields::Unnamed(_) => quote! { &self.0 }, + Fields::Unit => { + return Err(Error::new(self.ident().span(), "extends requires the struct to have at least one field of the parent type")); + } + }, + Class::Enum { .. } => { + return Err(Error::new( + self.ident().span(), + "extends is not supported for enums", + )); + } + }; + + let vtable_fn = quote! { + fn parent_vtable() -> Option<&'static #crate_name::class::ffi::VTable> { + Some(#crate_name::class::ffi::VTable::get::<#parent_ty>()) + } + }; + + let has_parent = quote! { + impl #generics_with_lifetimes #crate_name::class::inherits::HasParent<'js> for #class_name #generics + { + type Parent = #parent_ty; + + fn as_parent(&self) -> &Self::Parent { + #first_field_accessor + } + } + }; + + (vtable_fn, has_parent) + } else { + (TokenStream::new(), TokenStream::new()) + }; + let res = quote! { #reexpand @@ -358,6 +451,8 @@ impl Class { type Mutable = #crate_name::class::#mutability; + #parent_vtable_fn + fn prototype(ctx: &#crate_name::Ctx<'js>) -> #crate_name::Result>>{ use #crate_name::class::impl_::MethodImplementor; @@ -365,6 +460,9 @@ impl Class { #props let implementor = #crate_name::class::impl_::MethodImpl::::new(); (&implementor).implement(&proto)?; + + #parent_proto_impl + Ok(Some(proto)) } @@ -372,10 +470,16 @@ impl Class { use #crate_name::class::impl_::ConstructorCreator; let implementor = #crate_name::class::impl_::ConstructorCreate::::new(); - (&implementor).create_constructor(ctx) + let constructor = (&implementor).create_constructor(ctx)?; + + #parent_constructor_impl + + Ok(constructor) } } + #inheritance_impls + impl #generics_with_lifetimes #crate_name::IntoJs<'js> for #class_name #generics{ fn into_js(self,ctx: &#crate_name::Ctx<'js>) -> #crate_name::Result<#crate_name::Value<'js>>{ let cls = #crate_name::class::Class::::instance(ctx.clone(),self)?; diff --git a/macro/src/common.rs b/macro/src/common.rs index 7e18624f..ee93119e 100644 --- a/macro/src/common.rs +++ b/macro/src/common.rs @@ -139,4 +139,5 @@ pub(crate) mod kw { syn::custom_keyword!(prefix); syn::custom_keyword!(declare); syn::custom_keyword!(evaluate); + syn::custom_keyword!(extends); } diff --git a/macro/src/lib.rs b/macro/src/lib.rs index 37d66e53..46b18a33 100644 --- a/macro/src/lib.rs +++ b/macro/src/lib.rs @@ -46,6 +46,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. | +/// | `extends` | Type | Specifies the parent class of this class. When extending is enabled, the struct's first field must be of the parent type. | /// /// # Field options /// @@ -88,6 +89,14 @@ mod trace; /// another_value: u32, /// } /// +/// #[derive(Trace, JsLifetime)] +/// #[rquickjs::class(rename_all = "camelCase", extends = TestClass<'js>)] +/// pub struct TestSubClass<'js> { +/// super_: TestClass<'js>, +/// #[qjs(get, set)] +/// sub_value: u32, +/// } +/// /// pub fn main() { /// let rt = Runtime::new().unwrap(); /// let ctx = Context::full(&rt).unwrap(); @@ -103,13 +112,32 @@ mod trace; /// }, /// ) /// .unwrap(); +/// let sub_cls = Class::instance( +/// ctx.clone(), +/// TestSubClass { +/// super_: TestClass { +/// inner_object: Object::new(ctx.clone()).unwrap(), +/// some_value: 1, +/// another_value: 2, +/// }, +/// sub_value: 3, +/// }, +/// ) +/// .unwrap(); /// /// Pass it to JavaScript /// ctx.globals().set("t", cls.clone()).unwrap(); +/// ctx.globals().set("t2", sub_cls.clone()).unwrap(); /// ctx.eval::<(), _>( /// r#" /// // use the actual value. /// if(t.someValue !== 1){ /// throw new Error(1) +/// } +/// if(t2.someValue !== 1){ +/// throw new Error(2) +/// } +/// if(t2.subValue !== 3){ +/// throw new Error(3) /// }"# /// ).unwrap(); /// }) diff --git a/tests/macros/pass_class.rs b/tests/macros/pass_class.rs index e1f70ab4..ce433ea0 100644 --- a/tests/macros/pass_class.rs +++ b/tests/macros/pass_class.rs @@ -1,4 +1,7 @@ -use rquickjs::{class::Trace, CatchResultExt, Class, Context, JsLifetime, Object, Runtime}; +use rquickjs::{ + class::{inherits::HasParent, Trace}, + CatchResultExt, Class, Context, JsLifetime, Object, Runtime, +}; #[derive(Trace, JsLifetime)] #[rquickjs::class(rename_all = "camelCase")] @@ -11,6 +14,61 @@ pub struct TestClass<'js> { another_value: u32, } +#[rquickjs::methods] +impl<'js> TestClass<'js> { + #[qjs(constructor)] + pub fn new(inner_object: Object<'js>, some_value: u32, another_value: u32) -> Self { + Self { + inner_object, + some_value, + another_value, + } + } + + #[qjs(static)] + pub fn compare(a: &Self, b: &Self) -> bool { + a.some_value == b.some_value && a.another_value == b.another_value + } + + #[qjs(static, rename = "getInnerObject")] + pub fn get_inner_object(this: &Self) -> Object<'js> { + this.inner_object.clone() + } +} + +#[derive(Trace, JsLifetime)] +#[rquickjs::class(rename_all = "camelCase", extends = TestClass<'js>)] +pub struct TestSubClass<'js> { + super_: TestClass<'js>, + #[qjs(get, set)] + sub_value: u32, +} + +#[rquickjs::methods] +impl<'js> TestSubClass<'js> { + #[qjs(constructor)] + pub fn new( + inner_object: Object<'js>, + some_value: u32, + another_value: u32, + sub_value: u32, + ) -> Self { + Self { + super_: TestClass { + inner_object, + some_value, + another_value, + }, + sub_value, + } + } + + #[qjs(static)] + pub fn compare(a: &Self, b: &Self) -> bool { + TestClass::compare(&a.super_, &b.super_) && a.sub_value == b.sub_value + } +} + pub fn main() { let rt = Runtime::new().unwrap(); let ctx = Context::full(&rt).unwrap(); @@ -25,7 +83,29 @@ pub fn main() { }, ) .unwrap(); + let sub_cls = Class::instance( + ctx.clone(), + TestSubClass { + super_: TestClass { + inner_object: Object::new(ctx.clone()).unwrap(), + some_value: 1, + another_value: 2, + }, + sub_value: 3, + }, + ) + .unwrap(); ctx.globals().set("t", cls.clone()).unwrap(); + ctx.globals().set("t2", sub_cls.clone()).unwrap(); + ctx.globals() + .set("TestClass", Class::::constructor(&ctx).unwrap()) + .unwrap(); + ctx.globals() + .set( + "TestSubClass", + Class::::constructor(&ctx).unwrap(), + ) + .unwrap(); ctx.eval::<(), _>( r#" if(t.someValue !== 1){ @@ -52,6 +132,48 @@ pub fn main() { throw new Error(7) } t.innerObject.test = 42; + + if(t2.someValue !== 1){ + throw new Error(8) + } + if(t2.anotherValue !== 2){ + throw new Error(9) + } + if(t2.subValue !== 3){ + throw new Error(10) + } + t2.someValue = 4; + t2.subValue = 5; + if(t2.someValue !== 4){ + throw new Error(11) + } + if(t2.subValue !== 5){ + throw new Error(12) + } + let proto2 = Object.getPrototypeOf(t2); + if (Object.getPrototypeOf(proto2) !== proto){ + throw new Error(13) + } + if(!t2.innerObject){ + throw new Error(14) + } + if(typeof t2.innerObject !== "object"){ + throw new Error(15) + } + t2.innerObject.test = 43; + + if(!TestClass.compare(t,t)){ + throw new Error(16) + } + if(!TestSubClass.compare(t2,t2)){ + throw new Error(17) + } + if(!TestClass.getInnerObject(t).test){ + throw new Error(18) + } + if(!TestSubClass.getInnerObject(t2).test){ + throw new Error(19) + } "#, ) .catch(&ctx) @@ -60,6 +182,15 @@ pub fn main() { let b = cls.borrow(); assert_eq!(b.some_value, 3); assert_eq!(b.another_value, 2); - assert_eq!(b.inner_object.get::<_, u32>("test").unwrap(), 42) + assert_eq!(b.inner_object.get::<_, u32>("test").unwrap(), 42); + + let b2 = sub_cls.borrow(); + assert_eq!(b2.as_parent().some_value, 4); + assert_eq!(b2.as_parent().another_value, 2); + assert_eq!(b2.sub_value, 5); + assert_eq!( + b2.as_parent().inner_object.get::<_, u32>("test").unwrap(), + 43 + ); }); }