Skip to content
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 cppgc for prototype chain #1047

Open
wants to merge 18 commits into
base: main
Choose a base branch
from
Open

rework cppgc for prototype chain #1047

wants to merge 18 commits into from

Conversation

littledivy
Copy link
Member

@littledivy littledivy commented Jan 16, 2025

Implement prototype inheritance into cppgc object wraps.

struct Foo {}

impl GarbageCollected for Foo {}

#[op2]
impl Foo {
    fn super_method(&self) {
        println!("super method called!");
    }
}

struct Bar {}

impl GarbageCollected for Bar {}

#[op2(inherit = Foo)]
impl Bar {
    #[constructor]
    #[cppgc]
    fn new() -> (Foo, Bar) {
        let foo = Foo {};
        let bar = Bar {};

        (foo, bar)
    }

    #[fast]
    fn use_proto(&self, #[proto] foo: &Foo) {
        // do something with foo
    }
}
const bar = new Bar();

assert(bar instanceof Foo);
bar.superMethod();
bar.useProto();

@littledivy littledivy marked this pull request as ready for review January 16, 2025 14:41
core/cppgc.rs Outdated
@@ -58,6 +132,78 @@ pub fn make_cppgc_object<'a, T: GarbageCollected + 'static>(
wrap_object(scope, obj, t)
}

pub fn make_cppgc_object2<
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe change a name to something more descriptive to suggest that it uses the second argument as a prototype

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's named because its easier for the macro to do: format!("make_cppgc_object{}", chain.len()). These aren't supposed to be publicly used. I've added #[doc(hidden)]

Copy link
Member

@lucacasonato lucacasonato left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think cppgc_template_constructor should throw a TypeError.

/// ErasedPtr is a wrapper around a `v8::cppgc::Ptr` that allows downcasting
/// to a specific type. Safety is guaranteed by the `tag` field in the
/// `CppGcObject` struct.
struct ErasedPtr {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually this abstraction is much nicer than what we had previously. With this it is impossible to accidentially use T without checking ptr.tag anymore. Great!

fn trace(&self, visitor: &v8::cppgc::Visitor) {
// Trace all the objects top-down the prototype chain.
for ptr in self.0.iter().flatten() {
ptr.ptr.trace(visitor);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this dispatch correctly? We don't want to call EreasedPtr's T here - I assume this all works out, but can you add some comments?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added

Copy link
Member

@lucacasonato lucacasonato left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@devsnek PTAL also

tag: TypeId::of::<T>(),
member: t,
},
PrototypeChainStore([
Copy link
Member

@devsnek devsnek Jan 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do normal objects need this? its unfortunate to have double the allocations. could we instead have two tags, one for plain objects and one for prototype backed objects? plain objects can continue to use wrap/unwrap, and prototype backed objects can use wrap2/unwrap2.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That could work with a custom DST. A safe enum will require heap indirection or just have the same size.

I'll give the DSTs a shot, that might also improve the 3 protochain limitation we have since we could just size it at runtime.

size: u8
data: [CppGcObject]

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if you can solve it with one system that's cool, but i just meant have two different wrap mechanisms. the default one would be the existing one, and you can opt into a prototype mode if you're using that.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, I made the prototype chain opt-in with #[op2(base)] on the base class otherwise it is treated as a basic object with a single CppGcObject allocation.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh but that would not work with try_unwrap_cppgc_object. we'll basically have to have 2 functions and the macro would somehow need to know which one to use to "unwrap" self.

#[op2]
impl A {
  fn method(&self) {
    // we don't know statically if `self` is with proto or without proto.
    //
    // the macro produces `try_unwrap_cppgc_object(args.this())`
    // that uses the `PrototypeChain` trait to figure that out.
  }
}

#[op2]
fn op_a(#[cppgc] a: &A) {
  // same here
}

It is doable with extra attributes if you feel strongly about it. I think the PrototypeChain trait is not that big of a change and most of it is auto generated by the macro anyways.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

self would always be with proto. plain fn would be without proto unless you do like #[cppgc(proto)] or something

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

self would always be with proto.

don't we want it to without proto for single objects tho?

// want this to be without proto:
#[op2]
impl A {}

// want this to be with proto:
#[op2(base)]
impl Base {}
#[op2(inherit = Base)]
impl B {}

So maybe we do this: by default self and #[cppgc] are no-proto. And we do proto only when base and inherit attribute or#[cppgc(proto)] is used.

Downside is that devs will have to keep track of what has proto and what doesn't - which was already done for then with the default PrototypeChain trait.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ultimately I'm just aiming to keep the complexity down for cppgc backed resources. prototype stuff has a lot of its own codegen already so it should be free to use new apis I think.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PTAL 4dc27fb

didn't implement #[cppgc(proto)] cuz we don't need it right now.

}
}

struct PrototypeChainStore([Option<ErasedPtr>; MAX_PROTO_CHAIN]);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe crazy but could this be an interior linked list of ErasedPtr stored on each object? not sure if saving the memory is worth the slight overhead of traversing when unwrapping, perhaps worth testing?

Copy link
Member Author

@littledivy littledivy Jan 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tried it out:

#[repr(C)]
struct CppGcObject<T: GarbageCollected> {
  tag: TypeId,
  next: Option<ErasedPtr>,
  member: T,
}
./dcore_proto_linked_list test.mjs # linked list
🛑 deno_core binary is meant for development and testing purposes.
Run test.mjs

1e8x new DOMPoint(): 10290ms
1e8x point.x: 2985ms
./dcore_proto_array test.mjs # array
🛑 deno_core binary is meant for development and testing purposes.
Run test.mjs

1e8x new DOMPoint(): 10544ms
1e8x point.x: 2785ms

Maybe it makes sense to use linked list when we increase depth further than 3?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants