Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Expose method to allow synchronous initialization #2924

Merged
merged 2 commits into from Jun 8, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Binary file added .DS_Store
Binary file not shown.
2 changes: 1 addition & 1 deletion .github/workflows/main.yml
Expand Up @@ -228,7 +228,7 @@ jobs:
ln -snf `pwd`/target/debug/wasm-bindgen $(dirname `which cargo`)/wasm-bindgen
- run: mv _package.json package.json && npm install && rm package.json
- run: |
for dir in `ls examples | grep -v README | grep -v asm.js | grep -v raytrace | grep -v without-a-bundler | grep -v wasm-in-web-worker | grep -v websockets | grep -v webxr | grep -v deno`; do
for dir in `ls examples | grep -v README | grep -v asm.js | grep -v raytrace | grep -v without-a-bundler | grep -v wasm-in-web-worker | grep -v websockets | grep -v webxr | grep -v deno | grep -v synchronous-instantiation`; do
(cd examples/$dir &&
ln -fs ../../node_modules . &&
npm run build -- --output-path ../../exbuild/$dir) || exit 1;
Expand Down
1 change: 1 addition & 0 deletions Cargo.toml
Expand Up @@ -89,6 +89,7 @@ members = [
"examples/webxr",
"examples/without-a-bundler",
"examples/without-a-bundler-no-modules",
"examples/synchronous-instantiation",
"tests/no-std",
]

Expand Down
73 changes: 58 additions & 15 deletions crates/cli-support/src/js/mod.rs
Expand Up @@ -469,7 +469,8 @@ impl<'a> Context<'a> {
OutputMode::Web => {
self.imports_post.push_str("let wasm;\n");
init = self.gen_init(needs_manual_start, Some(&mut imports))?;
footer.push_str("export default init;\n");
footer.push_str("export { initSync }\n");
footer.push_str("export default init;");
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ I have noticed there was two newlines at the end of the generated JS so I removed one newline.

}
}

