Skip to content

Commit f338364

Browse files
committed
Add tests for the interface types output of wasm-bindgen
This commit expands the test suite with assertions about the output of the interface types pass in wasm-bindgen. The goal here is to actually assert that we produce the right output and have a suite of reference files to show how the interface types output is changing over time. The `reference` test suite added in the previous PR has been updated to work for interface types as well, generating `*.wit` file assertions which are printed via the `wit-printer` crate on crates.io. Along the way a number of bugs were fixed with the interface types output, such as: * Non-determinism in output caused by iteration of a `HashMap` * Avoiding JS generation entirely in interface types mode, ensuring that we don't export extraneous intrinsics that aren't otherwise needed. * Fixing location of the stack pointer for modules where it's GC'd out. It's now rooted in the aux section of wasm-bindgen so it's available to later passes, like the multi-value pass. * Interface types emission now works in debug mode, meaning the `--release` flag is no longer required. This previously did not work because the `__wbindgen_throw` intrinsic was required in debug mode. This comes about because of the `malloc_failure` and `internal_error` functions in the anyref pass. The purpose of these functions is to signal fatal runtime errors, if any, in a way that's usable to the user. For wasm interface types though we can replace calls to these functions with `unreachable` to avoid needing to import the intrinsic. This has the accidental side effect of making `wasm_bindgen::throw_str` "just work" with wasm interface types by aborting the program, but that's not actually entirely intended. It's hoped that a split of a `wasm-bindgen-core` crate would solve this issue for the future.
1 parent d7a4a77 commit f338364

21 files changed

+659
-155
lines changed

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

