Skip to content

Commit 2aac606

Browse files
committed
Support emitting direct imports in wasm files
Support was previously (re-)added in rustwasm#1654 for importing direct JS values into a WebAssembly module by completely skipping JS shim generation. This commit takes that PR one step further by *also* embedding a direct import in the wasm file, where supported. The wasm file currently largely just imports from the JS shim file that we generate, but this allows it to directly improt from ES modules where supported and where possible. Note that like rustwasm#1654 this only happens when the function signature doesn't actually require any conversions to happen in JS (such as handling closures). For imports from ES modules, local snippets, or inline JS they'll all have their import directives directly embedded into the final WebAssembly binary without any shims necessary to hook it all up. For imports from the global namespace or possibly vendor-prefixed items these still unconditionally require an import shim to be generated because there's no way to describe that import in an ES-friendly way (yet). There's a few consequences of this commit which are also worth noting: * The logic in `wasm-bindgen` where it gracefully handles (to some degree) not-defined items now only is guaranteed to be applied to the global namespace. If you import from a module, it'll be an instantiation time error rather than today's runtime error when the import is called. * Handling imports in the wasm module not registered with `#[wasm_bindgen]` has become more strict. Previously these imports were basically ignored, leaving them up for interpretation depending on the output format. The changes for each output target are: * `bundler` - not much has changed here. Previously these ignored imports would have been treated as ES module imports, and after this commit there might just be some more of these imports for bundlers to resolve. * `web` - previously the ignored imports would likely cause instantiation failures because the import object never actually included a binding for other imports. After this commit though the JS glue which instantiates the module now interprets all unrecognized wasm module imports as ES module imports, emitting an `import` directive. This matches what we want for the direct import functionality, and is also largely what we want for modules in general. * `nodejs` - previously ignored imports were handled in the translation shim for Node to generate `require` statements, so they were actually "correctly handled" sort of with module imports. The handling of this hasn't changed, and reflects what we want for direct imports of values where loading a wasm module in Node ends up translating the module field of each import to a `require`. * `no-modules` - this is very similar to the `web` target where previously this didn't really work one way or the other because we'd never fill in more fields of the import object when instantiating the module. After this PR though this is a hard-error to have unrecognized imports from `#[wasm_bindgen]` with the `no-modules` output type, because we don't know how to handle the imports. Note that this touches on rustwasm#1584 and will likely break the current use case being mentioned there. I think though that this tightening up of how we handle imports is what we'll want in the long run where everything is interpreted as modules, and we'll need to figure out best how wasi fits into this. This commit is unlikely to have any real major immediate effects. The goal here is to continue to inch us towards a world where there's less and less JS glue necessary and `wasm-bindgen` is just a polyfill for web standards that otherwise all already exist. Also note that there's no explicitly added tests for this since this is largely just a refactoring of an internal implementation detail of `wasm-bindgen`, but the main `wasm` test suite has many instances of this path being taken, for example having imports like: (import "tests/wasm/duplicates_a.js" "foo" (func $__wbg_foo_969c253238f136f0 (type 1))) (import "tests/wasm/duplicates_b.js" "foo" (func $__wbg_foo_027958cb2e320a94 (type 0))) (import "./snippets/wasm-bindgen-3dff2bc911f0a20c/inline0.js" "trivial" (func $__wbg_trivial_75e27c84882af23b (type 1))) (import "./snippets/wasm-bindgen-3dff2bc911f0a20c/inline0.js" "incoming_bool" (func $__wbg_incomingbool_0f2d9f55f73a256f (type 0)))
1 parent 38b8232 commit 2aac606

File tree

2 files changed

+126
-23
lines changed

2 files changed

+126
-23
lines changed

crates/cli-support/src/js/mod.rs

