js-promise-integration icon indicating copy to clipboard operation
js-promise-integration copied to clipboard

Interaction between `WebAssembly.Suspending` and `WebAssembly.Function`?

Open hoodmane opened this issue 1 year ago • 3 comments

When trying to put a suspending function into a WebAssembly.Table, I found that in the current v8 implementation, WebAssembly.Function removes the suspending. I made a v8 bug report here with a full reproduction.

const suspendingF = new WebAssembly.Suspending(x => x);
const wasmFuncF = new WebAssembly.Function({parameters: ["externref"], results: ["externref"]}, suspendingF);
const wasmTable = new WebAssembly.Table({element: "funcref", initial: 1});
wasmTable.set(0, wasmFuncF);
async function asyncFunc() {
   return 7;
}
function jslog(val) {
  console.log("jslog", val);
}

And then have a wasm module that does:

  (func 
    (call $asyncFunc)
    (i32.const 0)
    (call_indirect $table (type $indirect_call))
    (call $jslog)
  )

Instead of logging 7, it logs Promise {<pending>}.

If I compose them the other way

const wasmFuncF = new WebAssembly.Function({parameters: ["externref"], results: ["externref"]}, x => x);
const suspendingF = new WebAssembly.Suspending(wasmFuncF);
const wasmTable = new WebAssembly.Table({element: "funcref", initial: 1});
wasmTable.set(0, suspendingF);

It raises TypeError: WebAssembly.Table.set(): Argument 1 is invalid for table: function-typed object must be null (if nullable) or a Wasm function object.

Does the spec have anything to say about the expected interaction of WebAssembly.Function and WebAssembly.Table? I should think at least new WebAssembly.Function(sig, new WebAssembly.Suspending(func)) should be required to work in a compliant implementation.

hoodmane avatar Jul 31 '24 08:07 hoodmane

new WebAssembly.Suspending(func) does NOT return a function value! As specified, it represents a wrapped function which is only usable in the context of an import during module instantiation.

Note that WebAssembly.Function is part of a different proposal that is still in phase 3.

fgmccabe avatar Jul 31 '24 18:07 fgmccabe

So according to the current spec, if I want to use a suspending WebAssembly function with call_indirect I should reexport it rather than using WebAssembly.Function? Does it seem reasonable to you to specify somewhere that when both type reflection and JSPI are implemented, they should interact in this way? Perhaps the most appropriate spot for this is in the type reflection spec, where it could say for instance that the behavior of new WebAssembly.Function is identical to:

function wasmFunctionPolyfill({ parameters, results }, f) {
  const builder = new WasmModuleBuilder();
  const functionIndex = builder.addImport("env", "f", { parameters, results });
  builder.addExport("exportedFunction", functionIndex);
  const buffer = builder.toBuffer();

  const module = new WebAssembly.Module(buffer);
  const instance = new WebAssembly.Instance(module, {"env": { f } });
  return instance.exports.exportedFunction;
}

With language to that effect in the reflection proposal, compliant implementers of both would be forced to make them interact the way that makes sense.

hoodmane avatar Aug 01 '24 08:08 hoodmane

I guess v8 is also not implementing the current spec for type reflection as it is written because as I read it:

  1. IsCallable(suspending) is supposed to return false
  2. The spec for the new WebAssembly.Function says

    To construct a new WebAssembly Function from a JavaScript callable object callable and FunctionType signature, perform the following steps:

    1. Assert: IsCallable(callable).

So that assertion should fail and it should throw. The current behavior of the v8 implementation is as follows:

  1. If callable has a [[wrappedFunction]] slot, replace callablewithcallable's [[wrappedFunction]]` slot.
  2. ... original steps here

Explicitly what I would like is for that language to say:

  1. If v has a [[wrappedFunction]] internal slot: i. Let func be v's [[wrappedFunction]] slot. ii. Assert $IsCallable$(func). iii. Create a suspending function from func and functype, and let funcaddr be the result. iv. return the result of creating a new Exported Function from funcaddr
  2. ... original steps here

hoodmane avatar Aug 01 '24 08:08 hoodmane