WebAssembly/module-linking

Do we care about polyfillibility?

Closed this issue · 3 comments

The explainer talks about how an adapted module is essentially the same shape as a core module, which means we don't need to define any additional capability for the JavaScript API.

This made me consider if its possible to ahead of time transpile adapter modules into core modules as a form of polyfill.

While attempting to do so I ran into a case which isn't possible to polyfill cleanly and I wonder if there is any desire to change the spec to better align with the idea of "adapted module is essentially the same shape as a core module".

Problem

Adapter modules allow importing of func directly

(adapter module
  (import "answer" (func (result i32)))
)

Adapter modules uses a single level import where as core modules uses two levels. Therefore, this adapter module isn't representable as a core module as-is. We would need to add additional bits to handle this case.

Solution 1: Make up a second level import

One solution just to make up a a second level import. This is a pretty good solution as the polyfill can just toss all the single level imports into this.

(module
  (import "adapter module top level imports" "answer"  (func (result i32)))
)

Pros

  • Leave the spec as-is.

Cons

  • Can't round-trip adapter module to and from core modules.

Solution 2: Disallow direct func imports, only allow instance and module imports.

All over the explainer, the main type of imports shown are instance and module, which both express two level imports. So if we disallow direct func imports, we can force everybody to write either of the following.

(adapter module
  (import "the" (instance
    (export "answer" (func (result i32)))
  ))
)
(adapter module
  (import "the" (module
    (export "answer" (func (result i32)))
  ))
)

Both of these are transpilable to (minus the instance vs module handling the polyfill would need to do)

(module
  (import "the" "answer"  (func (result i32)))
)

Pros

  • Smaller spec by removing functionality.

Cons

  • Forced people to make up a name they don't care about.

It's definitely a goal that the entire component model (including module-linking) is polyfillable in terms of MVP JS API + Core WebAssembly. But I think it's reasonable for the polyfill to be able to produce both: (1) a collection of core .wasm and .js files implementing, collectively, a single adapter module, (2) a polyfill for WA.instantiate*() that is used instead the native WA.instantiate*(). That is: the polyfill shouldn't have to produce outputs that can be fed directly to MVP WA.instantiate*().

With that assumption, the WA.instantiate*() can be a JS-implemented polyfill that "does the right thing" for single-level imports. I think this shouldn't require Solutions 1 or 2 above: the adapter module (with the single-level imports) can translate to a .js file that exports a JS function that is called by the WA.instantiate*() polyfill.

Separately, an ESM-integration polyfill could be even cleaner: the polyfill can just emit JS ESMs that internally compile and instantiate (now using top-level await instead of awful base64-encoding hacks \o/) the core wasm modules.

Lastly: with either of these designs, I think it should never be necessary to transform the original core wasm modules: the polyfill would purely be generating JS glue to do what adapter modules do.

Makes sense if you allow yourself to polyfill the JS side too. Thanks

Haha, there is also a 3rd solution

(module
  (import "" "answer" (func (result i32)))
)
const wasmInstance = new WebAssembly.Instance(wasmModule, {
  '': {
    answer: () => 42
  }
});

That core module just works.