Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
Expand Down
22 changes: 19 additions & 3 deletions core/src/class.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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 {
Expand All @@ -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<Option<Object<'js>>> {
Object::new(ctx.clone()).map(Some)
Expand Down Expand Up @@ -149,9 +156,18 @@ impl<'js, C: JsClass<'js>> Class<'js, C> {
unsafe { ctx.get_opaque().get_or_insert_prototype::<C>(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<Option<Constructor<'js>>> {
unsafe { ctx.get_opaque().get_or_insert_constructor::<C>(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<Option<Constructor<'js>>> {
C::constructor(ctx)
Self::constructor(ctx)
}

/// Defines the predefined constructor of this class, if there is one, onto the given object.
Expand Down
2 changes: 2 additions & 0 deletions core/src/class/cell.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<T> {
count: Cell<usize>,
value: UnsafeCell<T>,
Expand Down Expand Up @@ -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: <T::Mutable as Mutability>::Cell<T>,
}
Expand Down
24 changes: 22 additions & 2 deletions core/src/class/ffi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -130,6 +133,7 @@ impl VTable {
finalizer: VTable::finalizer_impl::<'js, C>,
trace: VTable::trace_impl::<C>,
call: VTable::call_impl::<C>,
parent: C::parent_vtable,
};
}
&<C as HasVTable>::VTABLE
Expand All @@ -140,7 +144,23 @@ impl VTable {
}

pub fn is_of_class<'js, C: JsClass<'js>>(&self) -> bool {
(self.id_fn)() == TypeId::of::<C::Changed<'static>>()
let target_id = TypeId::of::<C::Changed<'static>>();

// 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
}
}

Expand Down
13 changes: 13 additions & 0 deletions core/src/class/inherits.rs
Original file line number Diff line number Diff line change
@@ -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 = <Self as JsClass<'js>>::Mutable>;

fn as_parent(&self) -> &Self::Parent;
}
21 changes: 21 additions & 0 deletions core/src/runtime/opaque.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use crate::{
class::{self, ffi::VTable, JsClass},
function::Constructor,
qjs, Ctx, Error, JsLifetime, Object, Value,
};

Expand Down Expand Up @@ -50,6 +51,7 @@ pub(crate) struct Opaque<'js> {
callable_class_id: qjs::JSClassID,

prototypes: UnsafeCell<HashMap<TypeId, Option<Object<'js>>>>,
constructors: UnsafeCell<HashMap<TypeId, Option<Constructor<'js>>>>,

userdata: UserDataMap,

Expand All @@ -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(),

Expand Down Expand Up @@ -252,6 +255,23 @@ impl<'js> Opaque<'js> {
}
}

pub fn get_or_insert_constructor<C: JsClass<'js>>(
&self,
ctx: &Ctx<'js>,
) -> Result<Option<Constructor<'js>>, Error> {
unsafe {
let vtable = VTable::get::<C>();
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
Expand All @@ -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()
Expand Down
108 changes: 106 additions & 2 deletions macro/src/class.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,15 @@ pub(crate) struct ClassConfig {
pub crate_: Option<String>,
pub rename: Option<String>,
pub rename_all: Option<Case>,
pub extends: Option<Box<syn::Type>>,
}

pub(crate) enum ClassOption {
Frozen(FlagOption<kw::frozen>),
Crate(ValueOption<Token![crate], LitStr>),
Rename(ValueOption<kw::rename, LitStr>),
RenameAll(ValueOption<kw::rename_all, Case>),
Extends(ValueOption<kw::extends, Box<syn::Type>>),
}

impl Parse for ClassOption {
Expand All @@ -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"))
}
Expand All @@ -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());
}
}
}

Expand Down Expand Up @@ -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 {
Expand All @@ -299,6 +306,7 @@ impl Class {
}
}
Class::Struct {
config,
attrs,
vis,
struct_token,
Expand All @@ -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
}
}
Expand All @@ -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

Expand All @@ -358,24 +451,35 @@ impl Class {

type Mutable = #crate_name::class::#mutability;

#parent_vtable_fn

fn prototype(ctx: &#crate_name::Ctx<'js>) -> #crate_name::Result<Option<#crate_name::Object<'js>>>{
use #crate_name::class::impl_::MethodImplementor;

let proto = #crate_name::Object::new(ctx.clone())?;
#props
let implementor = #crate_name::class::impl_::MethodImpl::<Self>::new();
(&implementor).implement(&proto)?;

#parent_proto_impl

Ok(Some(proto))
}

fn constructor(ctx: &#crate_name::Ctx<'js>) -> #crate_name::Result<Option<#crate_name::function::Constructor<'js>>>{
use #crate_name::class::impl_::ConstructorCreator;

let implementor = #crate_name::class::impl_::ConstructorCreate::<Self>::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::<Self>::instance(ctx.clone(),self)?;
Expand Down
Loading
Loading