esm-integration icon indicating copy to clipboard operation
esm-integration copied to clipboard

What happens when an async function is imported?

Open Pyrolistical opened this issue 3 years ago • 4 comments

For example, if we modify the function import example and make getCount async.

;; main.wat --> main.wasm
(module
  (import "./counter.js" "getCount" (func $getCount (func (result i32))))
)
// counter.js
async function getCount() {
    const response = await fetch('//api/get-count');
    ...
    return count;
}
export {getCount};

Given existing behaviour, it would crash at runtime. But should we do better?

Should this rely on or interact with https://github.com/WebAssembly/js-promise-integration?

Pyrolistical avatar Jun 26 '22 04:06 Pyrolistical

I feel like this needs to be decided before implementations (browsers or Node) start to implement and ship this feature by default!

It would be really unfortunate if this didn’t use the JSPI in any way, given how prominent async functions and promises have become over time in JS.

I’m not sure how this should integrate with JSPI, but I really wish this could have been discussed before starting to push for implementations.

I think the worst case scenario is that this becomes unimplementable, and people having to resort to using the existing WebAssembly.compile APIs or having to resort to hacks like Asyncify for eternity in order to interact with promises from Wasm.

zamfofex avatar Feb 26 '25 10:02 zamfofex

I suppose it’d always be possible for JS modules to explicitly wrap their exports with WebAssembly.Suspending to be used from Wasm, and to explicitly wrap imports from Wasm with WebAssembly.promising. But that feels really cumbersome and non‐ergonomic!

It would be nice to be able to explicitly mark imports as implicitly Suspending and exports as implicitly promising somehow. I think a somewhat straight‐forward approach would be to use async: in the import/export names.

E.g. if you want to import an async function readFile and have it be wrapped implicitly in Suspending, then you’d instead import async:readFile. Then you’d have to make sure that all functions that call it (transitively) would be exported with an async: suffix, which would mark it to be wrapped in promising.

zamfofex avatar Feb 27 '25 14:02 zamfofex

Would that async: marker be needed in every function, or just in the ones that are imported/exported?

On the export side this could also be done by listing the async functions in a custom section, and then the ESM implicit wrapper would know which functions to wrap in primising.

nicolo-ribaudo avatar Feb 27 '25 15:02 nicolo-ribaudo

Internal functions don’t have names, so it doesn’t make sense to ask whether they would need to be named starting with async:, I think. If you’re asking whether JSPI requires those functions to be marked in some way, the answer is no, as far as I know. As long as the import is wrapped with Suspending and the export is marked with promising, then everything “just works”.

From what I understand, it’s also possible for a Wasm function that conditionally calls a Suspending function to be called without promising in the case it doesn’t enter the branch that calls any Suspending functions. (It will throw/trap if it does call a Suspending function in that case.) For that purpose, it could be nice to be able to import the function directly too (without the promising wrapping), which would be possible with my idea.

E.g. imagine a Wasm module exports async:readFile. If JS code imports readFile, it will be wrapped in promising. If JS code imports async:readFile directly, it won’t be wrapped in promising.

zamfofex avatar Feb 27 '25 16:02 zamfofex

With the new reservation of wasm-js: and wasm: on imported names and exported names, we could in theory use these to indicate automatic suspending and promising wrappers.

For example, consider:

(module
  (import "env" "fetchData" (func $fetchData (result i32)))
  (func $main (result i32)
    call $fetchData
  )
  (export "main" (func $main))
)

where we want to wrap the fetchData import in WebAssembly.Suspending and we want to wrap the main export in in WebAssembly.promising.

It might be possible to define these in the ESM integration through wasm-js:suspending/ and wasm-js:promising/ as a convention on the import and export names providing the module as:

(module
  (import "env" "wasm-js:promising/fetchData" (func $fetchData (result i32)))
  (func $main (result i32)
    call $fetchData
  )
  (export "wasm-js:suspending/main" (func $main))
)

where the ESM integration would then automatically handle the wrapping on the provided WebAssembly namespace instance.

Will leave this open for discussion / interest / further interest further as it can be an addition at any point in time further and doesn't need to delay initial shipping since we have reserved these namespaces.

If there is strong interest we can also look at adding it sooner.

guybedford avatar Jul 11 '25 17:07 guybedford

@sbc100, @brendandahl, do we need JSPI support for Emscripten's use of ESM integration? cc @fgmccabe, too.

tlively avatar Jul 12 '25 02:07 tlively

I think JSPI is currently on the list of features not-yet-support in emscripten's ESM integration mode.

We can probably make it with either with or without this declarative syntax, but I imagine it would be sub-optimal.

sbc100 avatar Jul 14 '25 19:07 sbc100

How would you make it work without a declarative solution like this?

tlively avatar Jul 14 '25 22:07 tlively

How would you make it work without a declarative solution like this?

I was thinking of two possible options:

Option 1: Generate code that add wrappers:

import { foo_raw, bar_raw } from "./mymodule.wasm";

// Create JSPI wrappers for any function that need it.
var foo = wrap(foo_raw);
var bar = wrap(var_raw);

Option 2: Use import * as to get an enumerable set of wasm exports:

import * as wasmExports from "./mymodule.wasm";

for (var i in wasmExports) {
  if (shouldWrap(wasmExports[i])) {
    wasmExports[i] = wrap(wasmExports[i]);
  }
}

In both cases this is automatically-generated JS code that comes out of emscripten.

Option 1 is not great since is generates O(n) extra JS glue code.

Option 2 is O(1) but not great since is defeats (or makes harder at least) dead code elimination (IIUC).

sbc100 avatar Jul 14 '25 22:07 sbc100