Skip to content

What happens when an async function is imported? #64

Open

Description

@Pyrolistical

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?

Activity

  1. zamfofex commented on Feb 26, 2025

    @zamfofex

    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.

  2. zamfofex commented on Feb 27, 2025

    @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.

  3. nicolo-ribaudo commented on Feb 27, 2025

    @nicolo-ribaudo
    Collaborator

    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.

  4. zamfofex commented on Feb 27, 2025

    @zamfofex

    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.

  5. guybedford commented on Jul 11, 2025

    @guybedford
    Collaborator

    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.

  6. tlively commented on Jul 12, 2025

    @tlively
    Member

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

  7. sbc100 commented on Jul 14, 2025

    @sbc100
    Member

    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.

  8. tlively commented on Jul 14, 2025

    @tlively
    Member

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

  9. sbc100 commented on Jul 14, 2025

    @sbc100
    Member

    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).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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