design icon indicating copy to clipboard operation
design copied to clipboard

JS API; asynchronous imports

Open wanderer opened this issue 9 years ago • 17 comments

Currently it doesn't look like there is a way for imports written in JS to return asynchronously. If this is true, would there be interest in adding a way?

I think the easiest way would be to return a promise

wanderer avatar Jul 08 '16 20:07 wanderer

In the MVP, I agree there isn't any direct support; wasm can really only take and receive numbers. However, one could generate or hand-write a JS glue layer that supports interfacing async JS. E.g., the glue layer could create a JS function closure that contained an exported wasm function and wasm closure state (probably an integer; i.e., the classic C-API-style void* closure arg) such that, when you called the JS wrapper function, it'd call the underlying wasm function, passing the closure state.

Post-MVP, with GC and WebIDL integration, I think we will be able to support more direct integration with async JS (and also a new breed of async Web APIs). In particular, I think we should provide the building blocks for wasm to directly construct callback functions (ultimately just code-pointer + data-pointer) which would then allow wasm to directly call, e.g., a Promise's then method.

lukewagner avatar Jul 08 '16 21:07 lukewagner

the glue layer could create a JS function closure that contained an exported wasm function and wasm closure state

This won't work if you also use start. (which might be fine, but confused me for a second when trying the above)

wanderer avatar Jul 08 '16 21:07 wanderer

Oh, are you talking specifically about the in-progress (or maybe it has advanced?) async-in-ES6-module-top-level proposal? That would require a new "async_start" section, I expect, but it looks like it's a bit farther out.

lukewagner avatar Jul 08 '16 22:07 lukewagner

@lukewagner looking to have a wasm module perform some async network I/O mediated by JS - any way of doing this with the current api?

kumavis avatar Aug 10 '16 17:08 kumavis

@lukewagner we are thinking about this again now in our prototype. One of the import method we define needs to do a async look up (if in node.js from the disk or from the network if in the browser).

However, one could generate or hand-write a JS glue layer that supports interfacing async JS. E.g., the glue layer could create a JS function closure that contained an exported wasm function and wasm closure state (probably an integer; i.e., the classic C-API-style void* closure arg) such that, when you called the JS wrapper function, it'd call the underlying wasm function, passing the closure state.

Yes, this is possible but we can't rely on the wasm code to export its closure state. So I don't think it will work for us. We don't have any control the wasm code being run. (apart from adding metering via a transform).

wanderer avatar Aug 10 '16 17:08 wanderer

@kumavis I might be misunderstanding what you mean, but I think there should be no problem; the implementation of emscripten_async_wget does this. Is there a specific issue you're having you could describe?

lukewagner avatar Aug 10 '16 17:08 lukewagner

@lukewagner I don't think that will work, we don't control the code is running in the wasm instance. We want to be able to give the wasn instance an import that has to do some async work `(import $async_stuff_may_happen 'ethereum' 'getStorage')' which third parties will use.

wanderer avatar Aug 10 '16 18:08 wanderer

@wanderer What do you mean by "has to do some async work"? In your example import, is getStorage attempting to synchronously return a result or does it take a callback into user code that it calls when the asynchronous work is completed? If the former, then you can't do that in JS or wasm if the implementation of getStorage needs to call async APIs. If the latter, then that's comparable to what emscripten_async_wget is doing so I don't see the problem: you just have these APIs take callbacks (in the form of two i32s: the first is an index into the function table that serves as the function-pointer and the second is a pointer into linear memory and serves as the classic C-API "closure" argument).

lukewagner avatar Aug 10 '16 19:08 lukewagner

@lukewagner would there be a way to refer to WebAssembly exports with an index? That could simplify this.

axic avatar Aug 10 '16 19:08 axic

What do you mean by "has to do some async work"?

getStorage need to pull load from value from either the network or disk. Both of which are not nice to make synchronous in JS.

if the former, then you can't do that in JS

I'm not sure I understand this. Here is an example of what I was thinking. In this example the imported function returns a value via a callback instead of a return. This pattern can be implemented in pure JS if new WebAssembly.Instance was a JS function.

example

 var instance;
 var importObj = {
        getStorage: function (key, cb) {
            lookupVal(function (val, err) {
               cb(val); // return value to the wasm instance
            });
        }
    };

instance = new WebAssembly.Instance(module, importObj);

you just have these APIs take callbacks

This is an option, but it would only apply to clients running in the browser. Non-browser clients will have full control of the VM so they don't have to worry about this. This mean we have to transform the code on the fly in the browser by looking for (call_import $getStorage (i32)) and converting into (call_import $getStorage (i32 i32)) and injecting code to save all the locals and injecting a callback function which reloads the local and continues. Which is all doable (but not very nice and hacky).

But it seem to me that async lookups would be common usecase and it might make sense to have that optionality in the JS api.

wanderer avatar Aug 10 '16 20:08 wanderer

@axic Yes, you could either put all the exports into a JS array or you could put them into an exported WebAssembly.Table which you can also access from JS.

This is an option, but it would only apply to clients running in the browser.

Ok, so you're talking about designing a single host-environment that can run modules in either a browser or non-browser wasm VMs. (Sounds cool!) I still think passing the pair of (func-ptr, closure) indices would be suitable since they could have the same meaning in a non-browser VM: func-ptr would index into either the exported functions or an exported table; closure into exported linear memory.

lukewagner avatar Aug 10 '16 20:08 lukewagner

Thanks @lukewagner

found emscripten_async_wget defined in js This calls Runtime.dynCall here

Runtime.dynCall('vi', callback, [allocate(intArrayFromString(_file), 'i8', ALLOC_STACK)]);

Runtime.dynCall is defined here which calls the exported wasm function here

return Module['dynCall_' + sig].apply(null, args);

result is calling

Module.dynCall_vi(callback, allocate(intArrayFromString(_file), 'i8', ALLOC_STACK))

vi refers to the function sig void (int) (no return, receive int)

@lukewagner That clarified things for me a bit but I think the next step would be to find some wasm example code that uses emscripten_async_wget or just emscripten_async_call

kumavis avatar Aug 11 '16 14:08 kumavis

@kumavis FWIW, much of that dynCall machinery is working around the lack of an asm.js equivalent to WebAssembly.Table; with wasm, Emscripten should be able to simply tbl.get(i)(x,y,z) from JS (although it seems like there is also some parameter marshalling going on there too which is orthogonal).

lukewagner avatar Aug 11 '16 14:08 lukewagner

Is someone interested in championing a concrete proposal here?

jfbastien avatar May 11 '17 18:05 jfbastien

@jfbastien so far we have managed work around this limitatio by adding callback to all the c interface. Another option to run the wasm instance in a webworker use a SAB and Atomic.wait to "pause", then do the async work in a the main thread.

But it might be less "hacky" if the imported function returned a promise wasm instance would resume after the promised resolved. Are there any immediate problems with this?

wanderer avatar Jun 03 '17 00:06 wanderer

I'd need to see more concrete code to understand clearly.

jfbastien avatar Jun 13 '17 00:06 jfbastien

@jfbastien here's a minimal example to demonstrate the issue https://gist.github.com/cdetrio/7f5486004b0054b5c08e0496cf3ab21f

cdetrio avatar Dec 31 '17 01:12 cdetrio