Skip to content

Using builtins for prototype and descriptor creation #33

@eqrion

Description

@eqrion

Here's a sketch of an idea that avoids JS glue code and custom sections. It keeps custom descriptor types, but uses imported builtins for creating prototypes and associating them with descriptors.

I probably missed a couple use cases or requirements, so let me know if this seems feasible or not.

Calling imports from globals

The first thing we'd need is a core wasm extension to allow globals to call imported functions in initializer expressions. By itself this is dangerous due to the possiblity of re-entrance to a partially constructed instance from the called imported function. For example:

(module
    (* (x) => x() *)
    (import "" "invoke" (func $invoke (param funcref) (result i32)))

    (global i32 (call $invoke (ref.func $invokeMe)))
    (global $nonNullableGlobal (ref struct) ...)

    (func $invokeMe (result i32)
        ;; called by the $invoke function through funcref during global initialization
        ;; can do bad things like access nonNullableGlobal before it is initialized
        ...
    )
)

The issue is that globals are defined after the function section, and can therefore acquire references to defined functions (which close over the partially constructed instance) and pass those out to the wider world.

One workaround to this is to relax the order of the global section and allow repetition of it. Then we could say calling an imported function is allowed if the global is defined before the function section (the 'early' globals). Globals defined after the function section (the 'late' globals) would not be able to call imports, but could acquire funcrefs defined in the module.

Builtins for creating prototypes

If we can call imports during global initialization, then we could define builtins for creating prototypes.

One idea is to define a builtin which allocates a wasm struct and gives it field names that JS can access.

"wasm:js" "struct.new_with_names"
  (param $structFieldValues) (param externref $structFieldNames) -> (ref $struct)
  where $struct is a struct type with $structFieldValues. len($structFieldValues) == $structFieldNames

The resulting struct object is a normal wasm struct, but JS can get/set the fields through the names that was provided on creating. You could think of this as sugar for the simple case of Wasm-JS interop where you just want the struct fields to show up as own properties.

With this builtin, modules could construct all of their prototype objects using wasm structs in globals. The prototype struct fields that are methods would need to be initialized later, because the prototype globals are 'early'. I think if you have mutable nullable fields for each method, the prototype structs could be initialized in the start function.

This imported builtin is polymorphic depending on the type signature you import it with. The func type you use in the import contains the struct type, which is used to generate the correct logic for the builtin. Polyfilling this builtin is probably possible, but a bit convoluted. A name mangling approach would be easier to polyfill, but I think name mangling should be avoided.

We could also have a struct.new_with_names_and_proto builtin for if a prototype is in a chain.

Builtin for creating custom descriptor with prototype

Similar to above, we could then create a builtin for allocating a descriptor struct with an associated prototype.

"wasm:js" "struct.new_desc_with_proto"
  (param $structFieldValues) (param externref $proto) -> (ref $struct)
  where $struct is a struct type with $structFieldValues.

This could be used in the early global section to create the descriptors. If the descriptors have funcrefs from the module on them, this would require those to be mutable and initialized during the start function (this might be an issue for vtables, or are those stored in a struct pointed to by the descriptor?).

Function receivers

One other thing the custom section approach does is create wasm funcs that take the JS receiver as the first parameter. An alternative using builtins would be:

"wasm:js-function" "to-method"
  (param funcref) (result funcref)

It would construct a new wasm function that wraps the inner wasm function. When called from JS it has the alternative behavior of requiring a receiver and passing it as the first wasm param.

#31 proposes another alternative which doesn't allocate a wrapper wasm function, but mutates the behavior of the existing one. That could also work here as a builtin.

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