WebAssembly / js-string-builtins

JS String Builtins
Other
8 stars 6 forks source link

appears to make polyfills of `shareable` features impossible #6

Closed bakkot closed 9 months ago

bakkot commented 1 year ago

If I'm reading this right, this would make certain built-in functions postMessageable.

That makes those functions impossible to polyfill. Existing features sometimes need to be polyfilled when their behavior is updated - e.g. when AbortController was added, fetch needed to be polyfilled on old browsers to support the new behavior.

User-defined functions cannot be postMessage'd. So making any function postMessageable means it cannot ever be fully polyfilled. Also, the existence of a polyfill for something with a compile-time import would either cause a LinkError or prevent postMessageing the module. That seems bad.

It also means such functions can't be virtualized, which is also bad, though for pure computation it matters less.

conrad-watt commented 1 year ago

I think we've been evaluating builtins as more like a collection of additional importable Wasm operations, instead of as JS functions (I'm not saying we should be doing this, just trying to explicitly surface what I think have been our assumptions). We don't normally expect that newly introduced Wasm operations are polyfillable inline - a Web site wanting to support old browsers generally needs to just provide a version of the Wasm module without the unsupported operations.

The analogy here isn't totally precise, as a Wasm module attempting to import builtins could still be used in an older browser if the imports were polyfilled (with the caveats about postMessage above), but I think we would consider this situation "better" than the normal expectations of polyfillability. Also, we're slowly working towards a proposal for Wasm to get shared functions, which if successful might allow a more seamless polyfill in future situations like this.

Two immediate thoughts:

Maybe we would want to restrict built-ins from being directly callable from JS, since they're intended to be compiled to inline machine code in Wasm as opposed to a full function call?

Within the Wasm module, do we want to explicitly mark these import types as shared (e.g. in the binary)? This would seem to better fit the direction of travel if we eventually aim to have polyfills using shared Wasm functions.

bakkot commented 1 year ago

Ah, part of my concern stemmed from a misunderstanding that these would be marking existing things (like String.fromCodePoints) as shareable, rather than providing new things which are intended only to be used from wasm. I agree this isn't that much of a concern if these are wasm-only.

eqrion commented 1 year ago

Maybe we would want to restrict built-ins from being directly callable from JS, since they're intended to be compiled to inline machine code in Wasm as opposed to a full function call?

As builtins are intended to be instances of WebAssembly.Function, it'd be a bit challenging to prevent them from being called by JS. I would guess it'd be strictly more work for an engine to do?

Within the Wasm module, do we want to explicitly mark these import types as shared (e.g. in the binary)? This would seem to better fit the direction of travel if we eventually aim to have polyfills using shared Wasm functions.

What's the motivation for this? Allowing the imported funcs to have a shareable type in the future? My assumption was that shared <: noshared and so imports could request a non-shared func and receive a shared func.

conrad-watt commented 1 year ago

My assumption was that shared <: noshared

~Hmm - this definitely can't be true for anything mutable that can store refs like a global,~

EDIT: no longer sure about the above, so re-writing my justification :)

Currently this isn't true for shared memories since engines might want to compile shared and noshared memories with different bounds checking strategies, but maybe we could get away with it for functions? I'd need to think through all the implications. By default I think the assumption should be that this doesn't hold in the Wasm type system.

After thinking things through further though, I think it's ok for a pre-import provided at compile time to satisfy both shared and noshared imports, even if we don't have shared <: noshared when type-checking imports provided at instantiation-time, so long as pre-imports are whitelisted builtins rather than arbitrary user-defined functions. The idea is essentially that because of the whitelist, and the fact that builtins are restricted in the way they can mess with ambient state, we know that a builtin can be typed both as shared and noshared from the Wasm pov, even if user-defined functions have to pick one world or the other.

EDIT2:

or put another way, we could think of the "type hierarchy" for sharedness as like this

shared         noshared
  |               |
  -----------------
         |
       either

and currently only builtins can be typed with either, although maybe one day we could interpret Wasm functions with a shared annotation/bit set as typeable with either as well if it's clear this wouldn't damage implementation strategies. I think it would be hard for us to allow an either-typed memory with current implementations.

as opposed to

noshared 
  | 
shared

which I think can't be true in general right now

eqrion commented 9 months ago

The latest version of this proposal (after #8 landed) no longer has compile-time imports but uses opt-in host provided imports.

In this world, compiling a module with js-string-builtins enabled means that the host will provide the js-string imports. The resulting module is shareable, because any host provided import is reasonably expected to be available on all threads.

If you want to polyfill js-string-builtins for browser that don't support it, you would not opt-in to js-string-builtins, and instead provide your polyfill module during instantiation. For a shared module (i.e. postMessaged), you can provide a polyfill specific to the web worker when instantiating it.