Expand Down Expand Up @@ -637,12 +638,29 @@ impl<'a> Context<'a> {
// So using "declare" everywhere for at least the NoModules option.
// Also in (at least) the NoModules, the `init()` method is renamed to `wasm_bindgen()`.
let setup_function_declaration;
let mut sync_init_function = String::new();
let declare_or_export;
if self.config.mode.no_modules() {
declare_or_export = "declare";
setup_function_declaration = "declare function wasm_bindgen";
} else {
declare_or_export = "export";

sync_init_function.push_str(&format!("\
/**\n\
* Synchronously compiles the given `bytes` and instantiates the WebAssembly module.\n\
*\n\
* @param {{BufferSource}} bytes\n\
{memory_doc}\
*\n\
* @returns {{InitOutput}}\n\
*/\n\
export function initSync(bytes: BufferSource{memory_param}): InitOutput;\n\n\
",
memory_doc = memory_doc,
memory_param = memory_param
));

setup_function_declaration = "export default function init";
}
Ok(format!(
Expand All @@ -652,6 +670,7 @@ impl<'a> Context<'a> {
{declare_or_export} interface InitOutput {{\n\
{output}}}\n\
\n\
{sync_init_function}\
/**\n\
* If `module_or_path` is {{RequestInfo}} or {{URL}}, makes a request and\n\
* for everything else, calls `WebAssembly.instantiate` directly.\n\
Expand All @@ -665,6 +684,7 @@ impl<'a> Context<'a> {
(module_or_path{}: InitInput | Promise<InitInput>{}): Promise<InitOutput>;\n",
memory_doc, arg_optional, memory_param,
output = output,
sync_init_function = sync_init_function,
declare_or_export = declare_or_export,
setup_function_declaration = setup_function_declaration,
))
Expand Down Expand Up @@ -733,11 +753,11 @@ impl<'a> Context<'a> {
// Initialize the `imports` object for all import definitions that we're
// directed to wire up.
let mut imports_init = String::new();
if self.wasm_import_definitions.len() > 0 {
imports_init.push_str("imports.");
imports_init.push_str(module_name);
imports_init.push_str(" = {};\n");
}

imports_init.push_str("imports.");
imports_init.push_str(module_name);
imports_init.push_str(" = {};\n");
Comment on lines +757 to +759
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ I have noticed that if a module has no imports and we don't initialize the imports with the module_name (wbg) that it will break when it tries to initialize the memory, because it gets assigned to imports.wbg.memory.


for (id, js) in crate::sorted_iter(&self.wasm_import_definitions) {
let import = self.module.imports.get_mut(*id);
import.module = module_name.to_string();
Expand Down Expand Up @@ -814,26 +834,49 @@ impl<'a> Context<'a> {
}}
}}

async function init(input{init_memory_arg}) {{
{default_module_path}
function getImports() {{
const imports = {{}};
{imports_init}
return imports;
}}

if (typeof input === 'string' || (typeof Request === 'function' && input instanceof Request) || (typeof URL === 'function' && input instanceof URL)) {{
input = fetch(input);
}}

function initMemory(imports, maybe_memory) {{
{init_memory}
}}

const {{ instance, module }} = await load(await input, imports);

function finalizeInit(instance, module) {{
wasm = instance.exports;
init.__wbindgen_wasm_module = module;

{post_instantiate}
{start}
return wasm;
}}

function initSync(bytes{init_memory_arg}) {{
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ This function gets exported to synchronously initialize the module.

const imports = getImports();

initMemory(imports{init_memory_arg});

const module = new WebAssembly.Module(bytes);
const instance = new WebAssembly.Instance(module, imports);

return finalizeInit(instance, module);
}}

async function init(input{init_memory_arg}) {{
{default_module_path}
const imports = getImports();

if (typeof input === 'string' || (typeof Request === 'function' && input instanceof Request) || (typeof URL === 'function' && input instanceof URL)) {{
input = fetch(input);
}}

initMemory(imports{init_memory_arg});

const {{ instance, module }} = await load(await input, imports);

return finalizeInit(instance, module);
}}
",
init_memory_arg = init_memory_arg,
default_module_path = default_module_path,
Expand Down
4 changes: 2 additions & 2 deletions crates/cli/tests/wasm-bindgen/main.rs
Expand Up @@ -289,7 +289,7 @@ fn omit_default_module_path_target_web() {
"\
async function init(input) {

const imports = {};",
const imports = getImports();",
));
}

Expand All @@ -309,7 +309,7 @@ fn omit_default_module_path_target_no_modules() {
"\
async function init(input) {

const imports = {};",
const imports = getImports();",
));
}

Expand Down
11 changes: 11 additions & 0 deletions examples/synchronous-instantiation/Cargo.toml
@@ -0,0 +1,11 @@
[package]
name = "synchronous-instantiation"
version = "0.1.0"
authors = ["The wasm-bindgen Developers"]
edition = "2018"

[lib]
crate-type = ["cdylib"]

[dependencies]
wasm-bindgen = "0.2.80"
23 changes: 23 additions & 0 deletions examples/synchronous-instantiation/README.md
@@ -0,0 +1,23 @@
# Synchronous Instantiation

[View documentation for this example online][dox]

[dox]: https://rustwasm.github.io/docs/wasm-bindgen/examples/synchronous-instantiation.html

You can build the example locally with:

```
$ wasm-pack build --target web
```

Then serve this directory in your favourite webserver and navigate to `host:port`
to open the index.html in your browser:

```
# static server from https://crates.io/crates/https
http

# or use python
python2 -m SimpleHTTPServer
python3 -m http.server
```
6 changes: 6 additions & 0 deletions examples/synchronous-instantiation/build.sh
@@ -0,0 +1,6 @@
#!/bin/sh

set -ex

wasm-pack build --target web
python3 -m http.server
46 changes: 46 additions & 0 deletions examples/synchronous-instantiation/index.html
@@ -0,0 +1,46 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<script>
/**
* First off we spawn a Web Worker. That's where our lib will be used. Note that
* we set the `type` to `module` to enable support for ES modules.
*/
const worker = new Worker("/worker.js", { type: "module" });

/**
* Here we listen for messages from the worker.
*/
worker.onmessage = ({ data }) => {
const { type } = data;

switch (type) {
case "FETCH_WASM": {
/**
* The worker wants to fetch the bytes for the module and for that we can use the `fetch` API.
* Then we convert the response into an `ArrayBuffer` and transfer the bytes back to the worker.
*
* @see https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API
* @see https://developer.mozilla.org/en-US/docs/Glossary/Transferable_objects
*/
fetch("/pkg/synchronous_instantiation_bg.wasm")
.then((response) => response.arrayBuffer())
.then((bytes) => {
worker.postMessage(bytes, [bytes]);
});
break;
}
default: {
break;
}
}
};
</script>
</body>
</html>
12 changes: 12 additions & 0 deletions examples/synchronous-instantiation/src/lib.rs
@@ -0,0 +1,12 @@
use wasm_bindgen::prelude::*;

#[wasm_bindgen]
extern "C" {
#[wasm_bindgen(js_namespace = console)]
fn log(value: &str);
}

#[wasm_bindgen]
pub fn greet(name: &str) {
log(&format!("Hello, {}!", name));
}
23 changes: 23 additions & 0 deletions examples/synchronous-instantiation/worker.js
@@ -0,0 +1,23 @@
import * as wasm from "./pkg/synchronous_instantiation.js";

self.onmessage = ({ data: bytes }) => {
/**
* When we receive the bytes as an `ArrayBuffer` we can use that to
* synchronously initialize the module as opposed to asynchronously
* via the default export. The synchronous method internally uses
* `new WebAssembly.Module()` and `new WebAssembly.Instance()`.
*/
wasm.initSync(bytes);

/**
* Once initialized we can call our exported `greet()` functions.
*/
wasm.greet("Dominic");
};

/**
* Once the Web Worker was spwaned we ask the main thread to fetch the bytes
* for the WebAssembly module. Once fetched it will send the bytes back via
* a `postMessage` (see above).
*/
self.postMessage({ type: "FETCH_WASM" });
33 changes: 33 additions & 0 deletions guide/src/examples/synchronous-instantiation.md
@@ -0,0 +1,33 @@
# Synchronous Instantiation

[View full source code][code]

[code]: https://github.com/rustwasm/wasm-bindgen/tree/master/examples/synchronous-instantiation

This example shows how to synchronously initialize a WebAssembly module as opposed to [asynchronously][without-bundler]. In most cases, the default way of asynchronously initializing a module will suffice. However, there might be use cases where you'd like to lazy load a module on demand and synchronously compile and instantiate it. Note that this only works off the main thread and since compilation and instantiation of large modules can be expensive you should only use this method if it's absolutely required in your use case. Otherwise you should use the [default method][without-bundler].

For this deployment strategy bundlers like Webpack are not required. For more information on deployment see the [dedicated
documentation][deployment].

First let's take a look at our tiny lib:

```rust
{{#include ../../../examples/synchronous-instantiation/src/lib.rs}}
```

Next, let's have a look at the `index.html`:

```html
{{#include ../../../examples/synchronous-instantiation/index.html}}
```

Otherwise the rest of the magic happens in `worker.js`:

```js
{{#include ../../../examples/synchronous-instantiation/worker.js}}
```

And that's it! Be sure to read up on the [deployment options][deployment] to see what it means to deploy without a bundler.

[deployment]: ../reference/deployment.html
[without-bundler]: ./without-a-bundler.html