+6-21
Original file line numberDiff line numberDiff line change
@@ -227,7 +227,7 @@ impl<'a> Context<'a> {
227227
} => {
228228
js.push_str("let wasm;\n");
229229

230-
for (id, js) in sorted_iter(&self.wasm_import_definitions) {
230+
for (id, js) in crate::sorted_iter(&self.wasm_import_definitions) {
231231
let import = self.module.imports.get_mut(*id);
232232
import.module = format!("./{}.js", module_name);
233233
footer.push_str("\nmodule.exports.");
@@ -254,7 +254,7 @@ impl<'a> Context<'a> {
254254
"import * as wasm from './{}_bg.wasm';\n",
255255
module_name
256256
));
257-
for (id, js) in sorted_iter(&self.wasm_import_definitions) {
257+
for (id, js) in crate::sorted_iter(&self.wasm_import_definitions) {
258258
let import = self.module.imports.get_mut(*id);
259259
import.module = format!("./{}.js", module_name);
260260
footer.push_str("\nexport const ");
@@ -328,7 +328,7 @@ impl<'a> Context<'a> {
328328
OutputMode::Node {
329329
experimental_modules: false,
330330
} => {
331-
for (module, items) in sorted_iter(&self.js_imports) {
331+
for (module, items) in crate::sorted_iter(&self.js_imports) {
332332
imports.push_str("const { ");
333333
for (i, (item, rename)) in items.iter().enumerate() {
334334
if i > 0 {
@@ -351,7 +351,7 @@ impl<'a> Context<'a> {
351351
experimental_modules: true,
352352
}
353353
| OutputMode::Web => {
354-
for (module, items) in sorted_iter(&self.js_imports) {
354+
for (module, items) in crate::sorted_iter(&self.js_imports) {
355355
imports.push_str("import { ");
356356
for (i, (item, rename)) in items.iter().enumerate() {
357357
if i > 0 {
@@ -450,7 +450,7 @@ impl<'a> Context<'a> {
450450
imports_init.push_str(module_name);
451451
imports_init.push_str(" = {};\n");
452452
}
453-
for (id, js) in sorted_iter(&self.wasm_import_definitions) {
453+
for (id, js) in crate::sorted_iter(&self.wasm_import_definitions) {
454454
let import = self.module.imports.get_mut(*id);
455455
import.module = module_name.to_string();
456456
imports_init.push_str("imports.");
@@ -1852,7 +1852,7 @@ impl<'a> Context<'a> {
18521852
}
18531853

18541854
pub fn generate(&mut self) -> Result<(), Error> {
1855-
for (id, adapter) in sorted_iter(&self.wit.adapters) {
1855+
for (id, adapter) in crate::sorted_iter(&self.wit.adapters) {
18561856
let instrs = match &adapter.kind {
18571857
AdapterKind::Import { .. } => continue,
18581858
AdapterKind::Local { instructions } => instructions,
@@ -3055,21 +3055,6 @@ impl ExportedClass {
30553055
}
30563056
}
30573057

3058-
/// Returns a sorted iterator over a hash map, sorted based on key.
3059-
///
3060-
/// The intention of this API is to be used whenever the iteration order of a
3061-
/// `HashMap` might affect the generated JS bindings. We want to ensure that the
3062-
/// generated output is deterministic and we do so by ensuring that iteration of
3063-
/// hash maps is consistently sorted.
3064-
fn sorted_iter<K, V>(map: &HashMap<K, V>) -> impl Iterator<Item = (&K, &V)>
3065-
where
3066-
K: Ord,
3067-
{
3068-
let mut pairs = map.iter().collect::<Vec<_>>();
3069-
pairs.sort_by_key(|(k, _)| *k);
3070-
pairs.into_iter()
3071-
}
3072-
30733058
#[test]
30743059
fn test_generate_identifier() {
30753060
let mut used_names: HashMap<String, usize> = HashMap::new();

crates/cli-support/src/lib.rs

+115-80
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ mod js;
1818
mod multivalue;
1919
pub mod wasm2es6js;
2020
mod wit;
21+
mod throw2unreachable;
2122

2223
pub struct Bindgen {
2324
input: Input,
@@ -45,14 +46,22 @@ pub struct Bindgen {
4546
pub struct Output {
4647
module: walrus::Module,
4748
stem: String,
49+
generated: Generated,
50+
}
51+
52+
enum Generated {
53+
InterfaceTypes,
54+
Js(JsGenerated),
55+
}
56+
57+
struct JsGenerated {
58+
mode: OutputMode,
4859
js: String,
4960
ts: String,
50-
mode: OutputMode,
51-
typescript: bool,
5261
snippets: HashMap<String, Vec<String>>,
5362
local_modules: HashMap<String, String>,
5463
npm_dependencies: HashMap<String, (PathBuf, String)>,
55-
wasm_interface_types: bool,
64+
typescript: bool,
5665
}
5766

5867
#[derive(Clone)]
@@ -354,54 +363,69 @@ impl Bindgen {
354363
}
355364
}
356365

366+
// If wasm interface types are enabled then the `__wbindgen_throw`
367+
// intrinsic isn't available but it may be used by our runtime, so
368+
// change all calls to this function to calls to `unreachable` instead.
369+
// See more documentation in the pass documentation itself.
370+
if self.wasm_interface_types {
371+
throw2unreachable::run(&mut module);
372+
}
373+
374+
// Using all of our metadata convert our module to a multi-value using
375+
// module if applicable.
376+
if self.multi_value {
377+
if !self.wasm_interface_types {
378+
anyhow::bail!(
379+
"Wasm multi-value is currently only available when \
380+
Wasm interface types is also enabled"
381+
);
382+
}
383+
multivalue::run(&mut module)
384+
.context("failed to transform return pointers into multi-value Wasm")?;
385+
}
386+
357387
// We've done a whole bunch of transformations to the wasm module, many
358388
// of which leave "garbage" lying around, so let's prune out all our
359389
// unnecessary things here.
360390
gc_module_and_adapters(&mut module);
361391

362-
let aux = module
363-
.customs
364-
.delete_typed::<wit::WasmBindgenAux>()
365-
.expect("aux section should be present");
366-
let mut adapters = module
367-
.customs
368-
.delete_typed::<wit::NonstandardWitSection>()
369-
.unwrap();
370-
371-
// Now that our module is massaged and good to go, feed it into the JS
372-
// shim generation which will actually generate JS for all this.
373-
let (npm_dependencies, (js, ts)) = {
392+
// We're ready for the final emission passes now. If we're in wasm
393+
// interface types mode then we execute the various passes there and
394+
// generate a valid interface typess section into the wasm module.
395+
//
396+
// Otherwise we execute the JS generation passes to actually emit
397+
// JS/TypeScript/etc. The output here is unused in wasm interfac
398+
let generated = if self.wasm_interface_types {
399+
wit::section::add(&mut module)
400+
.context("failed to generate a standard interface types section")?;
401+
Generated::InterfaceTypes
402+
} else {
403+
let aux = module
404+
.customs
405+
.delete_typed::<wit::WasmBindgenAux>()
406+
.expect("aux section should be present");
407+
let adapters = module
408+
.customs
409+
.delete_typed::<wit::NonstandardWitSection>()
410+
.unwrap();
374411
let mut cx = js::Context::new(&mut module, self, &adapters, &aux)?;
375412
cx.generate()?;
376-
let npm_dependencies = cx.npm_dependencies.clone();
377-
(npm_dependencies, cx.finalize(stem)?)
413+
let (js, ts) = cx.finalize(stem)?;
414+
Generated::Js(JsGenerated {
415+
snippets: aux.snippets.clone(),
416+
local_modules: aux.local_modules.clone(),
417+
mode: self.mode.clone(),
418+
typescript: self.typescript,
419+
npm_dependencies: cx.npm_dependencies.clone(),
420+
js,
421+
ts,
422+
})
378423
};
379424

380-
if self.wasm_interface_types {
381-
multivalue::run(&mut module, &mut adapters)
382-
.context("failed to transform return pointers into multi-value Wasm")?;
383-
wit::section::add(&mut module, &aux, &adapters)
384-
.context("failed to generate a standard wasm bindings custom section")?;
385-
} else {
386-
if self.multi_value {
387-
anyhow::bail!(
388-
"Wasm multi-value is currently only available when \
389-
Wasm interface types is also enabled"
390-
);
391-
}
392-
}
393-
394425
Ok(Output {
395426
module,
396427
stem: stem.to_string(),
397-
snippets: aux.snippets.clone(),
398-
local_modules: aux.local_modules.clone(),
399-
npm_dependencies,
400-
js,
401-
ts,
402-
mode: self.mode.clone(),
403-
typescript: self.typescript,
404-
wasm_interface_types: self.wasm_interface_types,
428+
generated,
405429
})
406430
}
407431

@@ -554,8 +578,10 @@ fn unexported_unused_lld_things(module: &mut Module) {
554578

555579
impl Output {
556580
pub fn js(&self) -> &str {
557-
assert!(!self.wasm_interface_types);
558-
&self.js
581+
match &self.generated {
582+
Generated::InterfaceTypes => panic!("no js with interface types output"),
583+
Generated::Js(gen) => &gen.js,
584+
}
559585
}
560586

561587
pub fn wasm(&self) -> &walrus::Module {
@@ -571,24 +597,24 @@ impl Output {
571597
}
572598

573599
fn _emit(&mut self, out_dir: &Path) -> Result<(), Error> {
574-
let wasm_name = if self.wasm_interface_types {
575-
self.stem.clone()
576-
} else {
577-
format!("{}_bg", self.stem)
600+
let wasm_name = match &self.generated {
601+
Generated::InterfaceTypes => self.stem.clone(),
602+
Generated::Js(_) => format!("{}_bg", self.stem),
578603
};
579604
let wasm_path = out_dir.join(wasm_name).with_extension("wasm");
580605
fs::create_dir_all(out_dir)?;
581606
let wasm_bytes = self.module.emit_wasm();
582607
fs::write(&wasm_path, wasm_bytes)
583608
.with_context(|| format!("failed to write `{}`", wasm_path.display()))?;
584609

585-
if self.wasm_interface_types {
586-
return Ok(());
587-
}
610+
let gen = match &self.generated {
611+
Generated::InterfaceTypes => return Ok(()),
612+
Generated::Js(gen) => gen,
613+
};
588614

589615
// Write out all local JS snippets to the final destination now that
590616
// we've collected them from all the programs.
591-
for (identifier, list) in self.snippets.iter() {
617+
for (identifier, list) in gen.snippets.iter() {
592618
for (i, js) in list.iter().enumerate() {
593619
let name = format!("inline{}.js", i);
594620
let path = out_dir.join("snippets").join(identifier).join(name);
@@ -598,15 +624,15 @@ impl Output {
598624
}
599625
}
600626

601-
for (path, contents) in self.local_modules.iter() {
627+
for (path, contents) in gen.local_modules.iter() {
602628
let path = out_dir.join("snippets").join(path);
603629
fs::create_dir_all(path.parent().unwrap())?;
604630
fs::write(&path, contents)
605631
.with_context(|| format!("failed to write `{}`", path.display()))?;
606632
}
607633

608-
if self.npm_dependencies.len() > 0 {
609-
let map = self
634+
if gen.npm_dependencies.len() > 0 {
635+
let map = gen
610636
.npm_dependencies
611637
.iter()
612638
.map(|(k, v)| (k, &v.1))
@@ -617,29 +643,29 @@ impl Output {
617643

618644
// And now that we've got all our JS and TypeScript, actually write it
619645
// out to the filesystem.
620-
let extension = if self.mode.nodejs_experimental_modules() {
646+
let extension = if gen.mode.nodejs_experimental_modules() {
621647
"mjs"
622648
} else {
623649
"js"
624650
};
625651
let js_path = out_dir.join(&self.stem).with_extension(extension);
626-
fs::write(&js_path, reset_indentation(&self.js))
652+
fs::write(&js_path, reset_indentation(&gen.js))
627653
.with_context(|| format!("failed to write `{}`", js_path.display()))?;
628654

629-
if self.typescript {
655+
if gen.typescript {
630656
let ts_path = js_path.with_extension("d.ts");
631-
fs::write(&ts_path, &self.ts)
657+
fs::write(&ts_path, &gen.ts)
632658
.with_context(|| format!("failed to write `{}`", ts_path.display()))?;
633659
}
634660

635-
if self.mode.nodejs() {
661+
if gen.mode.nodejs() {
636662
let js_path = wasm_path.with_extension(extension);
637-
let shim = self.generate_node_wasm_import(&self.module, &wasm_path);
663+
let shim = gen.generate_node_wasm_import(&self.module, &wasm_path);
638664
fs::write(&js_path, shim)
639665
.with_context(|| format!("failed to write `{}`", js_path.display()))?;
640666
}
641667

642-
if self.typescript {
668+
if gen.typescript {
643669
let ts_path = wasm_path.with_extension("d.ts");
644670
let ts = wasm2es6js::typescript(&self.module)?;
645671
fs::write(&ts_path, ts)
@@ -648,7 +674,9 @@ impl Output {
648674

649675
Ok(())
650676
}
677+
}
651678

679+
impl JsGenerated {
652680
fn generate_node_wasm_import(&self, m: &Module, path: &Path) -> String {
653681
let mut imports = BTreeSet::new();
654682
for import in m.imports.iter() {
@@ -720,17 +748,14 @@ impl Output {
720748
}
721749

722750
fn gc_module_and_adapters(module: &mut Module) {
723-
// First up we execute walrus's own gc passes, and this may enable us to
724-
// delete entries in the `implements` section of the nonstandard wasm
725-
// interface types section. (if the import is GC'd, then the implements
726-
// annotation is no longer needed).
727-
//
728-
// By deleting adapter functions that may enable us to further delete more
729-
// functions, so we run this in a loop until we don't actually delete any
730-
// adapter functions.
731751
loop {
752+
// Fist up, cleanup the native wasm module. Note that roots can come
753+
// from custom sections, namely our wasm interface types custom section
754+
// as well as the aux section.
732755
walrus::passes::gc::run(module);
733756

757+
// ... and afterwards we can delete any `implements` directives for any
758+
// imports that have been deleted.
734759
let imports_remaining = module
735760
.imports
736761
.iter()
@@ -740,20 +765,30 @@ fn gc_module_and_adapters(module: &mut Module) {
740765
.customs
741766
.get_typed_mut::<wit::NonstandardWitSection>()
742767
.unwrap();
743-
let mut deleted_implements = Vec::new();
744-
section.implements.retain(|pair| {
745-
if imports_remaining.contains(&pair.0) {
746-
true
747-
} else {
748-
deleted_implements.push(pair.2);
749-
false
750-
}
751-
});
752-
if deleted_implements.len() == 0 {
768+
section.implements.retain(|pair| imports_remaining.contains(&pair.0));
769+
770+
// ... and after we delete the `implements` directive we try to
771+
// delete some adapters themselves. If nothing is deleted, then we're
772+
// good to go. If something is deleted though then we may have free'd up
773+
// some functions in the main module to get deleted, so go again to gc
774+
// things.
775+
if !section.gc() {
753776
break;
754777
}
755-
for id in deleted_implements {
756-
section.adapters.remove(&id);
757-
}
758778
}
759779
}
780+
781+
/// Returns a sorted iterator over a hash map, sorted based on key.
782+
///
783+
/// The intention of this API is to be used whenever the iteration order of a
784+
/// `HashMap` might affect the generated JS bindings. We want to ensure that the
785+
/// generated output is deterministic and we do so by ensuring that iteration of
786+
/// hash maps is consistently sorted.
787+
fn sorted_iter<K, V>(map: &HashMap<K, V>) -> impl Iterator<Item = (&K, &V)>
788+
where
789+
K: Ord,
790+
{
791+
let mut pairs = map.iter().collect::<Vec<_>>();
792+
pairs.sort_by_key(|(k, _)| *k);
793+
pairs.into_iter()
794+
}

0 commit comments

Comments
 (0)