wasm-bindgen icon indicating copy to clipboard operation
wasm-bindgen copied to clipboard

Add ability to pass imports when initializing module

Open Me1000 opened this issue 2 years ago • 5 comments

Motivation

First off let me preface, I'm very new to rust and wasm, so there may already be a way to do what I'm about to ask, or a good reason why it doesn't exist today.

Right now it looks like wasm_bindgen only supports passing js_namespace and an explicit module when trying to expose JS functions to Rust.

This leads to a very static way of doing things, but I'd like to be able to pass the imports directly to the module when it's being initialized. In my example I'm exposing a print function that takes a single string argument, but depending on my runtime I might want to swap out print for alert, console.log, or even my own implementation that prints rows to a table on my web page.

As far as I can tell there's no built in way to do this.

Proposed Solution

I was able to manually edit the generated JS file to achieve what I wanted like so:

async function init(input, passedImports) {
    if (typeof input === 'undefined' || typeof input === "object") {
        passedImports = input;
        input = new URL('my_wasm_module.wasm', import.meta.url);
    }
    const imports = getImports(passedImports);

(Since I'm not using input this was the trivial way to do it, but obviously the complete solution would need to handle the case where input was no undefined too)

Then getImports turns into something like this:

function getImports(passedImports) {
    const imports = {};
    imports.wbg = {};
    imports.wbg.__wbg_print_b82da52707b85e2a = function(arg0, arg1) {
        passedImports.print(getStringFromWasm0(arg0, arg1));
    };

    return imports;
}

Then in my rust code I set the function's js_namespace to passedImports like so:

#[wasm_bindgen]
extern "C" {
    #[wasm_bindgen(js_namespace = passedImports)]
    pub fn print(s: &str);
}

#[wasm_bindgen]
pub fn echo(text: &str) {
    print(&format!("{}", text));
}

(the echo example is a bit contrived, but it's for illustrative purposes)

And finally when I initialize my wasm module I can pass in an object that exposes the imports I want.

const passedImports = {
    print: function(text) {
        const newLine = document.createElement("div");
        newLine.innerText = text;
        document.getElementById("lines").appendChild(newLine);
    }
};

import init from "./pkg/my_wasm_module.js";
init(passedImports).then(module => {
    module.echo("This will call my passed imports 'print' function.");
});

What I did was basically just a hacky workaround to proof of concept this feature. I'm guessing you'd probably want a different kind of argument passed to the wasm_bindgen macro rather than overloading the existing js_namespace one.

Is this something you'd consider?

Me1000 avatar Aug 22 '22 19:08 Me1000

This seems to be similar to what was proposed in 2917 and 2920, with the concern noted in the latter applying here, too.

I would also like to at least have the option of passing custom imports and accessing more than just the exports on the finished instance, but with the current focus understandably being on building passably durable tooling, maybe that need would be better addressed by another custom target for wasm-bindgen that's intended from the ground up to be more open and "hackable".

Gearme avatar Aug 22 '22 20:08 Gearme

I just stumbled over this issue and was quite surprised that it's not possible. To me, it appears to be the most straightforward way to provide the necessary imports.

Maybe I'm missing something, but I also don't quite follow the concerns stated in https://github.com/rustwasm/wasm-bindgen/pull/2920#issuecomment-1142220600. At the moment, if I don't specify any of module, raw_module, etc., the imports will be pulled from the global this. This appears to be much more error-prone than requiring them to be passed explicitly. It tempts people into things like

import init from './wasm-bindgen-out/my-crate.js'
globalThis.my_function = () => { .... }
await init()

which can go very wrong.

I guess it's too much of a breaking change, but I would actually prefer to require all imports to be passed to init unless explicitly requested otherwise (via module, js_namespace, etc.)

bspot avatar Oct 20 '22 22:10 bspot

Was looking into the same functionality - especially for wasm-in-wasm context where I'd like to pass an import directly from the browser runtime directly through the host wasm to the guest wasm. It seems to be more difficult than it should be.

zees-dev avatar Aug 30 '23 23:08 zees-dev

I also think this would be really useful. The current setup is strongly geared towards using modules and bundling, which makes the resulting JavaScript less flexible to use. The concerns in #2920 seem to specifically be that passing raw imports would mess with wasm_bindgen's state. However, the same would not be true when adding an option to pass wrapped imports as proposed here.

laurmaedje avatar Sep 19 '23 19:09 laurmaedje

Wanted to +1 this idea. And if possible, also throw some typescript support into the mix, please!

I managed to work my around this for my needs, but it required an absurd amount of hacking with Babel to be able to inject imports.

Involved scanning the import section of the WASM binary, stripping the hardcoded imports from the JS file and replacing them with the injected imports, plus analysing the glue functions to generate type information (e.g. where there's a passStringToWasm0, that's understood as string return type).

This is obviously a tad fragile, would much rather have it built into wasm-bindgen 🙂

gustavohenke avatar Nov 07 '23 10:11 gustavohenke