Skip to content

Constructor & prototype reform #133

@pmdartus

Description

@pmdartus

Context

Currently, the constructor and prototype chains are shared between all the realms. This approach has multiple downstream impacts:

  • on the maintainer side, it requires hacks to implements interfaces that need a reference to the current global object, eg: [HTMLConstructor]. WebIDL2JS also had to implement the [WebIDL2JSFactory] IDL extended attribute to generate wrapper class per window object.
  • on the consumer side, it is unexpected for developers that all constructors and prototypes are shared, especially when they use a test framework abstracting jsdom. I personally spent a lot of time figuring out the best way to apply polyfill in the context of jsdom.

While recreating a brand new wrapper class for each new realm won't scale, I think there might be other alternatives worth exploring.

Approach 1: New constructor / Same prototype

This approach consists of recreating a brand new constructor per realm and reuse the same prototype. WebIDL2JS would generate for all the interfaces annotated with the [Exposed] IDL extended attribute would an install method that would be in charge of creating the interface in the context of the realm.

class TestGlobal extends BaseGlobal { /* ... */ }

const iface = {
    /* ... */

    install(globalObject, privateData) {
        const Test = function() {
            return iface.setup(Object.create(TestGlobal.prototype), [], privateData);
        };
        // Apply static properties if necessary

        Object.setPrototypeOf(Test, TestGlobal.prototype);

        Object.defineProperty(globalObject, "Test", {
            configurable: true,
            overridable: true,
            value: Test
        });

        // Apply [NamedConstructor] if necessary
    }
}
module.export = iface;

You can find here an experimental branch with the code change: pmdartus/constructor

Pros:

  • Really light-weight approach.
  • No need for [WebIDL2JSFactory] anymore.
  • The new install method can be used to implement [NamedConstructor].
  • Minimal change needed on jsdom

Cons:

  • It doesn't solve the polyfill issue.
  • With this approach the only the (new Test()).constructor points to TestGlobal instead of Test, since the prototype is shared. When running the experimental WebIDL2JS against jsdom test, this issue made 5 tests failed.

Approach 2: New constructor / New prototype

In this approach, a brand new constructor and prototype chain is created per realm. Instead of creating a brand new wrapper class per realm, like [WebIDL2JSFactory], we would instead only create a new prototype chain but reuse the existing property descriptors. It also reuses the concept of install showed in proposal 1.

The assumption here is that the performance cost of the creation of a prototype chain while reusing the existing descriptors is way lower than re-creating a brand new class per realm.

class TestGlobal extends BaseGlobal { /* ... */ }

const iface = {
    /* ... */

    install(globalObject, privateData) {
        const Test = function() {
            return iface.setup(Object.create(TestGlobal.prototype), [], privateData);
        };
        // Apply static properties if necessary

        const proto = Object.create(globalObject.Base.prototype);
        Object.defineProperties(proto, Object.getOwnProperties(TestGlobal.prototype));

        Object.setPrototypeOf(Test, proto);

        Object.defineProperty(globalObject, "Test", {
            configurable: true,
            overridable: true,
            value: Test
        });

        // Apply [NamedConstructor] if necessary
    }
}
module.export = iface;

Pros:

  • No need for [WebIDL2JSFactory] anymore.
  • The new install method can be used to implement [NamedConstructor].
  • Solves the polyfill issue.
  • Fully spec compliant.

Cons:

  • Heavier solution. Would need to do a POC to evaluate the actual performance penalty of this approach.
  • Requires changes to jsdom. Now that we have a brand new prototype per realm, we need to pass the global object for every create() and createImpl() invocations, for it to retrieve the right prototype associated with the realm.

Before diving deeper into this, I would like getting some initial feedback on both approaches and see if it worth pursuing.

/cc @domenic @TimothyGu @Sebmaster @Zirro

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions