-
Notifications
You must be signed in to change notification settings - Fork 70
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Rework the class system #373
Conversation
I really like this design - it feels much simpler and less object-oriented. While we're revisiting the class and function FFI, have you considered using polymorphism through Rust or JavaScript classes? I know it's out of scope for this PR, but I wanted to mention it so we don't make future integration more complex. I would add some test cases for extending Rust classes in JS (works in current class implementation): class Vec4 extends Vec3 {
w = 0
constructor(x,y,z,w){
super(x,y,z);
this.w = w;
}
} |
This is super nice. Once this is merged I will rebase my NG PR and we should be pretty good to go 👌 |
Unfortunately it seems I made an invalid assumption which makes this PR as it is currently unsound. It turns out that constant address are not guaranteed to be stable nor unique. So the type distinction by v-table address is unsound. See rust-lang/unsafe-code-guidelines#522. This is probably the reason why the windows test fails. This could still work if there was a way to get store a type-id within the v-table. However I am currently experimenting with adding a trait called |
What about using the approach i did to store an AtomicUsize in a TypeId struct with a const new function and initialize the counter via fetch_update? That way we get a thread safe alternative to Lines 8 to 10 in 6cf0761
Lines 35 to 43 in 6cf0761
|
Does this still use the old approach of having JsClass have a function struct Generic<T>(T);
impl<'js,T> JsClass<'js> for Generic<T> {
const NAME: &'static str = "Generic";
fn class_id() -> &'static crate::class::ClassId {
static ID: ClassId = ClassId::new();
&ID
}
fn prototype(ctx: &Ctx<'js>) -> Result<Option<Object<'js>>> {
// .. omit other functions.
}
} Then std::ptr::eq(<Generic<usize> as JsClass>::class_id(), <Generic<u8> as JsClass>::class_id()) Returns true because all |
Ah right, i didn't consider generics. Can we pub struct TypeId {
pub val: (u64, u64),
}
impl TypeId {
pub fn of<T: Any>() -> Self {
unsafe { mem::transmute(TypeId::of::<T>()) }
}
} |
The type id object itself is not the problem, the problem is in that to get the type id of So you wouldn't be able to obtain a |
I have an approach where I require all implementers of pub unsafe trait Outlive<'js> {
/// The target which has the same type as a `Self` but with another lifetime `'t`
type Target<'to>: 'to;
} Outlive was previously used to erase the lifetime of types in Persistant so that the could be handled outside of the This will however need support from the macros to properly implement and it might be a bit difficult to do this in a way that doesn't allow unsound implementations of |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Some minor comments
} | ||
|
||
let class_def = qjs::JSClassDef { | ||
class_name: b"RustFunction\0".as_ptr().cast(), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
RustFunction
=> __qjs_rf
qjs::JS_NewClassID((&mut self.callable_class_id) as *mut qjs::JSClassID); | ||
|
||
let class_def = qjs::JSClassDef { | ||
class_name: b"RustClass\0".as_ptr().cast(), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we also use a more private name, i.e __qjs_rc
and put this as a const some where?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can, but it doesn't really matter. RustClass
is not accessable anywhere, it is not stored in the global object or anywhere else so it can't pollute a namespace.
It also can't cause conflicts as you can register two classes with the same class_name without problems.
The only place you will see this name is in dumps when enabling the debug dump features. It might be a good idea to make this a bit more descriptive, maybe RquickjsClass
to clearly signify where the class came from.
/// The class id for rust classes which can be called. | ||
callable_class_id: qjs::JSClassID, | ||
|
||
prototypes: UnsafeCell<HashMap<*const VTable, Option<Object<'js>>>>, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So think i figured out a great way to solve this without hashtable:
- Each
vtable
gets an additional auto incrementingindex_id
(derived form an atomic u32) prototypes
are stored as aVec
(orTinyVec
)- Proto objects are stored and accessed via the array index of
index_id
for thevtable
Fast, constant time lookup, no hashing or collitions
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That unfortunately doesn't work the trick I use for generating a vtable for each type doesn't allow mutable state within that vtable. If you try to add any type which can mutate, like AtomicU32
, UnsafeCell
, and others it will cause a compile error.
So there is no way to get an auto incrementing value. You can't generate one within a const environment and you can't store one during runtime in a vtable like static.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I experimented with top level recursion but it doest work for this use case. We'll have to stick with hashtable which i think is fine :)
/// The class id for rust classes which can be called. | ||
callable_class_id: qjs::JSClassID, | ||
|
||
prototypes: UnsafeCell<HashMap<*const VTable, Option<Object<'js>>>>, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So think i figured out a great way to solve this without hashtable:
- Each
vtable
gets an additional auto incrementingindex_id
(derived form an atomic u32) prototypes
are stored as aVec
(orTinyVec
)- Proto objects are stored and accessed via the array index of
index_id
for thevtable
Fast, constant time lookup, no hashing or collitions
This PR changes how the classes work in rquickjs.
Currently rquickjs uses the internal QuickJS class registration system to handle rust classes.
This requires some way to store class-id's per type.
In the existing code this is done with a per type static global which is initialized once and then reused.
This works because QuickJS class ids are global and don't depend on the runtime.
However this approach has some problems.
First in QuickJS-NG the class-ids are no longer global which breaks this approach.
Second the class-ids globals are not actually unique per type but per implementation of the
JsClass
trait.This causes problems for generic types like in #340.
This PR instead uses only two QuickJS classes registered during initialization: rust-class and callable-rust-class.
These classes are defined to use a v-table stored alongside allocated rust class objects to handle finalization, tracing and calling in the case of callable rust classes.
This v-table can also be used to distinguish between different classes and different types and works for generic objects fixing #340.
Prototypes are handled fully on the rust side, whenever a class is instantiated its prototype is looked up in a hashmap stored within the runtime.