+111-23
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ use crate::webidl::{AuxValue, Binding};
66
use crate::webidl::{JsImport, JsImportName, NonstandardWebidlSection, WasmBindgenAux};
77
use crate::{Bindgen, EncodeInto, OutputMode};
88
use failure::{bail, Error, ResultExt};
9-
use std::collections::{BTreeMap, HashMap, HashSet};
9+
use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
1010
use std::fs;
1111
use std::path::{Path, PathBuf};
1212
use walrus::{ExportId, ImportId, MemoryId, Module};
@@ -248,7 +248,7 @@ impl<'a> Context<'a> {
248248
OutputMode::NoModules { global } => {
249249
js.push_str("const __exports = {};\n");
250250
js.push_str("let wasm;\n");
251-
init = self.gen_init(needs_manual_start);
251+
init = self.gen_init(needs_manual_start, None)?;
252252
footer.push_str(&format!(
253253
"self.{} = Object.assign(init, __exports);\n",
254254
global
@@ -309,7 +309,7 @@ impl<'a> Context<'a> {
309309
// as the default export of the module.
310310
OutputMode::Web => {
311311
self.imports_post.push_str("let wasm;\n");
312-
init = self.gen_init(needs_manual_start);
312+
init = self.gen_init(needs_manual_start, Some(&mut imports))?;
313313
footer.push_str("export default init;\n");
314314
}
315315
}
@@ -435,7 +435,11 @@ impl<'a> Context<'a> {
435435
)
436436
}
437437

438-
fn gen_init(&mut self, needs_manual_start: bool) -> (String, String) {
438+
fn gen_init(
439+
&mut self,
440+
needs_manual_start: bool,
441+
mut imports: Option<&mut String>,
442+
) -> Result<(String, String), Error> {
439443
let module_name = "wbg";
440444
let mem = self.module.memories.get(self.memory);
441445
let (init_memory1, init_memory2) = if let Some(id) = mem.import {
@@ -495,6 +499,30 @@ impl<'a> Context<'a> {
495499
imports_init.push_str(";\n");
496500
}
497501

502+
let extra_modules = self
503+
.module
504+
.imports
505+
.iter()
506+
.filter(|i| !self.wasm_import_definitions.contains_key(&i.id()))
507+
.filter(|i| {
508+
// Importing memory is handled specially in this area, so don't
509+
// consider this a candidate for importing from extra modules.
510+
match i.kind {
511+
walrus::ImportKind::Memory(_) => false,
512+
_ => true,
513+
}
514+
})
515+
.map(|i| &i.module)
516+
.collect::<BTreeSet<_>>();
517+
for (i, extra) in extra_modules.iter().enumerate() {
518+
let imports = match &mut imports {
519+
Some(list) => list,
520+
None => bail!("cannot import from modules (`{}`) with `--no-modules`", extra),
521+
};
522+
imports.push_str(&format!("import * as __wbg_star{} from '{}';\n", i, extra));
523+
imports_init.push_str(&format!("imports['{}'] = __wbg_star{};\n", extra, i));
524+
}
525+
498526
let js = format!(
499527
"\
500528
function init(module{init_memory_arg}) {{
@@ -553,7 +581,7 @@ impl<'a> Context<'a> {
553581
imports_init = imports_init,
554582
);
555583

556-
(js, ts)
584+
Ok((js, ts))
557585
}
558586

559587
fn write_classes(&mut self) -> Result<(), Error> {
@@ -1104,15 +1132,17 @@ impl<'a> Context<'a> {
11041132
//
11051133
// If `ptr` and `len` are both `0` then that means it's `None`, in that case we rely upon
11061134
// the fact that `getObject(0)` is guaranteed to be `undefined`.
1107-
self.global("
1135+
self.global(
1136+
"
11081137
function getCachedStringFromWasm(ptr, len) {
11091138
if (ptr === 0) {
11101139
return getObject(len);
11111140
} else {
11121141
return getStringFromWasm(ptr, len);
11131142
}
11141143
}
1115-
");
1144+
",
1145+
);
11161146
Ok(())
11171147
}
11181148

@@ -1730,7 +1760,8 @@ impl<'a> Context<'a> {
17301760

17311761
JsImportName::LocalModule { module, name } => {
17321762
let unique_name = generate_identifier(name, &mut self.defined_identifiers);
1733-
add_module_import(format!("./snippets/{}", module), name, &unique_name);
1763+
let module = self.config.local_module_name(module);
1764+
add_module_import(module, name, &unique_name);
17341765
unique_name
17351766
}
17361767

@@ -1739,11 +1770,10 @@ impl<'a> Context<'a> {
17391770
snippet_idx_in_crate,
17401771
name,
17411772
} => {
1773+
let module = self
1774+
.config
1775+
.inline_js_module_name(unique_crate_identifier, *snippet_idx_in_crate);
17421776
let unique_name = generate_identifier(name, &mut self.defined_identifiers);
1743-
let module = format!(
1744-
"./snippets/{}/inline{}.js",
1745-
unique_crate_identifier, snippet_idx_in_crate,
1746-
);
17471777
add_module_import(module, name, &unique_name);
17481778
unique_name
17491779
}
@@ -2014,16 +2044,11 @@ impl<'a> Context<'a> {
20142044
.types
20152045
.get::<ast::WebidlFunction>(binding.webidl_ty)
20162046
.unwrap();
2017-
let js = match import {
2047+
match import {
20182048
AuxImport::Value(AuxValue::Bare(js))
20192049
if !variadic && !catch && self.import_does_not_require_glue(binding, webidl) =>
20202050
{
2021-
self.expose_not_defined();
2022-
let name = self.import_name(js)?;
2023-
format!(
2024-
"typeof {name} == 'function' ? {name} : notDefined('{name}')",
2025-
name = name,
2026-
)
2051+
self.direct_import(id, js)
20272052
}
20282053
_ => {
20292054
if assert_no_shim {
@@ -2048,11 +2073,11 @@ impl<'a> Context<'a> {
20482073
cx.invoke_import(&binding, import, bindings, args, variadic, prelude)
20492074
},
20502075
)?;
2051-
format!("function{}", js)
2076+
self.wasm_import_definitions
2077+
.insert(id, format!("function{}", js));
2078+
Ok(())
20522079
}
2053-
};
2054-
self.wasm_import_definitions.insert(id, js);
2055-
Ok(())
2080+
}
20562081
}
20572082

20582083
fn import_does_not_require_glue(
@@ -2078,6 +2103,69 @@ impl<'a> Context<'a> {
20782103
)
20792104
}
20802105

2106+
/// Emit a direct import directive that hooks up the `js` value specified to
2107+
/// the wasm import `id`.
2108+
fn direct_import(&mut self, id: ImportId, js: &JsImport) -> Result<(), Error> {
2109+
// If there's no field projection happening here and this is a direct
2110+
// import from an ES-looking module, then we can actually just hook this
2111+
// up directly in the wasm file itself. Note that this is covered in the
2112+
// various output formats as well:
2113+
//
2114+
// * `bundler` - they think wasm is an ES module anyway
2115+
// * `web` - we're sure to emit more `import` directives during
2116+
// `gen_init` and we update the import object accordingly.
2117+
// * `nodejs` - the polyfill we have for requiring a wasm file as a node
2118+
// module will naturally emit `require` directives for the module
2119+
// listed on each wasm import.
2120+
// * `no-modules` - imports aren't allowed here anyway from other
2121+
// modules and an error is generated.
2122+
if js.fields.len() == 0 {
2123+
match &js.name {
2124+
JsImportName::Module { module, name } => {
2125+
let import = self.module.imports.get_mut(id);
2126+
import.module = module.clone();
2127+
import.name = name.clone();
2128+
return Ok(());
2129+
}
2130+
JsImportName::LocalModule { module, name } => {
2131+
let module = self.config.local_module_name(module);
2132+
let import = self.module.imports.get_mut(id);
2133+
import.module = module;
2134+
import.name = name.clone();
2135+
return Ok(());
2136+
}
2137+
JsImportName::InlineJs {
2138+
unique_crate_identifier,
2139+
snippet_idx_in_crate,
2140+
name,
2141+
} => {
2142+
let module = self
2143+
.config
2144+
.inline_js_module_name(unique_crate_identifier, *snippet_idx_in_crate);
2145+
let import = self.module.imports.get_mut(id);
2146+
import.module = module;
2147+
import.name = name.clone();
2148+
return Ok(());
2149+
}
2150+
2151+
// Fall through below to requiring a JS shim to create an item
2152+
// that we can import. These are plucked from the global
2153+
// environment so there's no way right now to describe these
2154+
// imports in an ES module-like fashion.
2155+
JsImportName::Global { .. } | JsImportName::VendorPrefixed { .. } => {}
2156+
}
2157+
}
2158+
2159+
self.expose_not_defined();
2160+
let name = self.import_name(js)?;
2161+
let js = format!(
2162+
"typeof {name} == 'function' ? {name} : notDefined('{name}')",
2163+
name = name,
2164+
);
2165+
self.wasm_import_definitions.insert(id, js);
2166+
Ok(())
2167+
}
2168+
20812169
/// Generates a JS snippet appropriate for invoking `import`.
20822170
///
20832171
/// This is generating code for `binding` where `bindings` has more type

crates/cli-support/src/lib.rs

+15
Original file line numberDiff line numberDiff line change
@@ -360,6 +360,21 @@ impl Bindgen {
360360
typescript: self.typescript,
361361
})
362362
}
363+
364+
fn local_module_name(&self, module: &str) -> String {
365+
format!("./snippets/{}", module)
366+
}
367+
368+
fn inline_js_module_name(
369+
&self,
370+
unique_crate_identifier: &str,
371+
snippet_idx_in_crate: usize,
372+
) -> String {
373+
format!(
374+
"./snippets/{}/inline{}.js",
375+
unique_crate_identifier, snippet_idx_in_crate,
376+
)
377+
}
363378
}
364379

365380
fn reset_indentation(s: &str) -> String {

0 commit comments

Comments
 (0)