Skip to content

Commit

Permalink
Expose helpers to allow synchronous initialization (#2924)
Browse files Browse the repository at this point in the history
* Expose helpers to allow synchronous initialization

* fixup: fix tests
  • Loading branch information
d3lm committed Jun 8, 2022
1 parent b7b4ef3 commit 3822e67
Show file tree
Hide file tree
Showing 12 changed files with 216 additions and 18 deletions.
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;");
}
}

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");

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}) {{
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

0 comments on commit 3822e67

Please sign in to comment.