From 8e56cdacc57d4caa420a5a34baf399a93f86dca5 Mon Sep 17 00:00:00 2001 From: Alex Crichton Date: Tue, 3 Dec 2019 11:16:44 -0600 Subject: [PATCH] Rewrite wasm-bindgen with updated interface types proposal (#1882) This commit is a pretty large scale rewrite of the internals of wasm-bindgen. No user-facing changes are expected as a result of this PR, but due to the scale of changes here it's likely inevitable that at least something will break. I'm hoping to get more testing in though before landing! The purpose of this PR is to update wasm-bindgen to the current state of the interface types proposal. The wasm-bindgen tool was last updated when it was still called "WebIDL bindings" so it's been awhile! All support is now based on https://github.com/bytecodealliance/wasm-interface-types which defines parsers/binary format/writers/etc for wasm-interface types. This is a pretty massive PR and unfortunately can't really be split up any more afaik. I don't really expect realistic review of all the code here (or commits), but some high-level changes are: * Interface types now consists of a set of "adapter functions". The IR in wasm-bindgen is modeled the same way not. * Each adapter function has a list of instructions, and these instructions work at a higher level than wasm itself, for example with strings. * The wasm-bindgen tool has a suite of instructions which are specific to it and not present in the standard. (like before with webidl bindings) * The anyref/multi-value transformations are now greatly simplified. They're simply "optimization passes" over adapter functions, removing instructions that are otherwise present. This way we don't have to juggle so much all over the place, and instructions always have the same meaning. --- azure-pipelines.yml | 4 + crates/anyref-xform/Cargo.toml | 12 +- crates/anyref-xform/src/lib.rs | 22 +- crates/anyref-xform/tests/all.rs | 258 +++ .../anyref-xform/tests/anyref-param-owned.wat | 31 + crates/anyref-xform/tests/anyref-param.wat | 43 + .../tests/clone-ref-intrinsic.wat | 50 + .../anyref-xform/tests/drop-ref-intrinsic.wat | 37 + .../tests/import-anyref-owned.wat | 36 + .../anyref-xform/tests/import-anyref-ret.wat | 36 + crates/anyref-xform/tests/import-anyref.wat | 34 + crates/anyref-xform/tests/mixed-export.wat | 52 + crates/anyref-xform/tests/mixed.wat | 50 + crates/anyref-xform/tests/ret-anyref.wat | 33 + .../tests/table-grow-intrinsic.wat | 40 + .../tests/table-set-null-intrinsic.wat | 38 + crates/anyref-xform/tests/table.wat | 35 + crates/backend/src/codegen.rs | 8 +- crates/cli-support/Cargo.toml | 4 +- crates/cli-support/src/anyref.rs | 367 ++-- crates/cli-support/src/intrinsic.rs | 16 +- crates/cli-support/src/js/binding.rs | 1320 +++++++++++---- crates/cli-support/src/js/incoming.rs | 556 ------- crates/cli-support/src/js/mod.rs | 1476 +++++++++-------- crates/cli-support/src/js/outgoing.rs | 451 ----- crates/cli-support/src/lib.rs | 37 +- crates/cli-support/src/multivalue.rs | 77 + crates/cli-support/src/webidl/bindings.rs | 250 --- crates/cli-support/src/webidl/incoming.rs | 492 ------ crates/cli-support/src/webidl/outgoing.rs | 556 ------- crates/cli-support/src/webidl/standard.rs | 562 ------- crates/cli-support/src/wit/incoming.rs | 385 +++++ crates/cli-support/src/{webidl => wit}/mod.rs | 1234 +++++--------- crates/cli-support/src/wit/nonstandard.rs | 360 ++++ crates/cli-support/src/wit/outgoing.rs | 382 +++++ crates/cli-support/src/wit/section.rs | 414 +++++ crates/cli-support/src/wit/standard.rs | 359 ++++ crates/cli/Cargo.toml | 2 +- crates/multi-value-xform/Cargo.toml | 12 +- crates/multi-value-xform/src/lib.rs | 37 +- crates/multi-value-xform/tests/align.wat | 37 + crates/multi-value-xform/tests/all.rs | 225 +++ crates/multi-value-xform/tests/many.wat | 39 + crates/multi-value-xform/tests/simple.wat | 93 ++ crates/threads-xform/Cargo.toml | 2 +- crates/wasm-conventions/Cargo.toml | 2 +- crates/wasm-interpreter/Cargo.toml | 2 +- src/convert/impls.rs | 3 - src/convert/mod.rs | 1 + src/lib.rs | 35 +- tests/wasm/imports.js | 12 + tests/wasm/imports.rs | 26 + tests/wasm/slice.js | 75 +- tests/wasm/slice.rs | 24 +- 54 files changed, 5747 insertions(+), 4997 deletions(-) create mode 100644 crates/anyref-xform/tests/all.rs create mode 100644 crates/anyref-xform/tests/anyref-param-owned.wat create mode 100644 crates/anyref-xform/tests/anyref-param.wat create mode 100644 crates/anyref-xform/tests/clone-ref-intrinsic.wat create mode 100644 crates/anyref-xform/tests/drop-ref-intrinsic.wat create mode 100644 crates/anyref-xform/tests/import-anyref-owned.wat create mode 100644 crates/anyref-xform/tests/import-anyref-ret.wat create mode 100644 crates/anyref-xform/tests/import-anyref.wat create mode 100644 crates/anyref-xform/tests/mixed-export.wat create mode 100644 crates/anyref-xform/tests/mixed.wat create mode 100644 crates/anyref-xform/tests/ret-anyref.wat create mode 100644 crates/anyref-xform/tests/table-grow-intrinsic.wat create mode 100644 crates/anyref-xform/tests/table-set-null-intrinsic.wat create mode 100644 crates/anyref-xform/tests/table.wat delete mode 100644 crates/cli-support/src/js/incoming.rs delete mode 100644 crates/cli-support/src/js/outgoing.rs create mode 100644 crates/cli-support/src/multivalue.rs delete mode 100644 crates/cli-support/src/webidl/bindings.rs delete mode 100644 crates/cli-support/src/webidl/incoming.rs delete mode 100644 crates/cli-support/src/webidl/outgoing.rs delete mode 100644 crates/cli-support/src/webidl/standard.rs create mode 100644 crates/cli-support/src/wit/incoming.rs rename crates/cli-support/src/{webidl => wit}/mod.rs (51%) create mode 100644 crates/cli-support/src/wit/nonstandard.rs create mode 100644 crates/cli-support/src/wit/outgoing.rs create mode 100644 crates/cli-support/src/wit/section.rs create mode 100644 crates/cli-support/src/wit/standard.rs create mode 100644 crates/multi-value-xform/tests/align.wat create mode 100644 crates/multi-value-xform/tests/all.rs create mode 100644 crates/multi-value-xform/tests/many.wat create mode 100644 crates/multi-value-xform/tests/simple.wat diff --git a/azure-pipelines.yml b/azure-pipelines.yml index c0f30506..fd4321a2 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -96,6 +96,10 @@ jobs: displayName: "wasm-bindgen-cli-support tests" - script: cargo test -p wasm-bindgen-cli displayName: "wasm-bindgen-cli tests" + - script: cargo test -p wasm-bindgen-anyref-xform + displayName: "wasm-bindgen-anyref-xform tests" + - script: cargo test -p wasm-bindgen-multi-value-xform + displayName: "wasm-bindgen-multi-value-xform tests" - job: test_web_sys displayName: "Run web-sys crate tests" diff --git a/crates/anyref-xform/Cargo.toml b/crates/anyref-xform/Cargo.toml index b2c7db7a..e06899a0 100644 --- a/crates/anyref-xform/Cargo.toml +++ b/crates/anyref-xform/Cargo.toml @@ -13,4 +13,14 @@ edition = '2018' [dependencies] anyhow = "1.0" -walrus = "0.13.0" +walrus = "0.14.0" + +[dev-dependencies] +rayon = "1.0" +wasmprinter = "0.2" +wast = "3.0" +wat = "1.0" + +[[test]] +name = "all" +harness = false diff --git a/crates/anyref-xform/src/lib.rs b/crates/anyref-xform/src/lib.rs index bd981519..90d63872 100644 --- a/crates/anyref-xform/src/lib.rs +++ b/crates/anyref-xform/src/lib.rs @@ -44,6 +44,12 @@ pub struct Context { table: Option, } +pub struct Meta { + pub table: TableId, + pub alloc: Option, + pub drop_slice: Option, +} + struct Transform<'a> { cx: &'a mut Context, @@ -161,7 +167,7 @@ impl Context { }) } - pub fn run(&mut self, module: &mut Module) -> Result<(), Error> { + pub fn run(&mut self, module: &mut Module) -> Result { let table = self.table.unwrap(); // Inject a stack pointer global which will be used for managing the @@ -171,6 +177,7 @@ impl Context { let mut heap_alloc = None; let mut heap_dealloc = None; + let mut drop_slice = None; // Find exports of some intrinsics which we only need for a runtime // implementation. @@ -182,7 +189,8 @@ impl Context { match export.name.as_str() { "__wbindgen_anyref_table_alloc" => heap_alloc = Some(f), "__wbindgen_anyref_table_dealloc" => heap_dealloc = Some(f), - _ => {} + "__wbindgen_drop_anyref_slice" => drop_slice = Some(f), + _ => continue, } } let mut clone_ref = None; @@ -226,7 +234,13 @@ impl Context { heap_dealloc, stack_pointer, } - .run(module) + .run(module)?; + + Ok(Meta { + table, + alloc: heap_alloc, + drop_slice, + }) } } @@ -619,7 +633,7 @@ impl Transform<'_> { // with a fresh type we've been calculating so far. Give the function a // nice name for debugging and then we're good to go! let id = builder.finish(params, funcs); - let name = format!("{}_anyref_shim", name); + let name = format!("{} anyref shim", name); funcs.get_mut(id).name = Some(name); self.shims.insert(id); Ok((id, anyref_ty)) diff --git a/crates/anyref-xform/tests/all.rs b/crates/anyref-xform/tests/all.rs new file mode 100644 index 00000000..7d07cf1b --- /dev/null +++ b/crates/anyref-xform/tests/all.rs @@ -0,0 +1,258 @@ +//! A small test framework to execute a test function over all files in a +//! directory. +//! +//! Each file in the directory has its own `CHECK-ALL` annotation indicating the +//! expected output of the test. That can be automatically updated with +//! `BLESS=1` in the environment. Otherwise the test are checked against the +//! listed expectation. + +use anyhow::{anyhow, bail, Context, Result}; +use rayon::prelude::*; +use std::env; +use std::fs; +use std::path::{Path, PathBuf}; +use wast::parser::{Parse, Parser}; + +fn main() { + run("tests".as_ref(), runtest); +} + +fn runtest(test: &Test) -> Result { + let wasm = wat::parse_file(&test.file)?; + let mut walrus = walrus::Module::from_buffer(&wasm)?; + let mut cx = wasm_bindgen_anyref_xform::Context::default(); + cx.prepare(&mut walrus)?; + for directive in test.directives.iter() { + match &directive.kind { + DirectiveKind::Export(name) => { + let export = walrus + .exports + .iter() + .find(|e| e.name == *name) + .ok_or_else(|| anyhow!("failed to find export"))?; + cx.export_xform(export.id(), &directive.args, directive.ret_anyref); + } + DirectiveKind::Import(module, field) => { + let import = walrus + .imports + .iter() + .find(|e| e.module == *module && e.name == *field) + .ok_or_else(|| anyhow!("failed to find export"))?; + cx.import_xform(import.id(), &directive.args, directive.ret_anyref); + } + DirectiveKind::Table(idx) => { + cx.table_element_xform(*idx, &directive.args, directive.ret_anyref); + } + } + } + cx.run(&mut walrus)?; + walrus::passes::gc::run(&mut walrus); + let printed = wasmprinter::print_bytes(&walrus.emit_wasm())?; + Ok(printed) +} + +fn run(dir: &Path, run: fn(&Test) -> Result) { + let mut tests = Vec::new(); + find_tests(dir, &mut tests); + let filter = std::env::args().nth(1); + + let bless = env::var("BLESS").is_ok(); + let tests = tests + .iter() + .filter(|test| { + if let Some(filter) = &filter { + if let Some(s) = test.file_name().and_then(|s| s.to_str()) { + if !s.contains(filter) { + return false; + } + } + } + true + }) + .collect::>(); + + println!("\nrunning {} tests\n", tests.len()); + + let errors = tests + .par_iter() + .filter_map(|test| run_test(test, bless, run).err()) + .collect::>(); + + if !errors.is_empty() { + for msg in errors.iter() { + eprintln!("error: {:?}", msg); + } + + panic!("{} tests failed", errors.len()) + } + + println!("test result: ok. {} passed\n", tests.len()); +} + +fn run_test(test: &Path, bless: bool, run: fn(&Test) -> anyhow::Result) -> Result<()> { + (|| -> Result<_> { + let expected = Test::from_file(test)?; + let actual = run(&expected)?; + expected.check(&actual, bless)?; + Ok(()) + })() + .context(format!("test failed - {}", test.display()))?; + Ok(()) +} + +fn find_tests(path: &Path, tests: &mut Vec) { + for f in path.read_dir().unwrap() { + let f = f.unwrap(); + if f.file_type().unwrap().is_dir() { + find_tests(&f.path(), tests); + continue; + } + match f.path().extension().and_then(|s| s.to_str()) { + Some("wat") => {} + _ => continue, + } + tests.push(f.path()); + } +} + +struct Test { + file: PathBuf, + directives: Vec, + assertion: Option, +} + +struct Directive { + args: Vec<(usize, bool)>, + ret_anyref: bool, + kind: DirectiveKind, +} + +enum DirectiveKind { + Import(String, String), + Export(String), + Table(u32), +} + +impl Test { + fn from_file(path: &Path) -> Result { + let contents = fs::read_to_string(path)?; + let mut iter = contents.lines(); + let mut assertion = None; + let mut directives = Vec::new(); + while let Some(line) = iter.next() { + if line.starts_with("(; CHECK-ALL:") { + let mut pattern = String::new(); + while let Some(line) = iter.next() { + if line == ";)" { + break; + } + pattern.push_str(line); + pattern.push_str("\n"); + } + while pattern.ends_with("\n") { + pattern.pop(); + } + if iter.next().is_some() { + bail!("CHECK-ALL must be at the end of the file"); + } + assertion = Some(pattern); + continue; + } + + if !line.starts_with(";; @xform") { + continue; + } + let directive = &line[9..]; + let buf = wast::parser::ParseBuffer::new(directive)?; + directives.push(wast::parser::parse::(&buf)?); + } + Ok(Test { + file: path.to_path_buf(), + directives, + assertion, + }) + } + + fn check(&self, output: &str, bless: bool) -> Result<()> { + if bless { + update_output(&self.file, output) + } else if let Some(pattern) = &self.assertion { + if output == pattern { + return Ok(()); + } + bail!( + "expected\n {}\n\nactual\n {}", + pattern.replace("\n", "\n "), + output.replace("\n", "\n ") + ); + } else { + bail!( + "no test assertions were found in this file, but you can \ + rerun tests with `BLESS=1` to automatically add assertions \ + to this file" + ); + } + } +} + +fn update_output(path: &Path, output: &str) -> Result<()> { + let contents = fs::read_to_string(path)?; + let start = contents.find("(; CHECK-ALL:").unwrap_or(contents.len()); + + let mut new_output = String::new(); + for line in output.lines() { + new_output.push_str(line); + new_output.push_str("\n"); + } + let new = format!( + "{}\n\n(; CHECK-ALL:\n{}\n;)\n", + contents[..start].trim(), + new_output.trim_end() + ); + fs::write(path, new)?; + Ok(()) +} + +impl<'a> Parse<'a> for Directive { + fn parse(parser: Parser<'a>) -> wast::parser::Result { + use wast::kw; + wast::custom_keyword!(anyref_owned); + wast::custom_keyword!(anyref_borrowed); + wast::custom_keyword!(other); + + let kind = if parser.peek::() { + parser.parse::()?; + DirectiveKind::Import(parser.parse()?, parser.parse()?) + } else if parser.peek::() { + parser.parse::()?; + DirectiveKind::Export(parser.parse()?) + } else { + parser.parse::()?; + DirectiveKind::Table(parser.parse()?) + }; + let mut args = Vec::new(); + parser.parens(|p| { + let mut i = 0; + while !p.is_empty() { + if parser.peek::() { + parser.parse::()?; + args.push((i, true)); + } else if parser.peek::() { + parser.parse::()?; + args.push((i, false)); + } else { + parser.parse::()?; + } + i += 1; + } + Ok(()) + })?; + + let ret_anyref = parser.parse::>()?.is_some(); + Ok(Directive { + args, + ret_anyref, + kind, + }) + } +} diff --git a/crates/anyref-xform/tests/anyref-param-owned.wat b/crates/anyref-xform/tests/anyref-param-owned.wat new file mode 100644 index 00000000..fa3148b9 --- /dev/null +++ b/crates/anyref-xform/tests/anyref-param-owned.wat @@ -0,0 +1,31 @@ +;; @xform export "foo" (anyref_owned) + +(module + (func $foo (export "foo") (param i32)) + (func $alloc (export "__wbindgen_anyref_table_alloc") (result i32) + i32.const 0) + (func $dealloc (export "__wbindgen_anyref_table_dealloc") (param i32)) +) + +(; CHECK-ALL: +(module + (type (;0;) (func (result i32))) + (type (;1;) (func (param i32))) + (type (;2;) (func (param anyref))) + (func $foo anyref shim (type 2) (param anyref) + (local i32) + call $alloc + local.tee 1 + local.get 0 + table.set 0 + local.get 1 + call $foo) + (func $alloc (type 0) (result i32) + i32.const 0) + (func $foo (type 1) (param i32)) + (func $dealloc (type 1) (param i32)) + (table (;0;) 32 anyref) + (export "foo" (func $foo anyref shim)) + (export "__wbindgen_anyref_table_alloc" (func $alloc)) + (export "__wbindgen_anyref_table_dealloc" (func $dealloc))) +;) diff --git a/crates/anyref-xform/tests/anyref-param.wat b/crates/anyref-xform/tests/anyref-param.wat new file mode 100644 index 00000000..a9058858 --- /dev/null +++ b/crates/anyref-xform/tests/anyref-param.wat @@ -0,0 +1,43 @@ +;; @xform export "foo" (anyref_borrowed) + +(module + (func $foo (export "foo") (param i32)) + (func $alloc (export "__wbindgen_anyref_table_alloc") (result i32) + i32.const 0) + (func $dealloc (export "__wbindgen_anyref_table_dealloc") (param i32)) +) + +(; CHECK-ALL: +(module + (type (;0;) (func (result i32))) + (type (;1;) (func (param i32))) + (type (;2;) (func (param anyref))) + (func $foo anyref shim (type 2) (param anyref) + (local i32) + global.get 0 + i32.const 1 + i32.sub + local.tee 1 + global.set 0 + local.get 1 + local.get 0 + table.set 0 + local.get 1 + call $foo + local.get 1 + ref.null + table.set 0 + local.get 1 + i32.const 1 + i32.add + global.set 0) + (func $alloc (type 0) (result i32) + i32.const 0) + (func $foo (type 1) (param i32)) + (func $dealloc (type 1) (param i32)) + (table (;0;) 32 anyref) + (global (;0;) (mut i32) (i32.const 32)) + (export "foo" (func $foo anyref shim)) + (export "__wbindgen_anyref_table_alloc" (func $alloc)) + (export "__wbindgen_anyref_table_dealloc" (func $dealloc))) +;) diff --git a/crates/anyref-xform/tests/clone-ref-intrinsic.wat b/crates/anyref-xform/tests/clone-ref-intrinsic.wat new file mode 100644 index 00000000..47d384df --- /dev/null +++ b/crates/anyref-xform/tests/clone-ref-intrinsic.wat @@ -0,0 +1,50 @@ +;; @xform export "foo" (anyref_owned) anyref_owned + +(module + (import "__wbindgen_placeholder__" "__wbindgen_object_clone_ref" + (func $clone (param i32) (result i32))) + (func $foo (export "foo") (param i32) (result i32) + local.get 0 + call $clone) + (func $alloc (export "__wbindgen_anyref_table_alloc") (result i32) + i32.const 0) + (func $dealloc (export "__wbindgen_anyref_table_dealloc") (param i32)) +) + +(; CHECK-ALL: +(module + (type (;0;) (func (result i32))) + (type (;1;) (func (param i32))) + (type (;2;) (func (param i32) (result i32))) + (type (;3;) (func (param anyref) (result anyref))) + (func $foo anyref shim (type 3) (param anyref) (result anyref) + (local i32) + call $alloc + local.tee 1 + local.get 0 + table.set 0 + local.get 1 + call $foo + local.tee 1 + table.get 0 + local.get 1 + call $dealloc) + (func $__wbindgen_object_clone_ref (type 2) (param i32) (result i32) + (local i32) + call $alloc + local.tee 1 + local.get 0 + table.get 0 + table.set 0 + local.get 1) + (func $foo (type 2) (param i32) (result i32) + local.get 0 + call $__wbindgen_object_clone_ref) + (func $alloc (type 0) (result i32) + i32.const 0) + (func $dealloc (type 1) (param i32)) + (table (;0;) 32 anyref) + (export "foo" (func $foo anyref shim)) + (export "__wbindgen_anyref_table_alloc" (func $alloc)) + (export "__wbindgen_anyref_table_dealloc" (func $dealloc))) +;) diff --git a/crates/anyref-xform/tests/drop-ref-intrinsic.wat b/crates/anyref-xform/tests/drop-ref-intrinsic.wat new file mode 100644 index 00000000..f1d2a2e6 --- /dev/null +++ b/crates/anyref-xform/tests/drop-ref-intrinsic.wat @@ -0,0 +1,37 @@ +;; @xform export "foo" (anyref_owned) + +(module + (import "__wbindgen_placeholder__" "__wbindgen_object_drop_ref" + (func $drop (param i32))) + (func $foo (export "foo") (param i32) + local.get 0 + call $drop) + (func $alloc (export "__wbindgen_anyref_table_alloc") (result i32) + i32.const 0) + (func $dealloc (export "__wbindgen_anyref_table_dealloc") (param i32)) +) + +(; CHECK-ALL: +(module + (type (;0;) (func (result i32))) + (type (;1;) (func (param i32))) + (type (;2;) (func (param anyref))) + (func $foo anyref shim (type 2) (param anyref) + (local i32) + call $alloc + local.tee 1 + local.get 0 + table.set 0 + local.get 1 + call $foo) + (func $foo (type 1) (param i32) + local.get 0 + call $dealloc) + (func $alloc (type 0) (result i32) + i32.const 0) + (func $dealloc (type 1) (param i32)) + (table (;0;) 32 anyref) + (export "foo" (func $foo anyref shim)) + (export "__wbindgen_anyref_table_alloc" (func $alloc)) + (export "__wbindgen_anyref_table_dealloc" (func $dealloc))) +;) diff --git a/crates/anyref-xform/tests/import-anyref-owned.wat b/crates/anyref-xform/tests/import-anyref-owned.wat new file mode 100644 index 00000000..36060be8 --- /dev/null +++ b/crates/anyref-xform/tests/import-anyref-owned.wat @@ -0,0 +1,36 @@ +;; @xform import "" "a" (anyref_owned) + +(module + (import "" "a" (func $a (param i32))) + (func (export "foo") + i32.const 0 + call $a) + (func $alloc (export "__wbindgen_anyref_table_alloc") (result i32) + i32.const 0) + (func $dealloc (export "__wbindgen_anyref_table_dealloc") (param i32)) +) + +(; CHECK-ALL: +(module + (type (;0;) (func)) + (type (;1;) (func (result i32))) + (type (;2;) (func (param i32))) + (type (;3;) (func (param anyref))) + (import "" "a" (func $a (type 3))) + (func $a anyref shim (type 2) (param i32) + local.get 0 + table.get 0 + local.get 0 + call $dealloc + call $a) + (func (;2;) (type 0) + i32.const 0 + call $a anyref shim) + (func $alloc (type 1) (result i32) + i32.const 0) + (func $dealloc (type 2) (param i32)) + (table (;0;) 32 anyref) + (export "foo" (func 2)) + (export "__wbindgen_anyref_table_alloc" (func $alloc)) + (export "__wbindgen_anyref_table_dealloc" (func $dealloc))) +;) diff --git a/crates/anyref-xform/tests/import-anyref-ret.wat b/crates/anyref-xform/tests/import-anyref-ret.wat new file mode 100644 index 00000000..4f692b88 --- /dev/null +++ b/crates/anyref-xform/tests/import-anyref-ret.wat @@ -0,0 +1,36 @@ +;; @xform import "" "a" () anyref_owned + +(module + (import "" "a" (func $a (result i32))) + (func (export "foo") (result i32) + call $a) + (func $alloc (export "__wbindgen_anyref_table_alloc") (result i32) + i32.const 0) + (func $dealloc (export "__wbindgen_anyref_table_dealloc") (param i32)) +) + +(; CHECK-ALL: +(module + (type (;0;) (func (result i32))) + (type (;1;) (func (result anyref))) + (type (;2;) (func (param i32))) + (import "" "a" (func $a (type 1))) + (func $a anyref shim (type 0) (result i32) + (local i32 anyref) + call $a + local.set 1 + call $alloc + local.tee 0 + local.get 1 + table.set 0 + local.get 0) + (func (;2;) (type 0) (result i32) + call $a anyref shim) + (func $alloc (type 0) (result i32) + i32.const 0) + (func $dealloc (type 2) (param i32)) + (table (;0;) 32 anyref) + (export "foo" (func 2)) + (export "__wbindgen_anyref_table_alloc" (func $alloc)) + (export "__wbindgen_anyref_table_dealloc" (func $dealloc))) +;) diff --git a/crates/anyref-xform/tests/import-anyref.wat b/crates/anyref-xform/tests/import-anyref.wat new file mode 100644 index 00000000..0a70f01d --- /dev/null +++ b/crates/anyref-xform/tests/import-anyref.wat @@ -0,0 +1,34 @@ +;; @xform import "" "a" (anyref_borrowed) + +(module + (import "" "a" (func $a (param i32))) + (func (export "foo") + i32.const 0 + call $a) + (func $alloc (export "__wbindgen_anyref_table_alloc") (result i32) + i32.const 0) + (func $dealloc (export "__wbindgen_anyref_table_dealloc") (param i32)) +) + +(; CHECK-ALL: +(module + (type (;0;) (func)) + (type (;1;) (func (result i32))) + (type (;2;) (func (param i32))) + (type (;3;) (func (param anyref))) + (import "" "a" (func $a (type 3))) + (func $a anyref shim (type 2) (param i32) + local.get 0 + table.get 0 + call $a) + (func (;2;) (type 0) + i32.const 0 + call $a anyref shim) + (func $alloc (type 1) (result i32) + i32.const 0) + (func $dealloc (type 2) (param i32)) + (table (;0;) 32 anyref) + (export "foo" (func 2)) + (export "__wbindgen_anyref_table_alloc" (func $alloc)) + (export "__wbindgen_anyref_table_dealloc" (func $dealloc))) +;) diff --git a/crates/anyref-xform/tests/mixed-export.wat b/crates/anyref-xform/tests/mixed-export.wat new file mode 100644 index 00000000..85439757 --- /dev/null +++ b/crates/anyref-xform/tests/mixed-export.wat @@ -0,0 +1,52 @@ +;; @xform export "a" (other anyref_borrowed other anyref_owned other) + +(module + (func $a (export "a") (param f32 i32 i64 i32 i32)) + (func $alloc (export "__wbindgen_anyref_table_alloc") (result i32) + i32.const 0) + (func $dealloc (export "__wbindgen_anyref_table_dealloc") (param i32)) +) + +(; CHECK-ALL: +(module + (type (;0;) (func (result i32))) + (type (;1;) (func (param i32))) + (type (;2;) (func (param f32 i32 i64 i32 i32))) + (type (;3;) (func (param f32 anyref i64 anyref i32))) + (func $a anyref shim (type 3) (param f32 anyref i64 anyref i32) + (local i32 i32) + global.get 0 + i32.const 1 + i32.sub + local.tee 5 + global.set 0 + local.get 0 + local.get 5 + local.get 1 + table.set 0 + local.get 5 + local.get 2 + call $alloc + local.tee 6 + local.get 3 + table.set 0 + local.get 6 + local.get 4 + call $a + local.get 5 + ref.null + table.set 0 + local.get 5 + i32.const 1 + i32.add + global.set 0) + (func $alloc (type 0) (result i32) + i32.const 0) + (func $a (type 2) (param f32 i32 i64 i32 i32)) + (func $dealloc (type 1) (param i32)) + (table (;0;) 32 anyref) + (global (;0;) (mut i32) (i32.const 32)) + (export "a" (func $a anyref shim)) + (export "__wbindgen_anyref_table_alloc" (func $alloc)) + (export "__wbindgen_anyref_table_dealloc" (func $dealloc))) +;) diff --git a/crates/anyref-xform/tests/mixed.wat b/crates/anyref-xform/tests/mixed.wat new file mode 100644 index 00000000..d220a861 --- /dev/null +++ b/crates/anyref-xform/tests/mixed.wat @@ -0,0 +1,50 @@ +;; @xform import "" "a" (other anyref_borrowed other anyref_owned other) + +(module + (import "" "a" (func $a (param f32 i32 i64 i32 i32))) + (func (export "foo") + f32.const 1 + i32.const 2 + i64.const 3 + i32.const 4 + i32.const 5 + call $a) + (func $alloc (export "__wbindgen_anyref_table_alloc") (result i32) + i32.const 0) + (func $dealloc (export "__wbindgen_anyref_table_dealloc") (param i32)) +) + +(; CHECK-ALL: +(module + (type (;0;) (func)) + (type (;1;) (func (result i32))) + (type (;2;) (func (param i32))) + (type (;3;) (func (param f32 i32 i64 i32 i32))) + (type (;4;) (func (param f32 anyref i64 anyref i32))) + (import "" "a" (func $a (type 4))) + (func $a anyref shim (type 3) (param f32 i32 i64 i32 i32) + local.get 0 + local.get 1 + table.get 0 + local.get 2 + local.get 3 + table.get 0 + local.get 3 + call $dealloc + local.get 4 + call $a) + (func (;2;) (type 0) + f32.const 0x1p+0 (;=1;) + i32.const 2 + i64.const 3 + i32.const 4 + i32.const 5 + call $a anyref shim) + (func $alloc (type 1) (result i32) + i32.const 0) + (func $dealloc (type 2) (param i32)) + (table (;0;) 32 anyref) + (export "foo" (func 2)) + (export "__wbindgen_anyref_table_alloc" (func $alloc)) + (export "__wbindgen_anyref_table_dealloc" (func $dealloc))) +;) diff --git a/crates/anyref-xform/tests/ret-anyref.wat b/crates/anyref-xform/tests/ret-anyref.wat new file mode 100644 index 00000000..77df8ab6 --- /dev/null +++ b/crates/anyref-xform/tests/ret-anyref.wat @@ -0,0 +1,33 @@ +;; @xform export "foo" () anyref_owned + +(module + (func $foo (export "foo") (result i32) + i32.const 0) + + (func $alloc (export "__wbindgen_anyref_table_alloc") (result i32) + i32.const 0) + (func $dealloc (export "__wbindgen_anyref_table_dealloc") (param i32)) +) + +(; CHECK-ALL: +(module + (type (;0;) (func (result i32))) + (type (;1;) (func (result anyref))) + (type (;2;) (func (param i32))) + (func $foo anyref shim (type 1) (result anyref) + (local i32) + call $foo + local.tee 0 + table.get 0 + local.get 0 + call $dealloc) + (func $foo (type 0) (result i32) + i32.const 0) + (func $alloc (type 0) (result i32) + i32.const 0) + (func $dealloc (type 2) (param i32)) + (table (;0;) 32 anyref) + (export "foo" (func $foo anyref shim)) + (export "__wbindgen_anyref_table_alloc" (func $alloc)) + (export "__wbindgen_anyref_table_dealloc" (func $dealloc))) +;) diff --git a/crates/anyref-xform/tests/table-grow-intrinsic.wat b/crates/anyref-xform/tests/table-grow-intrinsic.wat new file mode 100644 index 00000000..363165b1 --- /dev/null +++ b/crates/anyref-xform/tests/table-grow-intrinsic.wat @@ -0,0 +1,40 @@ +;; @xform export "foo" (anyref_owned) + +(module + (import "__wbindgen_anyref_xform__" "__wbindgen_anyref_table_grow" + (func $grow (param i32) (result i32))) + (func $foo (export "foo") (param i32) + i32.const 0 + call $grow + drop) + (func $alloc (export "__wbindgen_anyref_table_alloc") (result i32) + i32.const 0) + (func $dealloc (export "__wbindgen_anyref_table_dealloc") (param i32)) +) + +(; CHECK-ALL: +(module + (type (;0;) (func (result i32))) + (type (;1;) (func (param i32))) + (type (;2;) (func (param anyref))) + (func $foo anyref shim (type 2) (param anyref) + (local i32) + call $alloc + local.tee 1 + local.get 0 + table.set 0 + local.get 1 + call $foo) + (func $foo (type 1) (param i32) + ref.null + i32.const 0 + table.grow 0 + drop) + (func $alloc (type 0) (result i32) + i32.const 0) + (func $dealloc (type 1) (param i32)) + (table (;0;) 32 anyref) + (export "foo" (func $foo anyref shim)) + (export "__wbindgen_anyref_table_alloc" (func $alloc)) + (export "__wbindgen_anyref_table_dealloc" (func $dealloc))) +;) diff --git a/crates/anyref-xform/tests/table-set-null-intrinsic.wat b/crates/anyref-xform/tests/table-set-null-intrinsic.wat new file mode 100644 index 00000000..17988bff --- /dev/null +++ b/crates/anyref-xform/tests/table-set-null-intrinsic.wat @@ -0,0 +1,38 @@ +;; @xform export "foo" (anyref_owned) + +(module + (import "__wbindgen_anyref_xform__" "__wbindgen_anyref_table_set_null" + (func $set-null (param i32))) + (func $foo (export "foo") (param i32) + local.get 0 + call $set-null) + (func $alloc (export "__wbindgen_anyref_table_alloc") (result i32) + i32.const 0) + (func $dealloc (export "__wbindgen_anyref_table_dealloc") (param i32)) +) + +(; CHECK-ALL: +(module + (type (;0;) (func (result i32))) + (type (;1;) (func (param i32))) + (type (;2;) (func (param anyref))) + (func $foo anyref shim (type 2) (param anyref) + (local i32) + call $alloc + local.tee 1 + local.get 0 + table.set 0 + local.get 1 + call $foo) + (func $foo (type 1) (param i32) + local.get 0 + ref.null + table.set 0) + (func $alloc (type 0) (result i32) + i32.const 0) + (func $dealloc (type 1) (param i32)) + (table (;0;) 32 anyref) + (export "foo" (func $foo anyref shim)) + (export "__wbindgen_anyref_table_alloc" (func $alloc)) + (export "__wbindgen_anyref_table_dealloc" (func $dealloc))) +;) diff --git a/crates/anyref-xform/tests/table.wat b/crates/anyref-xform/tests/table.wat new file mode 100644 index 00000000..a19b7cca --- /dev/null +++ b/crates/anyref-xform/tests/table.wat @@ -0,0 +1,35 @@ +;; @xform table 0 (anyref_owned) + +(module + (func $foo (param i32)) + (table (export "func") 0 funcref) + (elem (i32.const 0) 0) + (func $alloc (export "__wbindgen_anyref_table_alloc") (result i32) + i32.const 0) + (func $dealloc (export "__wbindgen_anyref_table_dealloc") (param i32)) +) + +(; CHECK-ALL: +(module + (type (;0;) (func (result i32))) + (type (;1;) (func (param i32))) + (type (;2;) (func (param anyref))) + (func $closure0 anyref shim (type 2) (param anyref) + (local i32) + call $alloc + local.tee 1 + local.get 0 + table.set 1 + local.get 1 + call $foo) + (func $alloc (type 0) (result i32) + i32.const 0) + (func $foo (type 1) (param i32)) + (func $dealloc (type 1) (param i32)) + (table (;0;) 2 funcref) + (table (;1;) 32 anyref) + (export "func" (table 0)) + (export "__wbindgen_anyref_table_alloc" (func $alloc)) + (export "__wbindgen_anyref_table_dealloc" (func $dealloc)) + (elem (;0;) (i32.const 0) $foo $closure0 anyref shim)) +;) diff --git a/crates/backend/src/codegen.rs b/crates/backend/src/codegen.rs index 442e243f..16ceda1a 100644 --- a/crates/backend/src/codegen.rs +++ b/crates/backend/src/codegen.rs @@ -618,12 +618,16 @@ impl ToTokens for ast::ImportType { impl OptionIntoWasmAbi for #rust_name { #[inline] - fn none() -> Self::Abi { 0 } + fn none() -> Self::Abi { + 0 + } } impl<'a> OptionIntoWasmAbi for &'a #rust_name { #[inline] - fn none() -> Self::Abi { 0 } + fn none() -> Self::Abi { + 0 + } } impl FromWasmAbi for #rust_name { diff --git a/crates/cli-support/Cargo.toml b/crates/cli-support/Cargo.toml index b505efb9..82e3f9e8 100644 --- a/crates/cli-support/Cargo.toml +++ b/crates/cli-support/Cargo.toml @@ -18,11 +18,11 @@ log = "0.4" rustc-demangle = "0.1.13" serde_json = "1.0" tempfile = "3.0" -walrus = "0.13.0" +walrus = "0.14.0" wasm-bindgen-anyref-xform = { path = '../anyref-xform', version = '=0.2.55' } wasm-bindgen-shared = { path = "../shared", version = '=0.2.55' } wasm-bindgen-multi-value-xform = { path = '../multi-value-xform', version = '=0.2.55' } wasm-bindgen-threads-xform = { path = '../threads-xform', version = '=0.2.55' } wasm-bindgen-wasm-conventions = { path = '../wasm-conventions', version = '=0.2.55' } wasm-bindgen-wasm-interpreter = { path = "../wasm-interpreter", version = '=0.2.55' } -wasm-webidl-bindings = "0.6.0" +wit-walrus = "0.1.0" diff --git a/crates/cli-support/src/anyref.rs b/crates/cli-support/src/anyref.rs index 82ac0d17..19d85c2c 100644 --- a/crates/cli-support/src/anyref.rs +++ b/crates/cli-support/src/anyref.rs @@ -1,160 +1,269 @@ -use crate::webidl::{NonstandardIncoming, NonstandardOutgoing}; -use crate::webidl::{NonstandardWebidlSection, WasmBindgenAux}; +use crate::wit::{AdapterKind, Instruction, NonstandardWitSection}; +use crate::wit::{AdapterType, InstructionData, StackChange, WasmBindgenAux}; use anyhow::Error; -use std::collections::HashSet; +use std::collections::HashMap; use walrus::Module; use wasm_bindgen_anyref_xform::Context; -use wasm_webidl_bindings::ast; -pub fn process(module: &mut Module, wasm_interface_types: bool) -> Result<(), Error> { +pub fn process(module: &mut Module) -> Result<(), Error> { let mut cfg = Context::default(); cfg.prepare(module)?; - let bindings = module + let section = module .customs - .get_typed_mut::() - .expect("webidl custom section should exist"); + .get_typed_mut::() + .expect("wit custom section should exist"); + + let implements = section + .implements + .iter() + .cloned() + .map(|(core, adapter)| (adapter, core)) + .collect::>(); // Transform all exported functions in the module, using the bindings listed // for each exported function. - for (export, binding) in bindings.exports.iter_mut() { - let ty = module.types.get(binding.wasm_ty); - let args = Arguments::Incoming(&mut binding.incoming); - let (args, ret) = extract_anyrefs(ty, args); - cfg.export_xform(*export, &args, ret); - } - - // Transform all imported functions in the module, using the bindings listed - // for each imported function. - for (import, binding) in bindings.imports.iter_mut() { - let ty = module.types.get(binding.wasm_ty); - let args = Arguments::Outgoing(&mut binding.outgoing); - let (args, ret) = extract_anyrefs(ty, args); - cfg.import_xform(*import, &args, ret); - } - - // And finally transform all table elements that are used as function - // pointers for closures and such. - for (idx, binding) in bindings.elems.iter_mut() { - let ty = module.types.get(binding.wasm_ty); - let args = Arguments::Incoming(&mut binding.incoming); - let (args, ret) = extract_anyrefs(ty, args); - if let Some(new) = cfg.table_element_xform(*idx, &args, ret) { - *idx = new; + for (id, adapter) in section.adapters.iter_mut() { + let instructions = match &mut adapter.kind { + AdapterKind::Local { instructions } => instructions, + AdapterKind::Import { .. } => continue, + }; + if let Some(id) = implements.get(&id) { + import_xform( + &mut cfg, + *id, + instructions, + &mut adapter.params, + &mut adapter.results, + ); + continue; + } + if let Some(id) = find_call_export(instructions) { + export_xform(&mut cfg, id, instructions); + continue; } } - cfg.run(module)?; + let meta = cfg.run(module)?; - // If our output is using WebAssembly interface types then our bindings will - // never use this table, so no need to export it. Otherwise it's highly - // likely in web/JS embeddings this will be used, so make sure we export it - // to avoid it getting gc'd accidentally. - if !wasm_interface_types { - // Make sure to export the `anyref` table for the JS bindings since it - // will need to be initialized. If it doesn't exist though then the - // module must not use it, so we skip it. - let table = module.tables.iter().find(|t| match t.kind { - walrus::TableKind::Anyref(_) => true, - _ => false, - }); - let table = match table { - Some(t) => t.id(), - None => return Ok(()), - }; - module.exports.add("__wbg_anyref_table", table); - } - - // Clean up now-unused intrinsics and shims and such - walrus::passes::gc::run(module); - - // The GC pass above may end up removing some imported intrinsics. For - // example `__wbindgen_object_clone_ref` is no longer needed after the - // anyref pass. Make sure to delete the associated metadata for those - // intrinsics so we don't try to access stale intrinsics later on. - let remaining_imports = module - .imports - .iter() - .map(|i| i.id()) - .collect::>(); - module - .customs - .get_typed_mut::() - .expect("webidl custom section should exist") - .imports - .retain(|id, _| remaining_imports.contains(id)); - module + let section = module .customs .get_typed_mut::() - .expect("wasm-bindgen aux section should exist") - .import_map - .retain(|id, _| remaining_imports.contains(id)); + .expect("wit custom section should exist"); + section.anyref_table = Some(meta.table); + section.anyref_alloc = meta.alloc; + section.anyref_drop_slice = meta.drop_slice; Ok(()) } -enum Arguments<'a> { - Incoming(&'a mut [NonstandardIncoming]), - Outgoing(&'a mut [NonstandardOutgoing]), +fn find_call_export(instrs: &[InstructionData]) -> Option { + instrs + .iter() + .enumerate() + .filter_map(|(i, instr)| match instr.instr { + Instruction::CallExport(e) => Some(Export::Export(e)), + Instruction::CallTableElement(e) => Some(Export::TableElement { + idx: e, + call_idx: i, + }), + _ => None, + }) + .next() } -/// Extract a description of the anyref arguments from the function signature -/// described by `f`. -/// -/// The returned values are expected to be passed to the anyref transformation -/// pass, and indicate which arguments (by index) in the wasm signature should -/// be transformed from `i32` to `anyref` as well as whether the returned value -/// is an `anyref` or not. -/// -/// The `offset` argument here is typically 0 and indicates the offset at which -/// the wasm abi arguments described by `f` start at. For closures this is 2 -/// because two synthetic arguments are injected into the wasm signature which -/// aren't present in the `Function` signature. -fn extract_anyrefs(ty: &walrus::Type, args: Arguments<'_>) -> (Vec<(usize, bool)>, bool) { - let mut ret = Vec::new(); +enum Export { + Export(walrus::ExportId), + TableElement { + /// Table element that we're calling + idx: u32, + /// Index in the instruction stream where the call instruction is found + call_idx: usize, + }, +} - // First find all the `anyref` arguments in the input type, and we'll - // assume that they're owned anyref arguments for now (the `true`) - for (i, arg) in ty.params().iter().enumerate() { - if *arg == walrus::ValType::Anyref { - ret.push((i, true)); +/// Adapts the `instrs` given which are an implementation of the import of `id`. +/// +/// This function will pattern match outgoing arguments and update the +/// instruction stream to remove any anyref-management instructions since +/// we'll be sinking those into the WebAssembly module. +fn import_xform( + cx: &mut Context, + id: walrus::ImportId, + instrs: &mut Vec, + params: &mut [AdapterType], + results: &mut [AdapterType], +) { + struct Arg { + idx: usize, + // Some(false) for a borrowed anyref, Some(true) for an owned one + anyref: Option, + } + + let mut to_delete = Vec::new(); + let mut iter = instrs.iter().enumerate(); + let mut args = Vec::new(); + while let Some((i, instr)) = iter.next() { + match instr.instr { + Instruction::CallAdapter(_) => break, + Instruction::AnyrefLoadOwned | Instruction::TableGet => { + let owned = match instr.instr { + Instruction::TableGet => false, + _ => true, + }; + let mut arg: Arg = match args.pop().unwrap() { + Some(arg) => arg, + None => panic!("previous instruction must be `arg.get`"), + }; + arg.anyref = Some(owned); + match params[arg.idx] { + AdapterType::I32 => {} + _ => panic!("must be `i32` type"), + } + params[arg.idx] = AdapterType::Anyref; + args.push(Some(arg)); + to_delete.push(i); + } + Instruction::Standard(wit_walrus::Instruction::ArgGet(n)) => { + args.push(Some(Arg { + idx: n as usize, + anyref: None, + })); + } + _ => match instr.stack_change { + StackChange::Modified { pushed, popped } => { + for _ in 0..popped { + args.pop(); + } + for _ in 0..pushed { + args.push(None); + } + } + StackChange::Unknown => { + panic!("must have stack change data"); + } + }, } } - // Afterwards look through the argument list (specified with various - // bindings) to find any borrowed anyref values and update our - // transformation metadata accordingly. if we find one then the binding no - // longer needs to remember its borrowed but rather it's just a simple cast - // from wasm anyref to JS any. - match args { - Arguments::Incoming(incoming) => { - for binding in incoming { - let expr = match binding { - NonstandardIncoming::BorrowedAnyref { - val: ast::IncomingBindingExpression::Get(expr), - } => expr.clone(), - _ => continue, - }; - ret.iter_mut().find(|p| p.0 == expr.idx as usize).unwrap().1 = false; - let new_binding = ast::IncomingBindingExpressionAs { - ty: walrus::ValType::Anyref, - expr: Box::new(expr.into()), - }; - *binding = NonstandardIncoming::Standard(new_binding.into()); + let mut ret_anyref = false; + while let Some((i, instr)) = iter.next() { + match instr.instr { + Instruction::I32FromAnyrefOwned => { + assert_eq!(results.len(), 1); + match results[0] { + AdapterType::I32 => {} + _ => panic!("must be `i32` type"), + } + results[0] = AdapterType::Anyref; + ret_anyref = true; + to_delete.push(i); } + _ => {} } - Arguments::Outgoing(outgoing) => { - for binding in outgoing { - let idx = match binding { - NonstandardOutgoing::BorrowedAnyref { idx } => *idx, - _ => continue, - }; - ret.iter_mut().find(|p| p.0 == idx as usize).unwrap().1 = false; - let new_binding = ast::OutgoingBindingExpressionAs { - idx, - ty: ast::WebidlScalarType::Any.into(), - }; - *binding = NonstandardOutgoing::Standard(new_binding.into()); + } + + // Delete all unnecessary anyref management insructions + for idx in to_delete.into_iter().rev() { + instrs.remove(idx); + } + + // Filter down our list of arguments to just the ones that are anyref + // values. + let args = args + .iter() + .filter_map(|arg| arg.as_ref()) + .filter_map(|arg| arg.anyref.map(|owned| (arg.idx, owned))) + .collect::>(); + + // ... and register this entire transformation with the anyref + // transformation pass. + cx.import_xform(id, &args, ret_anyref); +} + +/// Adapts the `instrs` of an adapter function that calls an export. +/// +/// The `instrs` must be generated by wasm-bindgen itself and follow the +/// pattern matched below to pass off to the anyref transformation pass. The +/// signature of the adapter doesn't change (it remains as anyref-aware) but the +/// signature of the export we're calling will change during the transformation. +fn export_xform(cx: &mut Context, export: Export, instrs: &mut Vec) { + let mut to_delete = Vec::new(); + let mut iter = instrs.iter().enumerate(); + let mut args = Vec::new(); + + // Mutate instructions leading up to the `CallExport` instruction. We + // maintain a stack of indicators whether the element at that stack slot is + // unknown (`None`) or whether it's an owned/borrowed anyref + // (`Some(owned)`). + // + // Note that we're going to delete the `I32FromAnyref*` instructions, so we + // also maintain indices of the instructions to delete. + while let Some((i, instr)) = iter.next() { + match instr.instr { + Instruction::CallExport(_) | Instruction::CallTableElement(_) => break, + Instruction::I32FromAnyrefOwned => { + args.pop(); + args.push(Some(true)); + to_delete.push(i); + } + Instruction::I32FromAnyrefBorrow => { + args.pop(); + args.push(Some(false)); + to_delete.push(i); + } + _ => match instr.stack_change { + StackChange::Modified { pushed, popped } => { + for _ in 0..popped { + args.pop(); + } + for _ in 0..pushed { + args.push(None); + } + } + StackChange::Unknown => { + panic!("must have stack change data"); + } + }, + } + } + + // If one of the instructions after the call is an `AnyrefLoadOwned` then we + // know that the function returned an anyref. Currently `&'static Anyref` + // can't be done as a return value, so this is the only case we handle here. + let mut ret_anyref = false; + while let Some((i, instr)) = iter.next() { + match instr.instr { + Instruction::AnyrefLoadOwned => { + ret_anyref = true; + to_delete.push(i); + } + _ => {} + } + } + + // Filter down our list of arguments to just the ones that are anyref + // values. + let args = args + .iter() + .enumerate() + .filter_map(|(i, owned)| owned.map(|owned| (i, owned))) + .collect::>(); + + // ... and register this entire transformation with the anyref + // transformation pass. + match export { + Export::Export(id) => { + cx.export_xform(id, &args, ret_anyref); + } + Export::TableElement { idx, call_idx } => { + if let Some(new_idx) = cx.table_element_xform(idx, &args, ret_anyref) { + instrs[call_idx].instr = Instruction::CallTableElement(new_idx); } } } - (ret, ty.results() == &[walrus::ValType::Anyref]) + + // Delete all unnecessary anyref management instructions. We're going to + // sink these instructions into the wasm module itself. + for idx in to_delete.into_iter().rev() { + instrs.remove(idx); + } } diff --git a/crates/cli-support/src/intrinsic.rs b/crates/cli-support/src/intrinsic.rs index a54926f3..d7db4fc9 100644 --- a/crates/cli-support/src/intrinsic.rs +++ b/crates/cli-support/src/intrinsic.rs @@ -13,7 +13,7 @@ macro_rules! intrinsics { (pub enum Intrinsic { $( #[symbol = $sym:tt] - #[signature = fn($($arg:expr),*) -> $ret:ident] + #[signature = fn($($arg:expr),*) -> $ret:expr] $name:ident, )* }) => { @@ -36,7 +36,7 @@ macro_rules! intrinsics { /// Returns the expected signature of this intrinsic, used for /// generating a JS shim. - pub fn binding(&self) -> Function { + pub fn signature(&self) -> Function { use crate::descriptor::Descriptor::*; match self { $( @@ -71,6 +71,14 @@ fn ref_string() -> Descriptor { Descriptor::Ref(Box::new(Descriptor::String)) } +fn opt_string() -> Descriptor { + Descriptor::Option(Box::new(Descriptor::String)) +} + +fn opt_f64() -> Descriptor { + Descriptor::Option(Box::new(Descriptor::F64)) +} + intrinsics! { pub enum Intrinsic { #[symbol = "__wbindgen_jsval_eq"] @@ -122,10 +130,10 @@ intrinsics! { #[signature = fn(ref_string()) -> Anyref] SymbolNamedNew, #[symbol = "__wbindgen_number_get"] - #[signature = fn(ref_anyref(), I32) -> F64] + #[signature = fn(ref_anyref()) -> opt_f64()] NumberGet, #[symbol = "__wbindgen_string_get"] - #[signature = fn(ref_anyref(), I32) -> I32] + #[signature = fn(ref_anyref()) -> opt_string()] StringGet, #[symbol = "__wbindgen_boolean_get"] #[signature = fn(ref_anyref()) -> I32] diff --git a/crates/cli-support/src/js/binding.rs b/crates/cli-support/src/js/binding.rs index 2154d761..45b5c7c0 100644 --- a/crates/cli-support/src/js/binding.rs +++ b/crates/cli-support/src/js/binding.rs @@ -4,35 +4,17 @@ //! exported functions, table elements, imports, etc. All function shims //! generated by `wasm-bindgen` run through this type. -use crate::js::incoming; -use crate::js::outgoing; use crate::js::Context; -use crate::webidl::Binding; -use anyhow::{bail, Error}; -use std::collections::HashSet; -use wasm_webidl_bindings::ast; +use crate::wit::InstructionData; +use crate::wit::{Adapter, AdapterId, AdapterKind, AdapterType, Instruction}; +use anyhow::{anyhow, bail, Error}; +use walrus::Module; /// A one-size-fits-all builder for processing WebIDL bindings and generating /// JS. pub struct Builder<'a, 'b> { /// Parent context used to expose helper functions and such. - cx: &'a mut Context<'b>, - /// Prelude JS which is present before the main invocation to prepare - /// arguments. - args_prelude: String, - /// Finally block to be executed regardless of the call's status, mostly - /// used for cleanups like free'ing. - finally: String, - /// Code to execute after return value is materialized. - ret_finally: String, - /// Argument names to the JS function shim that we're generating. - function_args: Vec, - /// JS expressions that are arguments to the function that we're calling. - invoc_args: Vec, - /// JS to execute just before the return value is materialized. - ret_prelude: String, - /// The JS expression of the actual return value. - ret_js: String, + pub cx: &'a mut Context<'b>, /// The TypeScript definition for each argument to this function. pub ts_args: Vec, /// The TypeScript return value for this function. @@ -50,13 +32,38 @@ pub struct Builder<'a, 'b> { log_error: bool, } -/// Helper struct used in incoming/outgoing to generate JS. -pub struct JsBuilder { +/// Helper struct used to create JS to process all instructions in an adapter +/// function. +pub struct JsBuilder<'a, 'b> { + /// General context for building JS, used to manage intrinsic names, exposed + /// JS functions, etc. + cx: &'a mut Context<'b>, + + /// The list of typescript arguments that we're going to have to this + /// function. typescript: Vec, + + /// The "prelude" of the function, or largely just the JS function we've + /// built so far. prelude: String, + + /// JS code to execute in a `finally` block in case any exceptions happen. finally: String, + + /// An index used to manage allocation of temporary indices, used to name + /// variables to ensure nothing clashes with anything else. tmp: usize, + + /// Names or expressions representing the arguments to the adapter. This is + /// use to translate the `arg.get` instruction. args: Vec, + + /// The wasm interface types "stack". The expressions pushed onto this stack + /// are intended to be *pure*, and if they're not, they should be pushed + /// into the `prelude`, assigned to a variable, and the variable should be + /// pushed to the stack. We're not super principled about this though, so + /// improvements will likely happen here over time. + stack: Vec, } pub struct TypescriptArg { @@ -68,15 +75,8 @@ pub struct TypescriptArg { impl<'a, 'b> Builder<'a, 'b> { pub fn new(cx: &'a mut Context<'b>) -> Builder<'a, 'b> { Builder { - log_error: cx.config.debug, + log_error: false, cx, - args_prelude: String::new(), - finally: String::new(), - ret_finally: String::new(), - function_args: Vec::new(), - invoc_args: Vec::new(), - ret_prelude: String::new(), - ret_js: String::new(), ts_args: Vec::new(), ts_ret: None, constructor: None, @@ -93,306 +93,116 @@ impl<'a, 'b> Builder<'a, 'b> { self.constructor = Some(class.to_string()); } - pub fn catch(&mut self, catch: bool) -> Result<(), Error> { - if catch { - self.cx.expose_handle_error()?; - } + pub fn catch(&mut self, catch: bool) { self.catch = catch; - Ok(()) } - pub fn disable_log_error(&mut self, disable: bool) { - if disable { - self.log_error = false; - } + pub fn log_error(&mut self, log: bool) { + self.log_error = log; } pub fn process( &mut self, - binding: &Binding, - webidl: &ast::WebidlFunction, - incoming_args: bool, + adapter: &Adapter, + instructions: &[InstructionData], explicit_arg_names: &Option>, - invoke: &mut dyn FnMut(&mut Context, &mut String, &[String]) -> Result, ) -> Result { - // used in `finalize` below - if self.log_error { - self.cx.expose_log_error(); + if self.cx.aux.imports_with_assert_no_shim.contains(&adapter.id) { + bail!("generating a shim for something asserted to have no shim"); } - // First up we handle all the arguments. Depending on whether incoming - // or outgoing ar the arguments this is pretty different. - let mut arg_names = Vec::new(); - let mut js; - if incoming_args { - let mut webidl_params = webidl.params.iter(); + let mut params = adapter.params.iter(); + let mut function_args = Vec::new(); - // If we're returning via an out pointer then it's guaranteed to be - // the first argument. This isn't an argument of the function shim - // we're generating so synthesize the parameter and its value. - // - // For the actual value of the return pointer we just pick the first - // properly aligned nonzero address. We use the address for a - // BigInt64Array sometimes which means it needs to be 8-byte - // aligned. Otherwise valid code is unlikely to ever be working - // around address 8, so this should be a safe address to use for - // returning data through. - if binding.return_via_outptr.is_some() { - drop(webidl_params.next()); - self.args_prelude.push_str("const retptr = 8;\n"); - arg_names.push("retptr".to_string()); - } - - // If this is a method then we're generating this as part of a class - // method, so the leading parameter is the this pointer stored on - // the JS object, so synthesize that here. - match self.method { - Some(consumes_self) => { - drop(webidl_params.next()); - if self.cx.config.debug { - self.args_prelude.push_str( - "if (this.ptr == 0) throw new Error('Attempt to use a moved value');\n", - ); - } - if consumes_self { - self.args_prelude.push_str("const ptr = this.ptr;\n"); - self.args_prelude.push_str("this.ptr = 0;\n"); - arg_names.push("ptr".to_string()); - } else { - arg_names.push("this.ptr".to_string()); - } + // If this is a method then we're generating this as part of a class + // method, so the leading parameter is the this pointer stored on + // the JS object, so synthesize that here. + let mut js = JsBuilder::new(self.cx); + match self.method { + Some(consumes_self) => { + drop(params.next()); + if js.cx.config.debug { + js.prelude( + "if (this.ptr == 0) throw new Error('Attempt to use a moved value');\n", + ); + } + if consumes_self { + js.prelude("var ptr = this.ptr;"); + js.prelude("this.ptr = 0;"); + js.args.push("ptr".to_string()); + } else { + js.args.push("this.ptr".to_string()); } - None => {} - } - - // And now take the rest of the parameters and generate a name for them. - for (i, _) in webidl_params.enumerate() { - let arg = match explicit_arg_names { - Some(list) => list[i].clone(), - None => format!("arg{}", i), - }; - self.function_args.push(arg.clone()); - arg_names.push(arg); - } - js = JsBuilder::new(arg_names); - let mut args = incoming::Incoming::new(self.cx, &webidl.params, &mut js); - for argument in binding.incoming.iter() { - self.invoc_args.extend(args.process(argument)?); - } - } else { - // If we're getting arguments from outgoing values then the ret ptr - // is actually an argument of the function itself. That means that - // `arg0` we generate below is the ret ptr, and we shouldn't - // generate a JS binding for it and instead skip the first binding - // listed. - let mut skip = 0; - if binding.return_via_outptr.is_some() { - skip = 1; - } - - // And now take the rest of the parameters and generate a name for them. - for i in 0..self.cx.module.types.get(binding.wasm_ty).params().len() { - let arg = format!("arg{}", i); - self.function_args.push(arg.clone()); - arg_names.push(arg); - } - js = JsBuilder::new(arg_names); - let mut args = outgoing::Outgoing::new(self.cx, &mut js); - for argument in binding.outgoing.iter().skip(skip) { - self.invoc_args.push(args.process(argument)?); } + None => {} + } + for (i, _param) in params.enumerate() { + let arg = match explicit_arg_names { + Some(list) => list[i].clone(), + None => format!("arg{}", i), + }; + js.args.push(arg.clone()); + function_args.push(arg); } - // Save off the results of JS generation for the arguments. - self.args_prelude.push_str(&js.prelude); - self.finally.push_str(&js.finally); - self.ts_args.extend(js.typescript); + // Translate all instructions, the fun loop! + // + // This loop will process all instructions for this adapter function. + // Each instruction will push/pop from the `js.stack` variable, and will + // eventually build up the entire `js.prelude` variable with all the JS + // code that we're going to be adding. Note that the stack at the end + // represents all returned values. + // + // We don't actually manage a literal stack at runtime, but instead we + // act as more of a compiler to generate straight-line code to make it + // more JIT-friendly. The generated code should be equivalent to the + // wasm interface types stack machine, however. + for instr in instructions { + instruction(&mut js, &instr.instr, &mut self.log_error)?; + } + + assert_eq!(js.stack.len(), adapter.results.len()); + match js.stack.len() { + 0 => {} + 1 => { + self.ts_ret = js.typescript.pop(); + let val = js.pop(); + js.prelude(&format!("return {};", val)); + } + + // TODO: this should be pretty trivial to support (commented out + // code below), but we should be sure to have a test for this + // somewhere. Currently I don't think it's possible to actually + // exercise this with just Rust code, so let's wait until we get + // some tests to enable this path. + _ => bail!("multi-value returns from adapters not supported yet"), + // _ => { + // let expr = js.stack.join(", "); + // js.stack.truncate(0); + // js.prelude(&format!("return [{}];", expr)); + // } + } + assert!(js.stack.is_empty()); + self.ts_args = js.typescript; // Remove extraneous typescript args which were synthesized and aren't // part of our function shim. - while self.ts_args.len() > self.function_args.len() { + while self.ts_args.len() > function_args.len() { self.ts_args.remove(0); } - // Handle the special case where there is no return value. In this case - // we can skip all the logic below and go straight to the end. - if incoming_args { - if binding.outgoing.len() == 0 { - assert!(binding.return_via_outptr.is_none()); - assert!(self.constructor.is_none()); - let invoc = invoke(self.cx, &mut self.args_prelude, &self.invoc_args)?; - return Ok(self.finalize(&invoc)); - } - assert_eq!(binding.outgoing.len(), 1); - } else { - if binding.incoming.len() == 0 { - assert!(binding.return_via_outptr.is_none()); - assert!(self.constructor.is_none()); - let invoc = invoke(self.cx, &mut self.args_prelude, &self.invoc_args)?; - return Ok(self.finalize(&invoc)); - } - assert_eq!(binding.incoming.len(), 1); - } + let mut ret = String::new(); + ret.push_str("("); + ret.push_str(&function_args.join(", ")); + ret.push_str(") {\n"); - // Like above handling the return value is quite different based on - // whether it's an outgoing argument or an incoming argument. - let mut ret_args = Vec::new(); - let mut js; - if incoming_args { - match &binding.return_via_outptr { - // If we have an outgoing value that requires multiple - // aggregates then we're passing a return pointer (a global one) - // to a wasm function, and then afterwards we're going to read - // the results of that return pointer. Here we generate an - // expression effectively which represents reading each value of - // the return pointer that was filled in. These values are then - // used by the outgoing builder as inputs to generate the final - // actual return value. - Some(list) => { - let mut exposed = HashSet::new(); - for (i, ty) in list.iter().enumerate() { - let (mem, size) = match ty { - walrus::ValType::I32 => { - if exposed.insert(*ty) { - self.cx.expose_int32_memory(); - self.ret_prelude - .push_str("const memi32 = getInt32Memory();\n"); - } - ("memi32", 4) - } - walrus::ValType::F32 => { - if exposed.insert(*ty) { - self.cx.expose_f32_memory(); - self.ret_prelude - .push_str("const memf32 = getFloat32Memory();\n"); - } - ("memf32", 4) - } - walrus::ValType::F64 => { - if exposed.insert(*ty) { - self.cx.expose_f64_memory(); - self.ret_prelude - .push_str("const memf64 = getFloat64Memory();\n"); - } - ("memf64", 8) - } - _ => bail!("invalid aggregate return type"), - }; - ret_args.push(format!("{}[retptr / {} + {}]", mem, size, i)); - } - } - - // No return pointer? That's much easier! - // - // If there's one return value we just have one input of `ret` - // which is created in the JS shim below. If there's multiple - // return values (a multi-value module) then we'll pull results - // from the returned array. - None => { - let amt = self.cx.module.types.get(binding.wasm_ty).results().len(); - if amt == 1 { - ret_args.push("ret".to_string()); - } else { - for i in 0..amt { - ret_args.push(format!("ret[{}]", i)); - } - } - } - } - js = JsBuilder::new(ret_args); - let mut ret = outgoing::Outgoing::new(self.cx, &mut js); - let ret_js = ret.process(&binding.outgoing[0])?; - self.ret_js.push_str(&ret_js); - } else { - // If there's an out ptr for an incoming argument then it means that - // the first argument to our function is the return pointer, and we - // need to fill that in. After we process the value we then write - // each result of the processed value into the corresponding typed - // array. - js = JsBuilder::new(vec!["ret".to_string()]); - let results = match &webidl.result { - Some(ptr) => std::slice::from_ref(ptr), - None => &[], - }; - let mut ret = incoming::Incoming::new(self.cx, results, &mut js); - let ret_js = ret.process(&binding.incoming[0])?; - match &binding.return_via_outptr { - Some(list) => { - assert_eq!(list.len(), ret_js.len()); - for (i, js) in ret_js.iter().enumerate() { - self.ret_finally - .push_str(&format!("const ret{} = {};\n", i, js)); - } - for (i, ty) in list.iter().enumerate() { - let (mem, size) = match ty { - walrus::ValType::I32 => { - self.cx.expose_int32_memory(); - ("getInt32Memory()", 4) - } - walrus::ValType::F32 => { - self.cx.expose_f32_memory(); - ("getFloat32Memory()", 4) - } - walrus::ValType::F64 => { - self.cx.expose_f64_memory(); - ("getFloat64Memory()", 8) - } - _ => bail!("invalid aggregate return type"), - }; - self.ret_finally - .push_str(&format!("{}[arg0 / {} + {}] = ret{};\n", mem, size, i, i)); - } - } - None => { - assert_eq!(ret_js.len(), 1); - self.ret_js.push_str(&ret_js[0]); - } - } - } - self.ret_finally.push_str(&js.finally); - self.ret_prelude.push_str(&js.prelude); - self.ts_ret = Some(js.typescript.remove(0)); - let invoc = invoke(self.cx, &mut self.args_prelude, &self.invoc_args)?; - Ok(self.finalize(&invoc)) - } - - // This method... is a mess. Refactorings and improvements are more than - // welcome :) - fn finalize(&self, invoc: &str) -> String { - let mut js = String::new(); - js.push_str("("); - js.push_str(&self.function_args.join(", ")); - js.push_str(") {\n"); - if self.args_prelude.len() > 0 { - js.push_str(self.args_prelude.trim()); - js.push_str("\n"); - } - - let mut call = String::new(); - if self.ts_ret.is_some() { - call.push_str("const ret = "); - } - call.push_str(invoc); - call.push_str(";\n"); - - if self.ret_prelude.len() > 0 { - call.push_str(self.ret_prelude.trim()); - call.push_str("\n"); - } - - if self.ret_js.len() > 0 { - assert!(self.ts_ret.is_some()); - // Having a this field isn't supported yet, but shouldn't come up - assert!(self.ret_finally.len() == 0); - call.push_str("return "); - call.push_str(&self.ret_js); - call.push_str(";\n"); - } else if self.ret_finally.len() > 0 { - call.push_str(self.ret_finally.trim()); - call.push_str("\n"); + let mut call = js.prelude; + if js.finally.len() != 0 { + call = format!("try {{\n{}}} finally {{\n{}}}\n", call, js.finally); } if self.catch { + js.cx.expose_handle_error()?; call = format!("try {{\n{}}} catch (e) {{\n handleError(e)\n}}\n", call); } @@ -401,18 +211,14 @@ impl<'a, 'b> Builder<'a, 'b> { // logs what happened, but keeps the exception being thrown to propagate // elsewhere. if self.log_error { + js.cx.expose_log_error(); call = format!("try {{\n{}}} catch (e) {{\n logError(e)\n}}\n", call); } - let finally = self.finally.trim(); - if finally.len() != 0 { - call = format!("try {{\n{}}} finally {{\n{}\n}}\n", call, finally); - } + ret.push_str(&call); + ret.push_str("}"); - js.push_str(&call); - js.push_str("}"); - - return js; + return Ok(ret); } /// Returns the typescript signature of the binding that this has described. @@ -480,27 +286,25 @@ impl<'a, 'b> Builder<'a, 'b> { } } -impl JsBuilder { - pub fn new(args: Vec) -> JsBuilder { +impl<'a, 'b> JsBuilder<'a, 'b> { + pub fn new(cx: &'a mut Context<'b>) -> JsBuilder<'a, 'b> { JsBuilder { - args, + cx, + args: Vec::new(), tmp: 0, finally: String::new(), prelude: String::new(), typescript: Vec::new(), + stack: Vec::new(), } } - pub fn typescript_len(&self) -> usize { - self.typescript.len() - } - pub fn arg(&self, idx: u32) -> &str { &self.args[idx as usize] } pub fn typescript_required(&mut self, ty: &str) { - let name = self.args[self.typescript.len()].clone(); + let name = self.arg_name(); self.typescript.push(TypescriptArg { ty: ty.to_string(), optional: false, @@ -509,7 +313,7 @@ impl JsBuilder { } pub fn typescript_optional(&mut self, ty: &str) { - let name = self.args[self.typescript.len()].clone(); + let name = self.arg_name(); self.typescript.push(TypescriptArg { ty: ty.to_string(), optional: true, @@ -517,17 +321,28 @@ impl JsBuilder { }); } + fn arg_name(&self) -> String { + self.args + .get(self.typescript.len()) + .cloned() + .unwrap_or_else(|| format!("arg{}", self.typescript.len())) + } + pub fn prelude(&mut self, prelude: &str) { - for line in prelude.trim().lines() { - self.prelude.push_str(line); - self.prelude.push_str("\n"); + for line in prelude.trim().lines().map(|l| l.trim()) { + if !line.is_empty() { + self.prelude.push_str(line); + self.prelude.push_str("\n"); + } } } pub fn finally(&mut self, finally: &str) { - for line in finally.trim().lines() { - self.finally.push_str(line); - self.finally.push_str("\n"); + for line in finally.trim().lines().map(|l| l.trim()) { + if !line.is_empty() { + self.finally.push_str(line); + self.finally.push_str("\n"); + } } } @@ -536,4 +351,849 @@ impl JsBuilder { self.tmp += 1; return ret; } + + fn pop(&mut self) -> String { + self.stack.pop().unwrap() + } + + fn push(&mut self, arg: String) { + self.stack.push(arg); + } + + fn assert_class(&mut self, arg: &str, class: &str) { + self.cx.expose_assert_class(); + self.prelude(&format!("_assertClass({}, {});", arg, class)); + } + + fn assert_number(&mut self, arg: &str) { + if !self.cx.config.debug { + return; + } + self.cx.expose_assert_num(); + self.prelude(&format!("_assertNum({});", arg)); + } + + fn assert_bool(&mut self, arg: &str) { + if !self.cx.config.debug { + return; + } + self.cx.expose_assert_bool(); + self.prelude(&format!("_assertBoolean({});", arg)); + } + + fn assert_optional_number(&mut self, arg: &str) { + if !self.cx.config.debug { + return; + } + self.cx.expose_is_like_none(); + self.prelude(&format!("if (!isLikeNone({})) {{", arg)); + self.assert_number(arg); + self.prelude("}"); + } + + fn assert_optional_bool(&mut self, arg: &str) { + if !self.cx.config.debug { + return; + } + self.cx.expose_is_like_none(); + self.prelude(&format!("if (!isLikeNone({})) {{", arg)); + self.assert_bool(arg); + self.prelude("}"); + } + + fn assert_not_moved(&mut self, arg: &str) { + if !self.cx.config.debug { + return; + } + self.prelude(&format!( + "\ + if ({0}.ptr === 0) {{ + throw new Error('Attempt to use a moved value'); + }} + ", + arg, + )); + } +} + +fn instruction(js: &mut JsBuilder, instr: &Instruction, log_error: &mut bool) -> Result<(), Error> { + // Here first properly aligned nonzero address is chosen to be the + // out-pointer. We use the address for a BigInt64Array sometimes which + // means it needs to be 8-byte aligned. Otherwise valid code is + // unlikely to ever be working around address 8, so this should be a + // safe address to use for returning data through. + let retptr_val = 8; + + match instr { + Instruction::Standard(wit_walrus::Instruction::ArgGet(n)) => { + let arg = js.arg(*n).to_string(); + js.push(arg); + } + + Instruction::Standard(wit_walrus::Instruction::CallAdapter(_)) => { + panic!("standard call adapter functions should be mapped to our adapters"); + } + + Instruction::Standard(wit_walrus::Instruction::CallCore(_)) + | Instruction::CallExport(_) + | Instruction::CallAdapter(_) + | Instruction::CallTableElement(_) + | Instruction::Standard(wit_walrus::Instruction::DeferCallCore(_)) => { + let invoc = Invocation::from(instr, js.cx.module)?; + let (params, results) = invoc.params_results(js.cx); + + // Pop off the number of parameters for the function we're calling + let mut args = Vec::new(); + for _ in 0..params { + args.push(js.pop()); + } + args.reverse(); + + // Call the function through an export of the underlying module. + let call = invoc.invoke(js.cx, &args, &mut js.prelude, log_error)?; + + // And then figure out how to actually handle where the call + // happens. This is pretty conditional depending on the number of + // return values of the function. + match (invoc.defer(), results) { + (true, 0) => { + js.finally(&format!("{};", call)); + js.stack.extend(args); + } + (true, _) => panic!("deferred calls must have no results"), + (false, 0) => js.prelude(&format!("{};", call)), + (false, n) => { + js.prelude(&format!("var ret = {};", call)); + if n == 1 { + js.push("ret".to_string()); + } else { + for i in 0..n { + js.push(format!("ret[{}]", i)); + } + } + } + } + } + + Instruction::Standard(wit_walrus::Instruction::IntToWasm { trap: false, .. }) => { + js.typescript_required("number"); + let val = js.pop(); + js.assert_number(&val); + js.push(val); + } + + // When converting to a JS number we need to specially handle the `u32` + // case because if the high bit is set then it comes out as a negative + // number, but we want to switch that to an unsigned representation. + Instruction::Standard(wit_walrus::Instruction::WasmToInt { + trap: false, + output, + .. + }) => { + js.typescript_required("number"); + let val = js.pop(); + match output { + wit_walrus::ValType::U32 => js.push(format!("{} >>> 0", val)), + _ => js.push(val), + } + } + + Instruction::Standard(wit_walrus::Instruction::WasmToInt { trap: true, .. }) + | Instruction::Standard(wit_walrus::Instruction::IntToWasm { trap: true, .. }) => { + bail!("trapping wasm-to-int and int-to-wasm instructions not supported") + } + + Instruction::Standard(wit_walrus::Instruction::MemoryToString(mem)) => { + js.typescript_required("string"); + let len = js.pop(); + let ptr = js.pop(); + let get = js.cx.expose_get_string_from_wasm(*mem)?; + js.push(format!("{}({}, {})", get, ptr, len)); + } + + Instruction::Standard(wit_walrus::Instruction::StringToMemory { mem, malloc }) => { + js.typescript_required("string"); + let pass = js.cx.expose_pass_string_to_wasm(*mem)?; + let val = js.pop(); + let malloc = js.cx.export_name_of(*malloc); + let i = js.tmp(); + js.prelude(&format!( + "var ptr{i} = {f}({0}, wasm.{malloc});", + val, + i = i, + f = pass, + malloc = malloc, + )); + js.prelude(&format!("var len{} = WASM_VECTOR_LEN;", i)); + js.push(format!("ptr{}", i)); + js.push(format!("len{}", i)); + } + + Instruction::Retptr => js.stack.push(retptr_val.to_string()), + + Instruction::StoreRetptr { ty, offset, mem } => { + let (mem, size) = match ty { + AdapterType::I32 => (js.cx.expose_int32_memory(*mem), 4), + AdapterType::F32 => (js.cx.expose_f32_memory(*mem), 4), + AdapterType::F64 => (js.cx.expose_f64_memory(*mem), 8), + other => bail!("invalid aggregate return type {:?}", other), + }; + // Note that we always assume the return pointer is argument 0, + // which is currently the case for LLVM. + let val = js.pop(); + let expr = format!( + "{}()[{} / {} + {}] = {};", + mem, + js.arg(0), + size, + offset, + val, + ); + js.prelude(&expr); + } + + Instruction::LoadRetptr { ty, offset, mem } => { + let (mem, size) = match ty { + AdapterType::I32 => (js.cx.expose_int32_memory(*mem), 4), + AdapterType::F32 => (js.cx.expose_f32_memory(*mem), 4), + AdapterType::F64 => (js.cx.expose_f64_memory(*mem), 8), + other => bail!("invalid aggregate return type {:?}", other), + }; + // If we're loading from the return pointer then we must have pushed + // it earlier, and we always push the same value, so load that value + // here + let expr = format!("{}()[{} / {} + {}]", mem, retptr_val, size, offset); + js.prelude(&format!("var r{} = {};", offset, expr)); + js.push(format!("r{}", offset)); + } + + Instruction::I32FromBool => { + js.typescript_required("boolean"); + let val = js.pop(); + js.assert_bool(&val); + // JS will already coerce booleans into numbers for us + js.push(val); + } + + Instruction::I32FromStringFirstChar => { + js.typescript_required("string"); + let val = js.pop(); + js.push(format!("{}.codePointAt(0)", val)); + } + + Instruction::I32FromAnyrefOwned => { + js.typescript_required("any"); + js.cx.expose_add_heap_object(); + let val = js.pop(); + js.push(format!("addHeapObject({})", val)); + } + + Instruction::I32FromAnyrefBorrow => { + js.typescript_required("any"); + js.cx.expose_borrowed_objects(); + js.cx.expose_global_stack_pointer(); + let val = js.pop(); + js.push(format!("addBorrowedObject({})", val)); + js.finally("heap[stack_pointer++] = undefined;"); + } + + Instruction::I32FromAnyrefRustOwned { class } => { + js.typescript_required(class); + let val = js.pop(); + js.assert_class(&val, &class); + js.assert_not_moved(&val); + let i = js.tmp(); + js.prelude(&format!("var ptr{} = {}.ptr;", i, val)); + js.prelude(&format!("{}.ptr = 0;", val)); + js.push(format!("ptr{}", i)); + } + + Instruction::I32FromAnyrefRustBorrow { class } => { + js.typescript_required(class); + let val = js.pop(); + js.assert_class(&val, &class); + js.assert_not_moved(&val); + js.push(format!("{}.ptr", val)); + } + + Instruction::I32FromOptionRust { class } => { + js.typescript_optional(class); + let val = js.pop(); + js.cx.expose_is_like_none(); + let i = js.tmp(); + js.prelude(&format!("let ptr{} = 0;", i)); + js.prelude(&format!("if (!isLikeNone({0})) {{", val)); + js.assert_class(&val, class); + js.assert_not_moved(&val); + js.prelude(&format!("ptr{} = {}.ptr;", i, val)); + js.prelude(&format!("{}.ptr = 0;", val)); + js.prelude("}"); + js.push(format!("ptr{}", i)); + } + + Instruction::I32Split64 { signed } => { + js.typescript_required("BigInt"); + let val = js.pop(); + let f = if *signed { + js.cx.expose_int64_cvt_shim() + } else { + js.cx.expose_uint64_cvt_shim() + }; + let i = js.tmp(); + js.prelude(&format!( + " + {f}[0] = {val}; + const low{i} = u32CvtShim[0]; + const high{i} = u32CvtShim[1]; + ", + i = i, + f = f, + val = val, + )); + js.push(format!("low{}", i)); + js.push(format!("high{}", i)); + } + + Instruction::I32SplitOption64 { signed } => { + js.typescript_optional("BigInt"); + let val = js.pop(); + js.cx.expose_is_like_none(); + let f = if *signed { + js.cx.expose_int64_cvt_shim() + } else { + js.cx.expose_uint64_cvt_shim() + }; + let i = js.tmp(); + js.prelude(&format!( + "\ + {f}[0] = isLikeNone({val}) ? BigInt(0) : {val}; + const low{i} = u32CvtShim[0]; + const high{i} = u32CvtShim[1]; + ", + i = i, + f = f, + val = val, + )); + js.push(format!("!isLikeNone({0})", val)); + js.push(format!("low{}", i)); + js.push(format!("high{}", i)); + } + + Instruction::I32FromOptionAnyref => { + js.typescript_optional("any"); + let val = js.pop(); + js.cx.expose_is_like_none(); + match (js.cx.aux.anyref_table, js.cx.aux.anyref_alloc) { + (Some(table), Some(alloc)) => { + let alloc = js.cx.expose_add_to_anyref_table(table, alloc)?; + js.push(format!("isLikeNone({0}) ? 0 : {1}({0})", val, alloc)); + } + _ => { + js.cx.expose_add_heap_object(); + js.push(format!("isLikeNone({0}) ? 0 : addHeapObject({0})", val)); + } + } + } + + Instruction::I32FromOptionU32Sentinel => { + js.typescript_optional("number"); + let val = js.pop(); + js.cx.expose_is_like_none(); + js.assert_optional_number(&val); + js.push(format!("isLikeNone({0}) ? 0xFFFFFF : {0}", val)); + } + + Instruction::I32FromOptionBool => { + js.typescript_optional("boolean"); + let val = js.pop(); + js.cx.expose_is_like_none(); + js.assert_optional_bool(&val); + js.push(format!("isLikeNone({0}) ? 0xFFFFFF : {0} ? 1 : 0", val)); + } + + Instruction::I32FromOptionChar => { + js.typescript_optional("string"); + let val = js.pop(); + js.cx.expose_is_like_none(); + js.push(format!( + "isLikeNone({0}) ? 0xFFFFFF : {0}.codePointAt(0)", + val + )); + } + + Instruction::I32FromOptionEnum { hole } => { + js.typescript_optional("number"); + let val = js.pop(); + js.cx.expose_is_like_none(); + js.assert_optional_number(&val); + js.push(format!("isLikeNone({0}) ? {1} : {0}", val, hole)); + } + + Instruction::FromOptionNative { .. } => { + js.typescript_optional("number"); + let val = js.pop(); + js.cx.expose_is_like_none(); + js.assert_optional_number(&val); + js.push(format!("!isLikeNone({0})", val)); + js.push(format!("isLikeNone({0}) ? 0 : {0}", val)); + } + + Instruction::VectorToMemory { kind, malloc, mem } => { + js.typescript_required(kind.js_ty()); + let val = js.pop(); + let func = js.cx.pass_to_wasm_function(*kind, *mem)?; + let malloc = js.cx.export_name_of(*malloc); + let i = js.tmp(); + js.prelude(&format!( + "var ptr{i} = {f}({0}, wasm.{malloc});", + val, + i = i, + f = func, + malloc = malloc, + )); + js.prelude(&format!("var len{} = WASM_VECTOR_LEN;", i)); + js.push(format!("ptr{}", i)); + js.push(format!("len{}", i)); + } + + Instruction::OptionVector { kind, mem, malloc } => { + js.typescript_optional(kind.js_ty()); + let func = js.cx.pass_to_wasm_function(*kind, *mem)?; + js.cx.expose_is_like_none(); + let i = js.tmp(); + let malloc = js.cx.export_name_of(*malloc); + let val = js.pop(); + js.prelude(&format!( + "var ptr{i} = isLikeNone({0}) ? 0 : {f}({0}, wasm.{malloc});", + val, + i = i, + f = func, + malloc = malloc, + )); + js.prelude(&format!("var len{} = WASM_VECTOR_LEN;", i)); + js.push(format!("ptr{}", i)); + js.push(format!("len{}", i)); + } + + Instruction::MutableSliceToMemory { + kind, + malloc, + mem, + free, + } => { + js.typescript_required(kind.js_ty()); + // First up, pass the JS value into wasm, getting out a pointer and + // a length. These two pointer/length values get pushed onto the + // value stack. + let val = js.pop(); + let func = js.cx.pass_to_wasm_function(*kind, *mem)?; + let malloc = js.cx.export_name_of(*malloc); + let i = js.tmp(); + js.prelude(&format!( + "var ptr{i} = {f}({val}, wasm.{malloc});", + val = val, + i = i, + f = func, + malloc = malloc, + )); + js.prelude(&format!("var len{} = WASM_VECTOR_LEN;", i)); + js.push(format!("ptr{}", i)); + js.push(format!("len{}", i)); + + // Next we set up a `finally` clause which will both update the + // original mutable slice with any modifications, and then free the + // Rust-backed memory. + let free = js.cx.export_name_of(*free); + let get = js.cx.memview_function(*kind, *mem); + js.finally(&format!( + " + {val}.set({get}().subarray(ptr{i} / {size}, ptr{i} / {size} + len{i})); + wasm.{free}(ptr{i}, len{i} * {size}); + ", + val = val, + get = get, + free = free, + size = kind.size(), + i = i, + )); + } + + Instruction::BoolFromI32 => { + js.typescript_required("bool"); + let val = js.pop(); + js.push(format!("{} !== 0", val)); + } + + Instruction::AnyrefLoadOwned => { + js.typescript_required("any"); + js.cx.expose_take_object(); + let val = js.pop(); + js.push(format!("takeObject({})", val)); + } + + Instruction::StringFromChar => { + js.typescript_required("string"); + let val = js.pop(); + js.push(format!("String.fromCodePoint({})", val)); + } + + Instruction::I64FromLoHi { signed } => { + js.typescript_required("BigInt"); + let f = if *signed { + js.cx.expose_int64_cvt_shim() + } else { + js.cx.expose_uint64_cvt_shim() + }; + let i = js.tmp(); + let high = js.pop(); + let low = js.pop(); + js.prelude(&format!( + "\ + u32CvtShim[0] = {low}; + u32CvtShim[1] = {high}; + const n{i} = {f}[0]; + ", + low = low, + high = high, + f = f, + i = i, + )); + js.push(format!("n{}", i)) + } + + Instruction::RustFromI32 { class } => { + js.typescript_required(class); + js.cx.require_class_wrap(class); + let val = js.pop(); + js.push(format!("{}.__wrap({})", class, val)); + } + + Instruction::OptionRustFromI32 { class } => { + js.typescript_optional(class); + js.cx.require_class_wrap(class); + let val = js.pop(); + js.push(format!( + "{0} === 0 ? undefined : {1}.__wrap({0})", + val, class, + )) + } + + Instruction::CachedStringLoad { + owned, + optional, + mem, + free, + } => { + if *optional { + js.typescript_optional("string"); + } else { + js.typescript_required("string"); + } + + let len = js.pop(); + let ptr = js.pop(); + let tmp = js.tmp(); + + let get = js.cx.expose_get_cached_string_from_wasm(*mem)?; + + js.prelude(&format!("var v{} = {}({}, {});", tmp, get, ptr, len)); + + if *owned { + let free = js.cx.export_name_of(*free); + js.prelude(&format!( + "if ({ptr} !== 0) {{ wasm.{}({ptr}, {len}); }}", + free, + ptr = ptr, + len = len, + )); + } + + js.push(format!("v{}", tmp)); + } + + Instruction::TableGet => { + js.typescript_required("any"); + let val = js.pop(); + js.cx.expose_get_object(); + js.push(format!("getObject({})", val)); + } + + Instruction::StackClosure { + adapter, + nargs, + mutable, + } => { + js.typescript_optional("any"); + let i = js.tmp(); + let b = js.pop(); + let a = js.pop(); + js.prelude(&format!("var state{} = {{a: {}, b: {}}};", i, a, b)); + let args = (0..*nargs) + .map(|i| format!("arg{}", i)) + .collect::>() + .join(", "); + let wrapper = js.cx.adapter_name(*adapter); + if *mutable { + // Mutable closures need protection against being called + // recursively, so ensure that we clear out one of the + // internal pointers while it's being invoked. + js.prelude(&format!( + "var cb{i} = ({args}) => {{ + const a = state{i}.a; + state{i}.a = 0; + try {{ + return {name}(a, state{i}.b, {args}); + }} finally {{ + state{i}.a = a; + }} + }};", + i = i, + args = args, + name = wrapper, + )); + } else { + js.prelude(&format!( + "var cb{i} = ({args}) => {wrapper}(state{i}.a, state{i}.b, {args});", + i = i, + args = args, + wrapper = wrapper, + )); + } + + // Make sure to null out our internal pointers when we return + // back to Rust to ensure that any lingering references to the + // closure will fail immediately due to null pointers passed in + // to Rust. + js.finally(&format!("state{}.a = state{0}.b = 0;", i)); + js.push(format!("cb{}", i)); + } + + Instruction::VectorLoad { kind, mem, free } => { + js.typescript_required(kind.js_ty()); + let len = js.pop(); + let ptr = js.pop(); + let f = js.cx.expose_get_vector_from_wasm(*kind, *mem)?; + let i = js.tmp(); + let free = js.cx.export_name_of(*free); + js.prelude(&format!("var v{} = {}({}, {}).slice();", i, f, ptr, len)); + js.prelude(&format!( + "wasm.{}({}, {} * {});", + free, + ptr, + len, + kind.size() + )); + js.push(format!("v{}", i)) + } + + Instruction::OptionVectorLoad { kind, mem, free } => { + js.typescript_optional(kind.js_ty()); + let len = js.pop(); + let ptr = js.pop(); + let f = js.cx.expose_get_vector_from_wasm(*kind, *mem)?; + let i = js.tmp(); + let free = js.cx.export_name_of(*free); + js.prelude(&format!("let v{};", i)); + js.prelude(&format!("if ({} !== 0) {{", ptr)); + js.prelude(&format!("v{} = {}({}, {}).slice();", i, f, ptr, len)); + js.prelude(&format!( + "wasm.{}({}, {} * {});", + free, + ptr, + len, + kind.size() + )); + js.prelude("}"); + js.push(format!("v{}", i)); + } + + Instruction::View { kind, mem } => { + js.typescript_required(kind.js_ty()); + let len = js.pop(); + let ptr = js.pop(); + let f = js.cx.expose_get_vector_from_wasm(*kind, *mem)?; + js.push(format!("{f}({ptr}, {len})", ptr = ptr, len = len, f = f)); + } + + Instruction::OptionView { kind, mem } => { + js.typescript_optional(kind.js_ty()); + let len = js.pop(); + let ptr = js.pop(); + let f = js.cx.expose_get_vector_from_wasm(*kind, *mem)?; + js.push(format!( + "{ptr} === 0 ? undefined : {f}({ptr}, {len})", + ptr = ptr, + len = len, + f = f + )); + } + + Instruction::OptionU32Sentinel => { + js.typescript_optional("number"); + let val = js.pop(); + js.push(format!("{0} === 0xFFFFFF ? undefined : {0}", val)); + } + + Instruction::ToOptionNative { ty: _, signed } => { + js.typescript_optional("number"); + let val = js.pop(); + let present = js.pop(); + js.push(format!( + "{} === 0 ? undefined : {}{}", + present, + val, + if *signed { "" } else { " >>> 0" }, + )); + } + + Instruction::OptionBoolFromI32 => { + js.typescript_optional("boolean"); + let val = js.pop(); + js.push(format!("{0} === 0xFFFFFF ? undefined : {0} !== 0", val)); + } + + Instruction::OptionCharFromI32 => { + js.typescript_optional("string"); + let val = js.pop(); + js.push(format!( + "{0} === 0xFFFFFF ? undefined : String.fromCodePoint({0})", + val, + )); + } + + Instruction::OptionEnumFromI32 { hole } => { + js.typescript_optional("number"); + let val = js.pop(); + js.push(format!("{0} === {1} ? undefined : {0}", val, hole)); + } + + Instruction::Option64FromI32 { signed } => { + js.typescript_optional("BigInt"); + let f = if *signed { + js.cx.expose_int64_cvt_shim() + } else { + js.cx.expose_uint64_cvt_shim() + }; + let i = js.tmp(); + let high = js.pop(); + let low = js.pop(); + let present = js.pop(); + js.prelude(&format!( + " + u32CvtShim[0] = {low}; + u32CvtShim[1] = {high}; + const n{i} = {present} === 0 ? undefined : {f}[0]; + ", + present = present, + low = low, + high = high, + f = f, + i = i, + )); + js.push(format!("n{}", i)); + } + } + Ok(()) +} + +enum Invocation { + Core { id: walrus::FunctionId, defer: bool }, + Adapter(AdapterId), +} + +impl Invocation { + fn from(instr: &Instruction, module: &Module) -> Result { + use Instruction::*; + Ok(match instr { + Standard(wit_walrus::Instruction::CallCore(f)) => Invocation::Core { + id: *f, + defer: false, + }, + + Standard(wit_walrus::Instruction::DeferCallCore(f)) => Invocation::Core { + id: *f, + defer: true, + }, + + CallExport(e) => match module.exports.get(*e).item { + walrus::ExportItem::Function(id) => Invocation::Core { id, defer: false }, + _ => panic!("can only call exported function"), + }, + + // The function table never changes right now, so we can statically + // look up the desired function. + CallTableElement(idx) => { + let table = module + .tables + .main_function_table()? + .ok_or_else(|| anyhow!("no function table found"))?; + let functions = match &module.tables.get(table).kind { + walrus::TableKind::Function(f) => f, + _ => bail!("should have found a function table"), + }; + let id = functions + .elements + .get(*idx as usize) + .and_then(|id| *id) + .ok_or_else(|| anyhow!("function table wasn't filled in a {}", idx))?; + Invocation::Core { id, defer: false } + } + + CallAdapter(id) => Invocation::Adapter(*id), + + // this function is only called for the above instructions + _ => unreachable!(), + }) + } + + fn params_results(&self, cx: &Context) -> (usize, usize) { + match self { + Invocation::Core { id, .. } => { + let ty = cx.module.funcs.get(*id).ty(); + let ty = cx.module.types.get(ty); + (ty.params().len(), ty.results().len()) + } + Invocation::Adapter(id) => { + let adapter = &cx.wit.adapters[id]; + (adapter.params.len(), adapter.results.len()) + } + } + } + + fn invoke( + &self, + cx: &mut Context, + args: &[String], + prelude: &mut String, + log_error: &mut bool, + ) -> Result { + match self { + Invocation::Core { id, .. } => { + let name = cx.export_name_of(*id); + Ok(format!("wasm.{}({})", name, args.join(", "))) + } + Invocation::Adapter(id) => { + let adapter = &cx.wit.adapters[id]; + let kind = match adapter.kind { + AdapterKind::Import { kind, .. } => kind, + AdapterKind::Local { .. } => { + bail!("adapter-to-adapter calls not supported yet"); + } + }; + let import = &cx.aux.import_map[id]; + let variadic = cx.aux.imports_with_variadic.contains(id); + if cx.import_never_log_error(import) { + *log_error = false; + } + cx.invoke_import(import, kind, args, variadic, prelude) + } + } + } + + fn defer(&self) -> bool { + match self { + Invocation::Core { defer, .. } => *defer, + _ => false, + } + } } diff --git a/crates/cli-support/src/js/incoming.rs b/crates/cli-support/src/js/incoming.rs deleted file mode 100644 index e5592318..00000000 --- a/crates/cli-support/src/js/incoming.rs +++ /dev/null @@ -1,556 +0,0 @@ -//! Implementation of taking a `NonstandardIncoming` binding and generating JS -//! which represents it and executes it for what we need. -//! -//! This module is used to generate JS for all our incoming bindings which -//! includes arguments going into exports or return values from imports. - -use crate::descriptor::VectorKind; -use crate::js::binding::JsBuilder; -use crate::js::Context; -use crate::webidl::NonstandardIncoming; -use anyhow::{bail, Error}; -use wasm_webidl_bindings::ast; - -pub struct Incoming<'a, 'b> { - cx: &'a mut Context<'b>, - types: &'a [ast::WebidlTypeRef], - js: &'a mut JsBuilder, -} - -impl<'a, 'b> Incoming<'a, 'b> { - pub fn new( - cx: &'a mut Context<'b>, - types: &'a [ast::WebidlTypeRef], - js: &'a mut JsBuilder, - ) -> Incoming<'a, 'b> { - Incoming { cx, types, js } - } - - pub fn process(&mut self, incoming: &NonstandardIncoming) -> Result, Error> { - let before = self.js.typescript_len(); - let ret = self.nonstandard(incoming)?; - assert_eq!(before + 1, self.js.typescript_len()); - Ok(ret) - } - - fn nonstandard(&mut self, incoming: &NonstandardIncoming) -> Result, Error> { - let single = match incoming { - NonstandardIncoming::Standard(val) => return self.standard(val), - - // Evaluate the `val` binding, store it into a one-element `BigInt` - // array (appropriately typed) and then use a 32-bit view into the - // `BigInt` array to extract the high/low bits and pass them through - // in the ABI. - NonstandardIncoming::Int64 { val, signed } => { - self.js.typescript_required("BigInt"); - let (expr, ty) = self.standard_typed(val)?; - assert_eq!(ty, ast::WebidlScalarType::Any.into()); - let f = if *signed { - self.cx.expose_int64_cvt_shim() - } else { - self.cx.expose_uint64_cvt_shim() - }; - self.cx.expose_uint32_memory(); - let i = self.js.tmp(); - self.js.prelude(&format!( - " - {f}[0] = {expr}; - const low{i} = u32CvtShim[0]; - const high{i} = u32CvtShim[1]; - ", - i = i, - f = f, - expr = expr, - )); - return Ok(vec![format!("low{}", i), format!("high{}", i)]); - } - - // Same as `IncomingBindingExpressionAllocCopy`, except we use a - // different `VectorKind` - NonstandardIncoming::AllocCopyInt64 { - alloc_func_name: _, - expr, - signed, - } => { - let (expr, ty) = self.standard_typed(expr)?; - assert_eq!(ty, ast::WebidlScalarType::Any.into()); - let kind = if *signed { - VectorKind::I64 - } else { - VectorKind::U64 - }; - let func = self.cx.pass_to_wasm_function(kind)?; - self.js.typescript_required(kind.js_ty()); - return Ok(vec![ - format!("{}({})", func, expr), - "WASM_VECTOR_LEN".to_string(), - ]); - } - - // Same as `IncomingBindingExpressionAllocCopy`, except we use a - // different `VectorKind` - NonstandardIncoming::AllocCopyAnyrefArray { - alloc_func_name: _, - expr, - } => { - let (expr, ty) = self.standard_typed(expr)?; - assert_eq!(ty, ast::WebidlScalarType::Any.into()); - let func = self.cx.pass_to_wasm_function(VectorKind::Anyref)?; - self.js.typescript_required(VectorKind::Anyref.js_ty()); - return Ok(vec![ - format!("{}({})", func, expr), - "WASM_VECTOR_LEN".to_string(), - ]); - } - - // There's no `char` in JS, so we take a string instead and just - // forward along the first code point to Rust. - NonstandardIncoming::Char { val } => { - let (expr, ty) = self.standard_typed(val)?; - assert_eq!(ty, ast::WebidlScalarType::DomString.into()); - self.js.typescript_required("string"); - format!("{}.codePointAt(0)", expr) - } - - // When moving a type back into Rust we need to clear out the - // internal pointer in JS to prevent it from being reused again in - // the future. - NonstandardIncoming::RustType { class, val } => { - let (expr, ty) = self.standard_typed(val)?; - assert_eq!(ty, ast::WebidlScalarType::Any.into()); - self.assert_class(&expr, &class); - self.assert_not_moved(&expr); - let i = self.js.tmp(); - self.js.prelude(&format!("const ptr{} = {}.ptr;", i, expr)); - self.js.prelude(&format!("{}.ptr = 0;", expr)); - self.js.typescript_required(class); - format!("ptr{}", i) - } - - // Here we can simply pass along the pointer with no extra fluff - // needed. - NonstandardIncoming::RustTypeRef { class, val } => { - let (expr, ty) = self.standard_typed(val)?; - assert_eq!(ty, ast::WebidlScalarType::Any.into()); - self.assert_class(&expr, &class); - self.assert_not_moved(&expr); - self.js.typescript_required(class); - format!("{}.ptr", expr) - } - - // the "stack-ful" nature means that we're always popping from the - // stack, and make sure that we actually clear our reference to - // allow stale values to get GC'd - NonstandardIncoming::BorrowedAnyref { val } => { - let (expr, ty) = self.standard_typed(val)?; - assert_eq!(ty, ast::WebidlScalarType::Any.into()); - self.cx.expose_borrowed_objects(); - self.cx.expose_global_stack_pointer(); - self.js.finally("heap[stack_pointer++] = undefined;"); - self.js.typescript_required("any"); - format!("addBorrowedObject({})", expr) - } - - // Similar to `AllocCopy`, except that we deallocate in a finally - // block. - NonstandardIncoming::MutableSlice { kind, val } => { - let (expr, ty) = self.standard_typed(val)?; - assert_eq!(ty, ast::WebidlScalarType::Any.into()); - let func = self.cx.pass_to_wasm_function(*kind)?; - let i = self.js.tmp(); - self.js - .prelude(&format!("const ptr{} = {}({});", i, func, expr)); - self.js - .prelude(&format!("const len{} = WASM_VECTOR_LEN;", i)); - self.finally_free_slice(&expr, i, *kind, true)?; - self.js.typescript_required(kind.js_ty()); - return Ok(vec![format!("ptr{}", i), format!("len{}", i)]); - } - - // Pass `None` as a sentinel value that `val` will never take on. - // This is only manufactured for specific underlying types. - NonstandardIncoming::OptionU32Sentinel { val } => { - let (expr, ty) = self.standard_typed(val)?; - assert_eq!(ty, ast::WebidlScalarType::Any.into()); - self.cx.expose_is_like_none(); - self.js.typescript_optional("number"); - self.assert_optional_number(&expr); - format!("isLikeNone({0}) ? 0xFFFFFF : {0}", expr) - } - - // Pass `true` as 1, `false` as 0, and `None` as a sentinel value. - NonstandardIncoming::OptionBool { val } => { - let (expr, ty) = self.standard_typed(val)?; - assert_eq!(ty, ast::WebidlScalarType::Any.into()); - self.cx.expose_is_like_none(); - self.js.typescript_optional("boolean"); - self.assert_optional_bool(&expr); - format!("isLikeNone({0}) ? 0xFFFFFF : {0} ? 1 : 0", expr) - } - - // Pass `None` as a sentinel value a character can never have - NonstandardIncoming::OptionChar { val } => { - let (expr, ty) = self.standard_typed(val)?; - assert_eq!(ty, ast::WebidlScalarType::Any.into()); - self.cx.expose_is_like_none(); - self.js.typescript_optional("string"); - format!("isLikeNone({0}) ? 0xFFFFFF : {0}.codePointAt(0)", expr) - } - - // Pass `None` as the hole in the enum which no valid value can ever - // take - NonstandardIncoming::OptionIntegerEnum { val, hole } => { - let (expr, ty) = self.standard_typed(val)?; - assert_eq!(ty, ast::WebidlScalarType::Any.into()); - self.cx.expose_is_like_none(); - self.js.typescript_optional("number"); - self.assert_optional_number(&expr); - format!("isLikeNone({0}) ? {1} : {0}", expr, hole) - } - - // `None` here is zero, but if `Some` then we need to clear out the - // internal pointer because the value is being moved. - NonstandardIncoming::OptionRustType { class, val } => { - let (expr, ty) = self.standard_typed(val)?; - assert_eq!(ty, ast::WebidlScalarType::Any.into()); - self.cx.expose_is_like_none(); - let i = self.js.tmp(); - self.js.prelude(&format!("let ptr{} = 0;", i)); - self.js.prelude(&format!("if (!isLikeNone({0})) {{", expr)); - self.assert_class(&expr, class); - self.assert_not_moved(&expr); - self.js.prelude(&format!("ptr{} = {}.ptr;", i, expr)); - self.js.prelude(&format!("{}.ptr = 0;", expr)); - self.js.prelude("}"); - self.js.typescript_optional(class); - format!("ptr{}", i) - } - - // The ABI produces four values here, all zero for `None` and 1 in - // the first for the last two being the low/high bits - NonstandardIncoming::OptionInt64 { val, signed } => { - let (expr, ty) = self.standard_typed(val)?; - assert_eq!(ty, ast::WebidlScalarType::Any.into()); - self.cx.expose_is_like_none(); - let f = if *signed { - self.cx.expose_int64_cvt_shim() - } else { - self.cx.expose_uint64_cvt_shim() - }; - self.cx.expose_uint32_memory(); - let i = self.js.tmp(); - self.js.prelude(&format!( - "\ - {f}[0] = isLikeNone({expr}) ? BigInt(0) : {expr}; - const low{i} = isLikeNone({expr}) ? 0 : u32CvtShim[0]; - const high{i} = isLikeNone({expr}) ? 0 : u32CvtShim[1]; - ", - i = i, - f = f, - expr = expr, - )); - self.js.typescript_optional("BigInt"); - return Ok(vec![ - format!("!isLikeNone({0})", expr), - "0".to_string(), - format!("low{}", i), - format!("high{}", i), - ]); - } - - // The ABI here is always an integral index into the anyref table, - // and the anyref table just differs based on whether we ran the - // anyref pass or not. - NonstandardIncoming::OptionAnyref { val } => { - let (expr, ty) = self.standard_typed(val)?; - assert_eq!(ty, ast::WebidlScalarType::Any.into()); - self.cx.expose_is_like_none(); - self.js.typescript_optional("any"); - if self.cx.config.anyref { - self.cx.expose_add_to_anyref_table()?; - format!("isLikeNone({0}) ? 0 : addToAnyrefTable({0})", expr) - } else { - self.cx.expose_add_heap_object(); - format!("isLikeNone({0}) ? 0 : addHeapObject({0})", expr) - } - } - - // Native types of wasm take a leading discriminant to indicate - // whether the next value is valid or not. - NonstandardIncoming::OptionNative { val } => { - let (expr, ty) = self.standard_typed(val)?; - assert_eq!(ty, ast::WebidlScalarType::Any.into()); - self.cx.expose_is_like_none(); - self.js.typescript_optional("number"); - self.assert_optional_number(&expr); - return Ok(vec![ - format!("!isLikeNone({0})", expr), - format!("isLikeNone({0}) ? 0 : {0}", expr), - ]); - } - - // Similar to `AllocCopy`, except we're handling the undefined case - // and passing null for the pointer value. - NonstandardIncoming::OptionVector { kind, val } => { - let (expr, ty) = self.standard_typed(val)?; - assert_eq!(ty, ast::WebidlScalarType::Any.into()); - let func = self.cx.pass_to_wasm_function(*kind)?; - self.cx.expose_is_like_none(); - let i = self.js.tmp(); - self.js.prelude(&format!( - "const ptr{i} = isLikeNone({0}) ? 0 : {f}({0});", - expr, - i = i, - f = func, - )); - self.js - .prelude(&format!("const len{} = WASM_VECTOR_LEN;", i)); - self.js.typescript_optional(kind.js_ty()); - return Ok(vec![format!("ptr{}", i), format!("len{}", i)]); - } - - // An unfortunate smorgasboard of handling slices, transfers if - // mutable, etc. Not the prettiest binding option here, and of - // course never going to be standardized. - NonstandardIncoming::OptionSlice { kind, val, mutable } => { - let (expr, ty) = self.standard_typed(val)?; - assert_eq!(ty, ast::WebidlScalarType::Any.into()); - let func = self.cx.pass_to_wasm_function(*kind)?; - self.cx.expose_is_like_none(); - let i = self.js.tmp(); - self.js.prelude(&format!( - "const ptr{i} = isLikeNone({0}) ? 0 : {f}({0});", - expr, - i = i, - f = func, - )); - self.js - .prelude(&format!("const len{} = WASM_VECTOR_LEN;", i)); - self.js.finally(&format!("if (ptr{} !== 0) {{", i)); - self.finally_free_slice(&expr, i, *kind, *mutable)?; - self.js.finally("}"); - self.js.typescript_optional(kind.js_ty()); - return Ok(vec![format!("ptr{}", i), format!("len{}", i)]); - } - }; - Ok(vec![single]) - } - - /// Evaluates the `standard` binding expression, returning the JS expression - /// needed to evaluate the binding. - fn standard( - &mut self, - standard: &ast::IncomingBindingExpression, - ) -> Result, Error> { - let single = match standard { - ast::IncomingBindingExpression::As(as_) => { - let (expr, ty) = self.standard_typed(&as_.expr)?; - match ty { - ast::WebidlTypeRef::Scalar(ast::WebidlScalarType::Any) => { - self.js.typescript_required("any"); - - // If the type here is anyref but we didn't run the - // anyref pass that means we have to instead actually - // pass in an index - // - // TODO: we should ideally move this `addHeapObject` - // into a nonstanard binding whenever the anyref pass - // doesn't already run rather than implicitly picking - // it up here - if self.cx.config.anyref { - expr - } else { - self.cx.expose_add_heap_object(); - format!("addHeapObject({})", expr) - } - } - ast::WebidlTypeRef::Scalar(ast::WebidlScalarType::Boolean) => { - self.js.typescript_required("boolean"); - self.assert_bool(&expr); - // JS will already coerce booleans into numbers for us - expr - } - _ => { - self.js.typescript_required("number"); - self.assert_number(&expr); - expr - } - } - } - ast::IncomingBindingExpression::Get(_) => { - bail!("unsupported bare `get` in webidl bindings"); - } - ast::IncomingBindingExpression::AllocUtf8Str(expr) => { - let (expr, ty) = self.standard_typed(&expr.expr)?; - assert_eq!(ty, ast::WebidlScalarType::DomString.into()); - self.js.typescript_required("string"); - self.cx.expose_pass_string_to_wasm()?; - return Ok(vec![ - format!("passStringToWasm({})", expr), - "WASM_VECTOR_LEN".to_string(), - ]); - } - ast::IncomingBindingExpression::AllocCopy(expr) => { - let (expr, ty) = self.standard_typed(&expr.expr)?; - let scalar = match ty { - ast::WebidlTypeRef::Scalar(s) => s, - ast::WebidlTypeRef::Id(_) => { - bail!("unsupported type passed to `alloc-copy` in webidl binding") - } - }; - let kind = match scalar { - ast::WebidlScalarType::Int8Array => VectorKind::I8, - ast::WebidlScalarType::Uint8Array => VectorKind::U8, - ast::WebidlScalarType::Uint8ClampedArray => VectorKind::ClampedU8, - ast::WebidlScalarType::Int16Array => VectorKind::I16, - ast::WebidlScalarType::Uint16Array => VectorKind::U16, - ast::WebidlScalarType::Int32Array => VectorKind::I32, - ast::WebidlScalarType::Uint32Array => VectorKind::U32, - ast::WebidlScalarType::Float32Array => VectorKind::F32, - ast::WebidlScalarType::Float64Array => VectorKind::F64, - _ => bail!("unsupported type passed to alloc-copy: {:?}", scalar), - }; - self.js.typescript_required(kind.js_ty()); - let func = self.cx.pass_to_wasm_function(kind)?; - return Ok(vec![ - format!("{}({})", func, expr), - "WASM_VECTOR_LEN".to_string(), - ]); - } - ast::IncomingBindingExpression::EnumToI32(_) => { - bail!("unsupported enum-to-i32 conversion in webidl binding"); - } - ast::IncomingBindingExpression::Field(_) => { - bail!("unsupported field accessor in webidl binding"); - } - ast::IncomingBindingExpression::BindImport(_) => { - bail!("unsupported import binding in webidl binding"); - } - }; - Ok(vec![single]) - } - - /// Evaluates the `standard` binding expression, returning both the - /// JS expression to evaluate along with the WebIDL type of the expression. - /// - /// Currently only supports `Get`. - fn standard_typed( - &mut self, - standard: &ast::IncomingBindingExpression, - ) -> Result<(String, ast::WebidlTypeRef), Error> { - match standard { - ast::IncomingBindingExpression::As(_) => { - bail!("unsupported as in webidl binding"); - } - ast::IncomingBindingExpression::Get(expr) => { - let arg = self.js.arg(expr.idx).to_string(); - let ty = self.types[expr.idx as usize]; - Ok((arg, ty)) - } - ast::IncomingBindingExpression::AllocUtf8Str(_) => { - bail!("unsupported alloc-utf8-str in webidl binding"); - } - ast::IncomingBindingExpression::AllocCopy(_) => { - bail!("unsupported alloc-copy in webidl binding"); - } - ast::IncomingBindingExpression::EnumToI32(_) => { - bail!("unsupported enum-to-i32 in webidl binding"); - } - ast::IncomingBindingExpression::Field(_) => { - bail!("unsupported field accessor in webidl binding"); - } - ast::IncomingBindingExpression::BindImport(_) => { - bail!("unsupported import binding in webidl binding"); - } - } - } - - fn assert_class(&mut self, arg: &str, class: &str) { - self.cx.expose_assert_class(); - self.js - .prelude(&format!("_assertClass({}, {});", arg, class)); - } - - fn assert_number(&mut self, arg: &str) { - if !self.cx.config.debug { - return; - } - self.cx.expose_assert_num(); - self.js.prelude(&format!("_assertNum({});", arg)); - } - - fn assert_bool(&mut self, arg: &str) { - if !self.cx.config.debug { - return; - } - self.cx.expose_assert_bool(); - self.js.prelude(&format!("_assertBoolean({});", arg)); - } - - fn assert_optional_number(&mut self, arg: &str) { - if !self.cx.config.debug { - return; - } - self.cx.expose_is_like_none(); - self.js.prelude(&format!("if (!isLikeNone({})) {{", arg)); - self.assert_number(arg); - self.js.prelude("}"); - } - - fn assert_optional_bool(&mut self, arg: &str) { - if !self.cx.config.debug { - return; - } - self.cx.expose_is_like_none(); - self.js.prelude(&format!("if (!isLikeNone({})) {{", arg)); - self.assert_bool(arg); - self.js.prelude("}"); - } - - fn assert_not_moved(&mut self, arg: &str) { - if !self.cx.config.debug { - return; - } - self.js.prelude(&format!( - "\ - if ({0}.ptr === 0) {{ - throw new Error('Attempt to use a moved value'); - }} - ", - arg, - )); - } - - fn finally_free_slice( - &mut self, - expr: &str, - i: usize, - kind: VectorKind, - mutable: bool, - ) -> Result<(), Error> { - // If the slice was mutable it's currently a feature that we - // mirror back updates to the original slice. This... is - // arguably a misfeature of wasm-bindgen... - if mutable { - let get = self.cx.memview_function(kind); - self.js.finally(&format!( - "\ - {arg}.set({get}().subarray(\ - ptr{i} / {size}, \ - ptr{i} / {size} + len{i}\ - ));\ - ", - i = i, - arg = expr, - get = get, - size = kind.size() - )); - } - self.js.finally(&format!( - "wasm.__wbindgen_free(ptr{i}, len{i} * {size});", - i = i, - size = kind.size(), - )); - self.cx.require_internal_export("__wbindgen_free") - } -} diff --git a/crates/cli-support/src/js/mod.rs b/crates/cli-support/src/js/mod.rs index d1dcfad8..8869f4e1 100644 --- a/crates/cli-support/src/js/mod.rs +++ b/crates/cli-support/src/js/mod.rs @@ -1,29 +1,31 @@ use crate::descriptor::VectorKind; use crate::intrinsic::Intrinsic; -use crate::webidl; -use crate::webidl::{AuxEnum, AuxExport, AuxExportKind, AuxImport, AuxStruct}; -use crate::webidl::{AuxValue, Binding}; -use crate::webidl::{JsImport, JsImportName, NonstandardWebidlSection, WasmBindgenAux}; +use crate::wit::{Adapter, AdapterId, AdapterJsImportKind, AuxValue}; +use crate::wit::{AdapterKind, Instruction, InstructionData}; +use crate::wit::{AuxEnum, AuxExport, AuxExportKind, AuxImport, AuxStruct}; +use crate::wit::{JsImport, JsImportName, NonstandardWitSection, WasmBindgenAux}; use crate::{Bindgen, EncodeInto, OutputMode}; -use anyhow::{bail, Context as _, Error}; +use anyhow::{anyhow, bail, Context as _, Error}; +use std::borrow::Cow; use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet}; +use std::fmt; use std::fs; use std::path::{Path, PathBuf}; -use walrus::{ExportId, ImportId, MemoryId, Module}; -use wasm_webidl_bindings::ast; +use walrus::{ExportId, FunctionId, ImportId, MemoryId, Module, TableId}; mod binding; -mod incoming; -mod outgoing; pub struct Context<'a> { globals: String, imports_post: String, typescript: String, - exposed_globals: Option>, - required_internal_exports: HashSet<&'static str>, + exposed_globals: Option>>, + required_internal_exports: HashSet>, + next_export_idx: usize, config: &'a Bindgen, pub module: &'a mut Module, + aux: &'a WasmBindgenAux, + wit: &'a NonstandardWitSection, /// A map representing the `import` statements we'll be generating in the JS /// glue. The key is the module we're importing from and the value is the @@ -43,11 +45,15 @@ pub struct Context<'a> { defined_identifiers: HashMap, exported_classes: Option>, - memory: MemoryId, /// A map of the name of npm dependencies we've loaded so far to the path /// they're defined in as well as their version specification. pub npm_dependencies: HashMap, + + /// A mapping of a index for memories as we see them. Used in function + /// names. + memory_indices: HashMap, + table_indices: HashMap, } #[derive(Default)] @@ -70,19 +76,12 @@ const INITIAL_HEAP_VALUES: &[&str] = &["undefined", "null", "true", "false"]; const INITIAL_HEAP_OFFSET: usize = 32; impl<'a> Context<'a> { - pub fn new(module: &'a mut Module, config: &'a Bindgen) -> Result, Error> { - // Find the single memory, if there is one, and for ease of use in our - // binding generation just inject one if there's not one already (and - // we'll clean it up later if we end up not using it). - let mut memories = module.memories.iter().map(|m| m.id()); - let memory = memories.next(); - if memories.next().is_some() { - bail!("multiple memories currently not supported"); - } - drop(memories); - let memory = memory.unwrap_or_else(|| module.memories.add_local(false, 1, None)); - - // And then we're good to go! + pub fn new( + module: &'a mut Module, + config: &'a Bindgen, + wit: &'a NonstandardWitSection, + aux: &'a WasmBindgenAux, + ) -> Result, Error> { Ok(Context { globals: String::new(), imports_post: String::new(), @@ -96,13 +95,17 @@ impl<'a> Context<'a> { exported_classes: Some(Default::default()), config, module, - memory, npm_dependencies: Default::default(), + next_export_idx: 0, + wit, + aux, + memory_indices: Default::default(), + table_indices: Default::default(), }) } - fn should_write_global(&mut self, name: &'static str) -> bool { - self.exposed_globals.as_mut().unwrap().insert(name) + fn should_write_global(&mut self, name: impl Into>) -> bool { + self.exposed_globals.as_mut().unwrap().insert(name.into()) } fn export( @@ -167,7 +170,7 @@ impl<'a> Context<'a> { } fn require_internal_export(&mut self, name: &'static str) -> Result<(), Error> { - if !self.required_internal_exports.insert(name) { + if !self.required_internal_exports.insert(name.into()) { return Ok(()); } @@ -442,31 +445,29 @@ impl<'a> Context<'a> { mut imports: Option<&mut String>, ) -> Result<(String, String), Error> { let module_name = "wbg"; - let mem = self.module.memories.get(self.memory); - let (init_memory1, init_memory2) = if let Some(id) = mem.import { - self.module.imports.get_mut(id).module = module_name.to_string(); - let mut memory = String::from("new WebAssembly.Memory({"); - memory.push_str(&format!("initial:{}", mem.initial)); - if let Some(max) = mem.maximum { - memory.push_str(&format!(",maximum:{}", max)); + let mut init_memory_arg = ""; + let mut init_memory1 = String::new(); + let mut init_memory2 = String::new(); + let mut has_memory = false; + if let Some(mem) = self.module.memories.iter().next() { + if let Some(id) = mem.import { + self.module.imports.get_mut(id).module = module_name.to_string(); + let mut memory = String::from("new WebAssembly.Memory({"); + memory.push_str(&format!("initial:{}", mem.initial)); + if let Some(max) = mem.maximum { + memory.push_str(&format!(",maximum:{}", max)); + } + if mem.shared { + memory.push_str(",shared:true"); + } + memory.push_str("})"); + self.imports_post.push_str("let memory;\n"); + init_memory1 = format!("memory = imports.{}.memory = maybe_memory;", module_name); + init_memory2 = format!("memory = imports.{}.memory = {};", module_name, memory); + init_memory_arg = ", maybe_memory"; + has_memory = true; } - if mem.shared { - memory.push_str(",shared:true"); - } - memory.push_str("})"); - self.imports_post.push_str("let memory;\n"); - ( - format!("memory = imports.{}.memory = maybe_memory;", module_name), - format!("memory = imports.{}.memory = {};", module_name, memory), - ) - } else { - (String::new(), String::new()) - }; - let init_memory_arg = if mem.import.is_some() { - ", maybe_memory" - } else { - "" - }; + } let default_module_path = match self.config.mode { OutputMode::Web => { @@ -478,7 +479,7 @@ impl<'a> Context<'a> { _ => "", }; - let ts = Self::ts_for_init_fn(mem.import.is_some(), !default_module_path.is_empty()); + let ts = Self::ts_for_init_fn(has_memory, !default_module_path.is_empty()); // Initialize the `imports` object for all import definitions that we're // directed to wire up. @@ -857,12 +858,7 @@ impl<'a> Context<'a> { self.global("let WASM_VECTOR_LEN = 0;"); } - fn expose_pass_string_to_wasm(&mut self) -> Result<(), Error> { - if !self.should_write_global("pass_string_to_wasm") { - return Ok(()); - } - - self.require_internal_export("__wbindgen_malloc")?; + fn expose_pass_string_to_wasm(&mut self, memory: MemoryId) -> Result { self.expose_wasm_vector_len(); let debug = if self.config.debug { @@ -878,25 +874,40 @@ impl<'a> Context<'a> { // doesn't require creating intermediate view using `subarray` // and also has `Buffer::byteLength` to calculate size upfront. if self.config.mode.nodejs() { - self.expose_node_buffer_memory(); + let get_buf = self.expose_node_buffer_memory(memory); + let ret = MemView { + name: "passStringToWasm", + num: get_buf.num, + }; + if !self.should_write_global(ret.to_string()) { + return Ok(ret); + } self.global(&format!( " - function passStringToWasm(arg) {{ + function {}(arg, malloc) {{ {} const len = Buffer.byteLength(arg); - const ptr = wasm.__wbindgen_malloc(len); - getNodeBufferMemory().write(arg, ptr, len); + const ptr = malloc(len); + {}().write(arg, ptr, len); WASM_VECTOR_LEN = len; return ptr; }} ", - debug, + ret, debug, get_buf, )); - return Ok(()); + return Ok(ret); } + let mem = self.expose_uint8_memory(memory); + let ret = MemView { + name: "passStringToWasm", + num: mem.num, + }; + if !self.should_write_global(ret.to_string()) { + return Ok(ret); + } self.expose_text_encoder()?; // The first implementation we have for this is to use @@ -920,7 +931,7 @@ impl<'a> Context<'a> { // Looks like `encodeInto` doesn't currently work when the memory passed // in is backed by a `SharedArrayBuffer`, so force usage of `encode` if // a `SharedArrayBuffer` is in use. - let shared = self.module.memories.get(self.memory).shared; + let shared = self.module.memories.get(memory).shared; match self.config.encode_into { EncodeInto::Always if !shared => { @@ -951,7 +962,6 @@ impl<'a> Context<'a> { } } - self.expose_uint8_memory(); self.require_internal_export("__wbindgen_realloc")?; // A fast path that directly writes char codes into WASM memory as long @@ -963,20 +973,23 @@ impl<'a> Context<'a> { // This might be not very intuitive, but such calls are usually more // expensive in mainstream engines than staying in the JS, and // charCodeAt on ASCII strings is usually optimised to raw bytes. - let encode_as_ascii = "\ - let len = arg.length; - let ptr = wasm.__wbindgen_malloc(len); + let encode_as_ascii = format!( + "\ + let len = arg.length; + let ptr = malloc(len); - const mem = getUint8Memory(); + const mem = {}(); - let offset = 0; + let offset = 0; - for (; offset < len; offset++) { - const code = arg.charCodeAt(offset); - if (code > 0x7F) break; - mem[ptr + offset] = code; - } - "; + for (; offset < len; offset++) {{ + const code = arg.charCodeAt(offset); + if (code > 0x7F) break; + mem[ptr + offset] = code; + }} + ", + mem + ); // TODO: // When converting a JS string to UTF-8, the maximum size is `arg.length * 3`, @@ -984,134 +997,146 @@ impl<'a> Context<'a> { // looping over the string to calculate the precise size, or perhaps using // `shrink_to_fit` on the Rust side. self.global(&format!( - "function passStringToWasm(arg) {{ - {} - {} + "function {name}(arg, malloc) {{ + {debug} + {ascii} if (offset !== len) {{ if (offset !== 0) {{ arg = arg.slice(offset); }} ptr = wasm.__wbindgen_realloc(ptr, len, len = offset + arg.length * 3); - const view = getUint8Memory().subarray(ptr + offset, ptr + len); + const view = {mem}().subarray(ptr + offset, ptr + len); const ret = encodeString(arg, view); - {} + {debug_end} offset += ret.written; }} WASM_VECTOR_LEN = offset; return ptr; }}", - debug, - encode_as_ascii, - if self.config.debug { + name = ret, + debug = debug, + ascii = encode_as_ascii, + mem = mem, + debug_end = if self.config.debug { "if (ret.read !== arg.length) throw new Error('failed to pass whole string');" } else { "" }, )); - Ok(()) + Ok(ret) } - fn expose_pass_array8_to_wasm(&mut self) -> Result<(), Error> { - self.expose_uint8_memory(); - self.pass_array_to_wasm("passArray8ToWasm", "getUint8Memory", 1) + fn expose_pass_array8_to_wasm(&mut self, memory: MemoryId) -> Result { + let view = self.expose_uint8_memory(memory); + self.pass_array_to_wasm("passArray8ToWasm", view, 1) } - fn expose_pass_array16_to_wasm(&mut self) -> Result<(), Error> { - self.expose_uint16_memory(); - self.pass_array_to_wasm("passArray16ToWasm", "getUint16Memory", 2) + fn expose_pass_array16_to_wasm(&mut self, memory: MemoryId) -> Result { + let view = self.expose_uint16_memory(memory); + self.pass_array_to_wasm("passArray16ToWasm", view, 2) } - fn expose_pass_array32_to_wasm(&mut self) -> Result<(), Error> { - self.expose_uint32_memory(); - self.pass_array_to_wasm("passArray32ToWasm", "getUint32Memory", 4) + fn expose_pass_array32_to_wasm(&mut self, memory: MemoryId) -> Result { + let view = self.expose_uint32_memory(memory); + self.pass_array_to_wasm("passArray32ToWasm", view, 4) } - fn expose_pass_array64_to_wasm(&mut self) -> Result<(), Error> { - self.expose_uint64_memory(); - self.pass_array_to_wasm("passArray64ToWasm", "getUint64Memory", 8) + fn expose_pass_array64_to_wasm(&mut self, memory: MemoryId) -> Result { + let view = self.expose_uint64_memory(memory); + self.pass_array_to_wasm("passArray64ToWasm", view, 8) } - fn expose_pass_array_f32_to_wasm(&mut self) -> Result<(), Error> { - self.expose_f32_memory(); - self.pass_array_to_wasm("passArrayF32ToWasm", "getFloat32Memory", 4) + fn expose_pass_array_f32_to_wasm(&mut self, memory: MemoryId) -> Result { + let view = self.expose_f32_memory(memory); + self.pass_array_to_wasm("passArrayF32ToWasm", view, 4) } - fn expose_pass_array_f64_to_wasm(&mut self) -> Result<(), Error> { - self.expose_f64_memory(); - self.pass_array_to_wasm("passArrayF64ToWasm", "getFloat64Memory", 8) + fn expose_pass_array_f64_to_wasm(&mut self, memory: MemoryId) -> Result { + let view = self.expose_f64_memory(memory); + self.pass_array_to_wasm("passArrayF64ToWasm", view, 8) } - fn expose_pass_array_jsvalue_to_wasm(&mut self) -> Result<(), Error> { - if !self.should_write_global("pass_array_jsvalue") { - return Ok(()); + fn expose_pass_array_jsvalue_to_wasm(&mut self, memory: MemoryId) -> Result { + let mem = self.expose_uint32_memory(memory); + let ret = MemView { + name: "passArrayJsValueToWasm", + num: mem.num, + }; + if !self.should_write_global(ret.to_string()) { + return Ok(ret); } - self.require_internal_export("__wbindgen_malloc")?; - self.expose_uint32_memory(); self.expose_wasm_vector_len(); - if self.config.anyref { - // TODO: using `addToAnyrefTable` goes back and forth between wasm - // and JS a lot, we should have a bulk operation for this. - self.expose_add_to_anyref_table()?; - self.global( - " - function passArrayJsValueToWasm(array) { - const ptr = wasm.__wbindgen_malloc(array.length * 4); - const mem = getUint32Memory(); - for (let i = 0; i < array.length; i++) { - mem[ptr / 4 + i] = addToAnyrefTable(array[i]); - } - WASM_VECTOR_LEN = array.length; - return ptr; - } - ", - ); - } else { - self.expose_add_heap_object(); - self.global( - " - function passArrayJsValueToWasm(array) { - const ptr = wasm.__wbindgen_malloc(array.length * 4); - const mem = getUint32Memory(); - for (let i = 0; i < array.length; i++) { - mem[ptr / 4 + i] = addHeapObject(array[i]); - } - WASM_VECTOR_LEN = array.length; - return ptr; - } - - ", - ); + match (self.aux.anyref_table, self.aux.anyref_alloc) { + (Some(table), Some(alloc)) => { + // TODO: using `addToAnyrefTable` goes back and forth between wasm + // and JS a lot, we should have a bulk operation for this. + let add = self.expose_add_to_anyref_table(table, alloc)?; + self.global(&format!( + " + function {}(array, malloc) {{ + const ptr = malloc(array.length * 4); + const mem = {}(); + for (let i = 0; i < array.length; i++) {{ + mem[ptr / 4 + i] = {}(array[i]); + }} + WASM_VECTOR_LEN = array.length; + return ptr; + }} + ", + ret, mem, add, + )); + } + _ => { + self.expose_add_heap_object(); + self.global(&format!( + " + function {}(array, malloc) {{ + const ptr = malloc(array.length * 4); + const mem = {}(); + for (let i = 0; i < array.length; i++) {{ + mem[ptr / 4 + i] = addHeapObject(array[i]); + }} + WASM_VECTOR_LEN = array.length; + return ptr; + }} + ", + ret, mem, + )); + } } - Ok(()) + Ok(ret) } fn pass_array_to_wasm( &mut self, name: &'static str, - delegate: &str, + view: MemView, size: usize, - ) -> Result<(), Error> { - if !self.should_write_global(name) { - return Ok(()); + ) -> Result { + let ret = MemView { + name, + num: view.num, + }; + if !self.should_write_global(ret.to_string()) { + return Ok(ret); } - self.require_internal_export("__wbindgen_malloc")?; self.expose_wasm_vector_len(); self.global(&format!( " - function {}(arg) {{ - const ptr = wasm.__wbindgen_malloc(arg.length * {size}); + function {}(arg, malloc) {{ + const ptr = malloc(arg.length * {size}); {}().set(arg, ptr / {size}); WASM_VECTOR_LEN = arg.length; return ptr; }} ", - name, - delegate, + ret, + view, size = size )); - Ok(()) + Ok(ret) } fn expose_text_encoder(&mut self) -> Result<(), Error> { @@ -1163,12 +1188,17 @@ impl<'a> Context<'a> { Ok(()) } - fn expose_get_string_from_wasm(&mut self) -> Result<(), Error> { - if !self.should_write_global("get_string_from_wasm") { - return Ok(()); - } + fn expose_get_string_from_wasm(&mut self, memory: MemoryId) -> Result { self.expose_text_decoder()?; - self.expose_uint8_memory(); + let mem = self.expose_uint8_memory(memory); + let ret = MemView { + name: "getStringFromWasm", + num: mem.num, + }; + + if !self.should_write_global(ret.to_string()) { + return Ok(ret); + } // Typically we try to give a raw view of memory out to `TextDecoder` to // avoid copying too much data. If, however, a `SharedArrayBuffer` is @@ -1178,27 +1208,31 @@ impl<'a> Context<'a> { // creates just a view. That way in shared mode we copy more data but in // non-shared mode there's no need to copy the data except for the // string itself. - let is_shared = self.module.memories.get(self.memory).shared; + let is_shared = self.module.memories.get(memory).shared; let method = if is_shared { "slice" } else { "subarray" }; self.global(&format!( " - function getStringFromWasm(ptr, len) {{ - return cachedTextDecoder.decode(getUint8Memory().{}(ptr, ptr + len)); + function {}(ptr, len) {{ + return cachedTextDecoder.decode({}().{}(ptr, ptr + len)); }} - ", - method + ", + ret, mem, method )); - Ok(()) + Ok(ret) } - fn expose_get_cached_string_from_wasm(&mut self) -> Result<(), Error> { - if !self.should_write_global("get_cached_string_from_wasm") { - return Ok(()); - } - + fn expose_get_cached_string_from_wasm(&mut self, memory: MemoryId) -> Result { self.expose_get_object(); - self.expose_get_string_from_wasm()?; + let get = self.expose_get_string_from_wasm(memory)?; + let ret = MemView { + name: "getCachedStringFromWasm", + num: get.num, + }; + + if !self.should_write_global(ret.to_string()) { + return Ok(ret); + } // This has support for both `&str` and `Option<&str>`. // @@ -1208,118 +1242,133 @@ impl<'a> Context<'a> { // // If `ptr` and `len` are both `0` then that means it's `None`, in that case we rely upon // the fact that `getObject(0)` is guaranteed to be `undefined`. - self.global( + self.global(&format!( " - function getCachedStringFromWasm(ptr, len) { - if (ptr === 0) { + function {}(ptr, len) {{ + if (ptr === 0) {{ return getObject(len); - } else { - return getStringFromWasm(ptr, len); - } + }} else {{ + return {}(ptr, len); + }} + }} + ", + ret, get, + )); + Ok(ret) + } + + fn expose_get_array_js_value_from_wasm(&mut self, memory: MemoryId) -> Result { + let mem = self.expose_uint32_memory(memory); + let ret = MemView { + name: "getArrayJsValueFromWasm", + num: mem.num, + }; + if !self.should_write_global(ret.to_string()) { + return Ok(ret); + } + match (self.aux.anyref_table, self.aux.anyref_drop_slice) { + (Some(table), Some(drop)) => { + let table = self.export_name_of(table); + let drop = self.export_name_of(drop); + self.global(&format!( + " + function {}(ptr, len) {{ + const mem = {}(); + const slice = mem.subarray(ptr / 4, ptr / 4 + len); + const result = []; + for (let i = 0; i < slice.length; i++) {{ + result.push(wasm.{}.get(slice[i])); + }} + wasm.{}(ptr, len); + return result; + }} + ", + ret, mem, table, drop, + )); + } + _ => { + self.expose_take_object(); + self.global(&format!( + " + function {}(ptr, len) {{ + const mem = {}(); + const slice = mem.subarray(ptr / 4, ptr / 4 + len); + const result = []; + for (let i = 0; i < slice.length; i++) {{ + result.push(takeObject(slice[i])); + }} + return result; + }} + ", + ret, mem, + )); } - ", - ); - Ok(()) - } - - fn expose_get_array_js_value_from_wasm(&mut self) -> Result<(), Error> { - if !self.should_write_global("get_array_js_value_from_wasm") { - return Ok(()); } - self.expose_uint32_memory(); - if self.config.anyref { - self.global( - " - function getArrayJsValueFromWasm(ptr, len) { - const mem = getUint32Memory(); - const slice = mem.subarray(ptr / 4, ptr / 4 + len); - const result = []; - for (let i = 0; i < slice.length; i++) { - result.push(wasm.__wbg_anyref_table.get(slice[i])); - } - wasm.__wbindgen_drop_anyref_slice(ptr, len); - return result; - } - ", - ); - self.require_internal_export("__wbindgen_drop_anyref_slice")?; - } else { - self.expose_take_object(); - self.global( - " - function getArrayJsValueFromWasm(ptr, len) { - const mem = getUint32Memory(); - const slice = mem.subarray(ptr / 4, ptr / 4 + len); - const result = []; - for (let i = 0; i < slice.length; i++) { - result.push(takeObject(slice[i])); - } - return result; - } - ", - ); - } - Ok(()) + Ok(ret) } - fn expose_get_array_i8_from_wasm(&mut self) { - self.expose_int8_memory(); - self.arrayget("getArrayI8FromWasm", "getInt8Memory", 1); + fn expose_get_array_i8_from_wasm(&mut self, memory: MemoryId) -> MemView { + let view = self.expose_int8_memory(memory); + self.arrayget("getArrayI8FromWasm", view, 1) } - fn expose_get_array_u8_from_wasm(&mut self) { - self.expose_uint8_memory(); - self.arrayget("getArrayU8FromWasm", "getUint8Memory", 1); + fn expose_get_array_u8_from_wasm(&mut self, memory: MemoryId) -> MemView { + let view = self.expose_uint8_memory(memory); + self.arrayget("getArrayU8FromWasm", view, 1) } - fn expose_get_clamped_array_u8_from_wasm(&mut self) { - self.expose_clamped_uint8_memory(); - self.arrayget("getClampedArrayU8FromWasm", "getUint8ClampedMemory", 1); + fn expose_get_clamped_array_u8_from_wasm(&mut self, memory: MemoryId) -> MemView { + let view = self.expose_clamped_uint8_memory(memory); + self.arrayget("getClampedArrayU8FromWasm", view, 1) } - fn expose_get_array_i16_from_wasm(&mut self) { - self.expose_int16_memory(); - self.arrayget("getArrayI16FromWasm", "getInt16Memory", 2); + fn expose_get_array_i16_from_wasm(&mut self, memory: MemoryId) -> MemView { + let view = self.expose_int16_memory(memory); + self.arrayget("getArrayI16FromWasm", view, 2) } - fn expose_get_array_u16_from_wasm(&mut self) { - self.expose_uint16_memory(); - self.arrayget("getArrayU16FromWasm", "getUint16Memory", 2); + fn expose_get_array_u16_from_wasm(&mut self, memory: MemoryId) -> MemView { + let view = self.expose_uint16_memory(memory); + self.arrayget("getArrayU16FromWasm", view, 2) } - fn expose_get_array_i32_from_wasm(&mut self) { - self.expose_int32_memory(); - self.arrayget("getArrayI32FromWasm", "getInt32Memory", 4); + fn expose_get_array_i32_from_wasm(&mut self, memory: MemoryId) -> MemView { + let view = self.expose_int32_memory(memory); + self.arrayget("getArrayI32FromWasm", view, 4) } - fn expose_get_array_u32_from_wasm(&mut self) { - self.expose_uint32_memory(); - self.arrayget("getArrayU32FromWasm", "getUint32Memory", 4); + fn expose_get_array_u32_from_wasm(&mut self, memory: MemoryId) -> MemView { + let view = self.expose_uint32_memory(memory); + self.arrayget("getArrayU32FromWasm", view, 4) } - fn expose_get_array_i64_from_wasm(&mut self) { - self.expose_int64_memory(); - self.arrayget("getArrayI64FromWasm", "getInt64Memory", 8); + fn expose_get_array_i64_from_wasm(&mut self, memory: MemoryId) -> MemView { + let view = self.expose_int64_memory(memory); + self.arrayget("getArrayI64FromWasm", view, 8) } - fn expose_get_array_u64_from_wasm(&mut self) { - self.expose_uint64_memory(); - self.arrayget("getArrayU64FromWasm", "getUint64Memory", 8); + fn expose_get_array_u64_from_wasm(&mut self, memory: MemoryId) -> MemView { + let view = self.expose_uint64_memory(memory); + self.arrayget("getArrayU64FromWasm", view, 8) } - fn expose_get_array_f32_from_wasm(&mut self) { - self.expose_f32_memory(); - self.arrayget("getArrayF32FromWasm", "getFloat32Memory", 4); + fn expose_get_array_f32_from_wasm(&mut self, memory: MemoryId) -> MemView { + let view = self.expose_f32_memory(memory); + self.arrayget("getArrayF32FromWasm", view, 4) } - fn expose_get_array_f64_from_wasm(&mut self) { - self.expose_f64_memory(); - self.arrayget("getArrayF64FromWasm", "getFloat64Memory", 8); + fn expose_get_array_f64_from_wasm(&mut self, memory: MemoryId) -> MemView { + let view = self.expose_f64_memory(memory); + self.arrayget("getArrayF64FromWasm", view, 8) } - fn arrayget(&mut self, name: &'static str, mem: &'static str, size: usize) { + fn arrayget(&mut self, name: &'static str, view: MemView, size: usize) -> MemView { + let ret = MemView { + name, + num: view.num, + }; if !self.should_write_global(name) { - return; + return ret; } self.global(&format!( " @@ -1327,136 +1376,112 @@ impl<'a> Context<'a> { return {mem}().subarray(ptr / {size}, ptr / {size} + len); }} ", - name = name, - mem = mem, + name = ret, + mem = view, size = size, )); + return ret; } - fn expose_node_buffer_memory(&mut self) { - self.memview("getNodeBufferMemory", "Buffer.from"); + fn expose_node_buffer_memory(&mut self, memory: MemoryId) -> MemView { + self.memview("getNodeBufferMemory", "Buffer.from", memory) } - fn expose_int8_memory(&mut self) { - self.memview("getInt8Memory", "new Int8Array"); + fn expose_int8_memory(&mut self, memory: MemoryId) -> MemView { + self.memview("getInt8Memory", "new Int8Array", memory) } - fn expose_uint8_memory(&mut self) { - self.memview("getUint8Memory", "new Uint8Array"); + fn expose_uint8_memory(&mut self, memory: MemoryId) -> MemView { + self.memview("getUint8Memory", "new Uint8Array", memory) } - fn expose_clamped_uint8_memory(&mut self) { - self.memview("getUint8ClampedMemory", "new Uint8ClampedArray"); + fn expose_clamped_uint8_memory(&mut self, memory: MemoryId) -> MemView { + self.memview("getUint8ClampedMemory", "new Uint8ClampedArray", memory) } - fn expose_int16_memory(&mut self) { - self.memview("getInt16Memory", "new Int16Array"); + fn expose_int16_memory(&mut self, memory: MemoryId) -> MemView { + self.memview("getInt16Memory", "new Int16Array", memory) } - fn expose_uint16_memory(&mut self) { - self.memview("getUint16Memory", "new Uint16Array"); + fn expose_uint16_memory(&mut self, memory: MemoryId) -> MemView { + self.memview("getUint16Memory", "new Uint16Array", memory) } - fn expose_int32_memory(&mut self) { - self.memview("getInt32Memory", "new Int32Array"); + fn expose_int32_memory(&mut self, memory: MemoryId) -> MemView { + self.memview("getInt32Memory", "new Int32Array", memory) } - fn expose_uint32_memory(&mut self) { - self.memview("getUint32Memory", "new Uint32Array"); + fn expose_uint32_memory(&mut self, memory: MemoryId) -> MemView { + self.memview("getUint32Memory", "new Uint32Array", memory) } - fn expose_int64_memory(&mut self) { - self.memview("getInt64Memory", "new BigInt64Array"); + fn expose_int64_memory(&mut self, memory: MemoryId) -> MemView { + self.memview("getInt64Memory", "new BigInt64Array", memory) } - fn expose_uint64_memory(&mut self) { - self.memview("getUint64Memory", "new BigUint64Array"); + fn expose_uint64_memory(&mut self, memory: MemoryId) -> MemView { + self.memview("getUint64Memory", "new BigUint64Array", memory) } - fn expose_f32_memory(&mut self) { - self.memview("getFloat32Memory", "new Float32Array"); + fn expose_f32_memory(&mut self, memory: MemoryId) -> MemView { + self.memview("getFloat32Memory", "new Float32Array", memory) } - fn expose_f64_memory(&mut self) { - self.memview("getFloat64Memory", "new Float64Array"); + fn expose_f64_memory(&mut self, memory: MemoryId) -> MemView { + self.memview("getFloat64Memory", "new Float64Array", memory) } - fn memview_function(&mut self, t: VectorKind) -> &'static str { + fn memview_function(&mut self, t: VectorKind, memory: MemoryId) -> MemView { match t { - VectorKind::String => { - self.expose_uint8_memory(); - "getUint8Memory" - } - VectorKind::I8 => { - self.expose_int8_memory(); - "getInt8Memory" - } - VectorKind::U8 => { - self.expose_uint8_memory(); - "getUint8Memory" - } - VectorKind::ClampedU8 => { - self.expose_clamped_uint8_memory(); - "getUint8ClampedMemory" - } - VectorKind::I16 => { - self.expose_int16_memory(); - "getInt16Memory" - } - VectorKind::U16 => { - self.expose_uint16_memory(); - "getUint16Memory" - } - VectorKind::I32 => { - self.expose_int32_memory(); - "getInt32Memory" - } - VectorKind::U32 => { - self.expose_uint32_memory(); - "getUint32Memory" - } - VectorKind::I64 => { - self.expose_int64_memory(); - "getInt64Memory" - } - VectorKind::U64 => { - self.expose_uint64_memory(); - "getUint64Memory" - } - VectorKind::F32 => { - self.expose_f32_memory(); - "getFloat32Memory" - } - VectorKind::F64 => { - self.expose_f64_memory(); - "getFloat64Memory" - } - VectorKind::Anyref => { - self.expose_uint32_memory(); - "getUint32Memory" - } + VectorKind::String => self.expose_uint8_memory(memory), + VectorKind::I8 => self.expose_int8_memory(memory), + VectorKind::U8 => self.expose_uint8_memory(memory), + VectorKind::ClampedU8 => self.expose_clamped_uint8_memory(memory), + VectorKind::I16 => self.expose_int16_memory(memory), + VectorKind::U16 => self.expose_uint16_memory(memory), + VectorKind::I32 => self.expose_int32_memory(memory), + VectorKind::U32 => self.expose_uint32_memory(memory), + VectorKind::I64 => self.expose_int64_memory(memory), + VectorKind::U64 => self.expose_uint64_memory(memory), + VectorKind::F32 => self.expose_f32_memory(memory), + VectorKind::F64 => self.expose_f64_memory(memory), + VectorKind::Anyref => self.expose_uint32_memory(memory), } } - fn memview(&mut self, name: &'static str, js: &str) { - if !self.should_write_global(name) { - return; + fn memview(&mut self, name: &'static str, js: &str, memory: walrus::MemoryId) -> MemView { + let view = self.memview_memory(name, memory); + if !self.should_write_global(name.to_string()) { + return view; } - let mem = self.memory(); + let mem = self.export_name_of(memory); self.global(&format!( " let cache{name} = null; function {name}() {{ - if (cache{name} === null || cache{name}.buffer !== {mem}.buffer) {{ - cache{name} = {js}({mem}.buffer); + if (cache{name} === null || cache{name}.buffer !== wasm.{mem}.buffer) {{ + cache{name} = {js}(wasm.{mem}.buffer); }} return cache{name}; }} ", - name = name, + name = view, js = js, mem = mem, )); + return view; + } + + fn memview_memory(&mut self, name: &'static str, memory: walrus::MemoryId) -> MemView { + let next = self.memory_indices.len(); + let num = *self.memory_indices.entry(memory).or_insert(next); + MemView { name, num } + } + + fn memview_table(&mut self, name: &'static str, table: walrus::TableId) -> MemView { + let next = self.table_indices.len(); + let num = *self.table_indices.entry(table).or_insert(next); + MemView { name, num } } fn expose_assert_class(&mut self) { @@ -1561,25 +1586,29 @@ impl<'a> Context<'a> { return Ok(()); } self.require_internal_export("__wbindgen_exn_store")?; - if self.config.anyref { - self.expose_add_to_anyref_table()?; - self.global( - " - function handleError(e) { - const idx = addToAnyrefTable(e); - wasm.__wbindgen_exn_store(idx); - } - ", - ); - } else { - self.expose_add_heap_object(); - self.global( - " - function handleError(e) { - wasm.__wbindgen_exn_store(addHeapObject(e)); - } - ", - ); + match (self.aux.anyref_table, self.aux.anyref_alloc) { + (Some(table), Some(alloc)) => { + let add = self.expose_add_to_anyref_table(table, alloc)?; + self.global(&format!( + " + function handleError(e) {{ + const idx = {}(e); + wasm.__wbindgen_exn_store(idx); + }} + ", + add, + )); + } + _ => { + self.expose_add_heap_object(); + self.global( + " + function handleError(e) { + wasm.__wbindgen_exn_store(addHeapObject(e)); + } + ", + ); + } } Ok(()) } @@ -1609,98 +1638,40 @@ impl<'a> Context<'a> { ); } - fn pass_to_wasm_function(&mut self, t: VectorKind) -> Result<&'static str, Error> { - let s = match t { - VectorKind::String => { - self.expose_pass_string_to_wasm()?; - "passStringToWasm" - } + fn pass_to_wasm_function(&mut self, t: VectorKind, memory: MemoryId) -> Result { + match t { + VectorKind::String => self.expose_pass_string_to_wasm(memory), VectorKind::I8 | VectorKind::U8 | VectorKind::ClampedU8 => { - self.expose_pass_array8_to_wasm()?; - "passArray8ToWasm" + self.expose_pass_array8_to_wasm(memory) } - VectorKind::U16 | VectorKind::I16 => { - self.expose_pass_array16_to_wasm()?; - "passArray16ToWasm" - } - VectorKind::I32 | VectorKind::U32 => { - self.expose_pass_array32_to_wasm()?; - "passArray32ToWasm" - } - VectorKind::I64 | VectorKind::U64 => { - self.expose_pass_array64_to_wasm()?; - "passArray64ToWasm" - } - VectorKind::F32 => { - self.expose_pass_array_f32_to_wasm()?; - "passArrayF32ToWasm" - } - VectorKind::F64 => { - self.expose_pass_array_f64_to_wasm()?; - "passArrayF64ToWasm" - } - VectorKind::Anyref => { - self.expose_pass_array_jsvalue_to_wasm()?; - "passArrayJsValueToWasm" - } - }; - Ok(s) + VectorKind::U16 | VectorKind::I16 => self.expose_pass_array16_to_wasm(memory), + VectorKind::I32 | VectorKind::U32 => self.expose_pass_array32_to_wasm(memory), + VectorKind::I64 | VectorKind::U64 => self.expose_pass_array64_to_wasm(memory), + VectorKind::F32 => self.expose_pass_array_f32_to_wasm(memory), + VectorKind::F64 => self.expose_pass_array_f64_to_wasm(memory), + VectorKind::Anyref => self.expose_pass_array_jsvalue_to_wasm(memory), + } } - fn expose_get_vector_from_wasm(&mut self, ty: VectorKind) -> Result<&'static str, Error> { + fn expose_get_vector_from_wasm( + &mut self, + ty: VectorKind, + memory: MemoryId, + ) -> Result { Ok(match ty { - VectorKind::String => { - self.expose_get_string_from_wasm()?; - "getStringFromWasm" - } - VectorKind::I8 => { - self.expose_get_array_i8_from_wasm(); - "getArrayI8FromWasm" - } - VectorKind::U8 => { - self.expose_get_array_u8_from_wasm(); - "getArrayU8FromWasm" - } - VectorKind::ClampedU8 => { - self.expose_get_clamped_array_u8_from_wasm(); - "getClampedArrayU8FromWasm" - } - VectorKind::I16 => { - self.expose_get_array_i16_from_wasm(); - "getArrayI16FromWasm" - } - VectorKind::U16 => { - self.expose_get_array_u16_from_wasm(); - "getArrayU16FromWasm" - } - VectorKind::I32 => { - self.expose_get_array_i32_from_wasm(); - "getArrayI32FromWasm" - } - VectorKind::U32 => { - self.expose_get_array_u32_from_wasm(); - "getArrayU32FromWasm" - } - VectorKind::I64 => { - self.expose_get_array_i64_from_wasm(); - "getArrayI64FromWasm" - } - VectorKind::U64 => { - self.expose_get_array_u64_from_wasm(); - "getArrayU64FromWasm" - } - VectorKind::F32 => { - self.expose_get_array_f32_from_wasm(); - "getArrayF32FromWasm" - } - VectorKind::F64 => { - self.expose_get_array_f64_from_wasm(); - "getArrayF64FromWasm" - } - VectorKind::Anyref => { - self.expose_get_array_js_value_from_wasm()?; - "getArrayJsValueFromWasm" - } + VectorKind::String => self.expose_get_string_from_wasm(memory)?, + VectorKind::I8 => self.expose_get_array_i8_from_wasm(memory), + VectorKind::U8 => self.expose_get_array_u8_from_wasm(memory), + VectorKind::ClampedU8 => self.expose_get_clamped_array_u8_from_wasm(memory), + VectorKind::I16 => self.expose_get_array_i16_from_wasm(memory), + VectorKind::U16 => self.expose_get_array_u16_from_wasm(memory), + VectorKind::I32 => self.expose_get_array_i32_from_wasm(memory), + VectorKind::U32 => self.expose_get_array_u32_from_wasm(memory), + VectorKind::I64 => self.expose_get_array_i64_from_wasm(memory), + VectorKind::U64 => self.expose_get_array_u64_from_wasm(memory), + VectorKind::F32 => self.expose_get_array_f32_from_wasm(memory), + VectorKind::F64 => self.expose_get_array_f64_from_wasm(memory), + VectorKind::Anyref => self.expose_get_array_js_value_from_wasm(memory)?, }) } @@ -1791,14 +1762,6 @@ impl<'a> Context<'a> { self.globals.push_str("\n"); } - fn memory(&mut self) -> &'static str { - if self.module.memories.get(self.memory).import.is_some() { - "memory" - } else { - "wasm.memory" - } - } - fn require_class_wrap(&mut self, name: &str) { require_class(&mut self.exported_classes, name).wrap_needed = true; } @@ -1909,235 +1872,189 @@ impl<'a> Context<'a> { true } - fn expose_add_to_anyref_table(&mut self) -> Result<(), Error> { + fn expose_add_to_anyref_table( + &mut self, + table: TableId, + alloc: FunctionId, + ) -> Result { + let view = self.memview_table("addToAnyrefTable", table); assert!(self.config.anyref); - if !self.should_write_global("add_to_anyref_table") { - return Ok(()); + if !self.should_write_global(view.to_string()) { + return Ok(view); } - self.require_internal_export("__wbindgen_anyref_table_alloc")?; - self.global( + let alloc = self.export_name_of(alloc); + let table = self.export_name_of(table); + self.global(&format!( " - function addToAnyrefTable(obj) { - const idx = wasm.__wbindgen_anyref_table_alloc(); - wasm.__wbg_anyref_table.set(idx, obj); + function {}(obj) {{ + const idx = wasm.{}(); + wasm.{}.set(idx, obj); return idx; - } + }} ", - ); + view, alloc, table, + )); - Ok(()) + Ok(view) } - pub fn generate( - &mut self, - aux: &WasmBindgenAux, - bindings: &NonstandardWebidlSection, - ) -> Result<(), Error> { - for (i, (idx, binding)) in bindings.elems.iter().enumerate() { - self.generate_elem_binding(i, *idx, binding, bindings)?; + pub fn generate(&mut self) -> Result<(), Error> { + for (id, adapter) in sorted_iter(&self.wit.adapters) { + let instrs = match &adapter.kind { + AdapterKind::Import { .. } => continue, + AdapterKind::Local { instructions } => instructions, + }; + self.generate_adapter(*id, adapter, instrs)?; } - let mut pairs = aux.export_map.iter().collect::>(); - pairs.sort_by_key(|(k, _)| *k); - check_duplicated_getter_and_setter_names(&pairs)?; - for (id, export) in pairs { - self.generate_export(*id, export, bindings) - .with_context(|| { - format!( - "failed to generate bindings for Rust export `{}`", - export.debug_name, - ) - })?; - } + // let mut pairs = aux.export_map.iter().collect::>(); + // pairs.sort_by_key(|(k, _)| *k); + // check_duplicated_getter_and_setter_names(&pairs)?; - for (id, import) in sorted_iter(&aux.import_map) { - let variadic = aux.imports_with_variadic.contains(&id); - let catch = aux.imports_with_catch.contains(&id); - let assert_no_shim = aux.imports_with_assert_no_shim.contains(&id); - self.generate_import(*id, import, bindings, variadic, catch, assert_no_shim) - .with_context(|| { - format!("failed to generate bindings for import `{:?}`", import,) - })?; - } - for e in aux.enums.iter() { + for e in self.aux.enums.iter() { self.generate_enum(e)?; } - for s in aux.structs.iter() { + for s in self.aux.structs.iter() { self.generate_struct(s)?; } - self.typescript.push_str(&aux.extra_typescript); + self.typescript.push_str(&self.aux.extra_typescript); - for path in aux.package_jsons.iter() { + for path in self.aux.package_jsons.iter() { self.process_package_json(path)?; } Ok(()) } - /// Generates a wrapper function for each bound element of the function - /// table. These wrapper functions have the expected WebIDL signature we'd - /// like them to have. This currently isn't part of the WebIDL bindings - /// proposal, but the thinking is that it'd look something like this if - /// added. - /// - /// Note that this is just an internal function shim used by closures and - /// such, so we're not actually exporting anything here. - fn generate_elem_binding( + fn generate_adapter( &mut self, - idx: usize, - elem_idx: u32, - binding: &Binding, - bindings: &NonstandardWebidlSection, + id: AdapterId, + adapter: &Adapter, + instrs: &[InstructionData], ) -> Result<(), Error> { - let webidl = bindings - .types - .get::(binding.webidl_ty) - .unwrap(); - self.export_function_table()?; - let mut builder = binding::Builder::new(self); - builder.disable_log_error(true); - let js = builder.process(&binding, &webidl, true, &None, &mut |_, _, args| { - Ok(format!( - "wasm.__wbg_function_table.get({})({})", - elem_idx, - args.join(", ") - )) - })?; - self.globals - .push_str(&format!("function __wbg_elem_binding{}{}\n", idx, js)); - Ok(()) - } + enum Kind<'a> { + Export(&'a AuxExport), + Import(walrus::ImportId), + Adapter, + } - fn generate_export( - &mut self, - id: ExportId, - export: &AuxExport, - bindings: &NonstandardWebidlSection, - ) -> Result<(), Error> { - let wasm_name = self.module.exports.get(id).name.clone(); - let binding = &bindings.exports[&id]; - let webidl = bindings - .types - .get::(binding.webidl_ty) - .unwrap(); + let kind = match self.aux.export_map.get(&id) { + Some(export) => Kind::Export(export), + None => { + let core = self.wit.implements.iter().find(|pair| pair.1 == id); + match core { + Some((core, _)) => Kind::Import(*core), + None => Kind::Adapter, + } + } + }; + + let catch = self.aux.imports_with_catch.contains(&id); + if let Kind::Import(core) = kind { + if !catch && self.attempt_direct_import(core, instrs)? { + return Ok(()); + } + } // Construct a JS shim builder, and configure it based on the kind of // export that we're generating. let mut builder = binding::Builder::new(self); - builder.disable_log_error(true); - match &export.kind { - AuxExportKind::Function(_) => {} - AuxExportKind::StaticFunction { .. } => {} - AuxExportKind::Constructor(class) => builder.constructor(class), - AuxExportKind::Getter { .. } | AuxExportKind::Setter { .. } => builder.method(false), - AuxExportKind::Method { consumed, .. } => builder.method(*consumed), + builder.log_error(match kind { + Kind::Export(_) | Kind::Adapter => false, + Kind::Import(_) => builder.cx.config.debug, + }); + builder.catch(catch); + let mut arg_names = &None; + match kind { + Kind::Export(export) => { + arg_names = &export.arg_names; + match &export.kind { + AuxExportKind::Function(_) => {} + AuxExportKind::StaticFunction { .. } => {} + AuxExportKind::Constructor(class) => builder.constructor(class), + AuxExportKind::Getter { .. } | AuxExportKind::Setter { .. } => { + builder.method(false) + } + AuxExportKind::Method { consumed, .. } => builder.method(*consumed), + } + } + Kind::Import(_) => {} + Kind::Adapter => {} } // Process the `binding` and generate a bunch of JS/TypeScript/etc. - let js = builder.process( - &binding, - &webidl, - true, - &export.arg_names, - &mut |_, _, args| Ok(format!("wasm.{}({})", wasm_name, args.join(", "))), - )?; + let js = builder + .process(&adapter, instrs, arg_names) + .with_context(|| match kind { + Kind::Export(e) => format!("failed to generate bindings for `{}`", e.debug_name), + Kind::Import(i) => { + let i = builder.cx.module.imports.get(i); + format!( + "failed to generate bindings for import of `{}::{}`", + i.module, i.name + ) + } + Kind::Adapter => format!("failed to generates bindings for adapter"), + })?; let ts = builder.typescript_signature(); let js_doc = builder.js_doc_comments(); - let docs = format_doc_comments(&export.comments, Some(js_doc)); // Once we've got all the JS then put it in the right location depending // on what's being exported. - match &export.kind { - AuxExportKind::Function(name) => { - self.export(&name, &format!("function{}", js), Some(docs))?; - self.globals.push_str("\n"); - self.typescript.push_str("export function "); - self.typescript.push_str(&name); - self.typescript.push_str(&ts); - self.typescript.push_str(";\n"); - } - AuxExportKind::Constructor(class) => { - let exported = require_class(&mut self.exported_classes, class); - if exported.has_constructor { - bail!("found duplicate constructor for class `{}`", class); + match kind { + Kind::Export(export) => { + let docs = format_doc_comments(&export.comments, Some(js_doc)); + match &export.kind { + AuxExportKind::Function(name) => { + self.export(&name, &format!("function{}", js), Some(docs))?; + self.globals.push_str("\n"); + self.typescript.push_str("export function "); + self.typescript.push_str(&name); + self.typescript.push_str(&ts); + self.typescript.push_str(";\n"); + } + AuxExportKind::Constructor(class) => { + let exported = require_class(&mut self.exported_classes, class); + if exported.has_constructor { + bail!("found duplicate constructor for class `{}`", class); + } + exported.has_constructor = true; + exported.push(&docs, "constructor", "", &js, &ts); + } + AuxExportKind::Getter { class, field } => { + let ret_ty = builder.ts_ret.as_ref().unwrap().ty.clone(); + let exported = require_class(&mut self.exported_classes, class); + exported.push_getter(&docs, field, &js, &ret_ty); + } + AuxExportKind::Setter { class, field } => { + let arg_ty = builder.ts_args[0].ty.clone(); + let exported = require_class(&mut self.exported_classes, class); + exported.push_setter(&docs, field, &js, &arg_ty); + } + AuxExportKind::StaticFunction { class, name } => { + let exported = require_class(&mut self.exported_classes, class); + exported.push(&docs, name, "static ", &js, &ts); + } + AuxExportKind::Method { class, name, .. } => { + let exported = require_class(&mut self.exported_classes, class); + exported.push(&docs, name, "", &js, &ts); + } } - exported.has_constructor = true; - exported.push(&docs, "constructor", "", &js, &ts); } - AuxExportKind::Getter { class, field } => { - let ret_ty = builder.ts_ret.as_ref().unwrap().ty.clone(); - let exported = require_class(&mut self.exported_classes, class); - exported.push_getter(&docs, field, &js, &ret_ty); - } - AuxExportKind::Setter { class, field } => { - let arg_ty = builder.ts_args[0].ty.clone(); - let exported = require_class(&mut self.exported_classes, class); - exported.push_setter(&docs, field, &js, &arg_ty); - } - AuxExportKind::StaticFunction { class, name } => { - let exported = require_class(&mut self.exported_classes, class); - exported.push(&docs, name, "static ", &js, &ts); - } - AuxExportKind::Method { class, name, .. } => { - let exported = require_class(&mut self.exported_classes, class); - exported.push(&docs, name, "", &js, &ts); - } - } - Ok(()) - } - - fn generate_import( - &mut self, - id: ImportId, - import: &AuxImport, - bindings: &NonstandardWebidlSection, - variadic: bool, - catch: bool, - assert_no_shim: bool, - ) -> Result<(), Error> { - let binding = &bindings.imports[&id]; - let webidl = bindings - .types - .get::(binding.webidl_ty) - .unwrap(); - match import { - AuxImport::Value(AuxValue::Bare(js)) - if !variadic && !catch && self.import_does_not_require_glue(binding, webidl) => - { - self.direct_import(id, js) - } - _ => { - if assert_no_shim { - panic!( - "imported function was annotated with `#[wasm_bindgen(assert_no_shim)]` \ - but we need to generate a JS shim for it:\n\n\ - \timport = {:?}\n\n\ - \tbinding = {:?}\n\n\ - \twebidl = {:?}", - import, binding, webidl, - ); - } - - let disable_log_error = self.import_never_log_error(import); - let mut builder = binding::Builder::new(self); - builder.catch(catch)?; - builder.disable_log_error(disable_log_error); - let js = builder.process( - &binding, - &webidl, - false, - &None, - &mut |cx, prelude, args| { - cx.invoke_import(&binding, import, bindings, args, variadic, prelude) - }, - )?; + Kind::Import(core) => { self.wasm_import_definitions - .insert(id, format!("function{}", js)); - Ok(()) + .insert(core, format!("function{}", js)); + } + Kind::Adapter => { + self.globals.push_str("function "); + self.globals.push_str(&self.adapter_name(id)); + self.globals.push_str(&js); + self.globals.push_str("\n\n"); } } + return Ok(()); } /// Returns whether we should disable the logic, in debug mode, to catch an @@ -2156,34 +2073,72 @@ impl<'a> Context<'a> { } } - fn import_does_not_require_glue( - &self, - binding: &Binding, - webidl: &ast::WebidlFunction, - ) -> bool { - if !self.config.anyref && binding.contains_anyref(self.module) { - return false; + /// Attempts to directly hook up the `id` import in the wasm module with + /// the `instrs` specified. + /// + /// If this succeeds it returns `Ok(true)`, otherwise if it cannot be + /// directly imported then `Ok(false)` is returned. + fn attempt_direct_import( + &mut self, + id: ImportId, + instrs: &[InstructionData], + ) -> Result { + // First up extract the ID of the single called adapter, if any. + let mut call = None; + for instr in instrs { + match instr.instr { + Instruction::CallAdapter(id) => { + if call.is_some() { + return Ok(false); + } else { + call = Some(id); + } + } + Instruction::CallExport(_) + | Instruction::CallTableElement(_) + | Instruction::Standard(wit_walrus::Instruction::CallCore(_)) + | Instruction::Standard(wit_walrus::Instruction::CallAdapter(_)) => { + return Ok(false) + } + _ => {} + } + } + let adapter = match call { + Some(id) => id, + None => return Ok(false), + }; + match &self.wit.adapters[&adapter].kind { + AdapterKind::Import { kind, .. } => match kind { + AdapterJsImportKind::Normal => {} + // method/constructors need glue because we either need to + // invoke them as `new` or we need to invoke them with + // method-call syntax to get the `this` parameter right. + AdapterJsImportKind::Method | AdapterJsImportKind::Constructor => return Ok(false), + }, + // This is an adapter-to-adapter call, so it needs a shim. + AdapterKind::Local { .. } => return Ok(false), } - let wasm_ty = self.module.types.get(binding.wasm_ty); - webidl.kind == ast::WebidlFunctionKind::Static - && webidl::outgoing_do_not_require_glue( - &binding.outgoing, - wasm_ty.params(), - &webidl.params, - self.config.wasm_interface_types, - ) - && webidl::incoming_do_not_require_glue( - &binding.incoming, - &webidl.result.into_iter().collect::>(), - wasm_ty.results(), - self.config.wasm_interface_types, - ) - } + // Next up check to make sure that this import is to a bare JS value + // itself, no extra fluff intended. + let js = match &self.aux.import_map[&adapter] { + AuxImport::Value(AuxValue::Bare(js)) => js, + _ => return Ok(false), + }; + + // Make sure this isn't variadic in any way which means we need some + // sort of adapter glue. + if self.aux.imports_with_variadic.contains(&adapter) { + return Ok(false); + } + + // Ensure that every single instruction can be represented without JS + // glue being generated, aka it's covered by the JS ECMAScript bindings + // for wasm. + if !self.representable_without_js_glue(instrs) { + return Ok(false); + } - /// Emit a direct import directive that hooks up the `js` value specified to - /// the wasm import `id`. - fn direct_import(&mut self, id: ImportId, js: &JsImport) -> Result<(), Error> { // If there's no field projection happening here and this is a direct // import from an ES-looking module, then we can actually just hook this // up directly in the wasm file itself. Note that this is covered in the @@ -2203,14 +2158,14 @@ impl<'a> Context<'a> { let import = self.module.imports.get_mut(id); import.module = module.clone(); import.name = name.clone(); - return Ok(()); + return Ok(true); } JsImportName::LocalModule { module, name } => { let module = self.config.local_module_name(module); let import = self.module.imports.get_mut(id); import.module = module; import.name = name.clone(); - return Ok(()); + return Ok(true); } JsImportName::InlineJs { unique_crate_identifier, @@ -2223,7 +2178,7 @@ impl<'a> Context<'a> { let import = self.module.imports.get_mut(id); import.module = module; import.name = name.clone(); - return Ok(()); + return Ok(true); } // Fall through below to requiring a JS shim to create an item @@ -2241,7 +2196,61 @@ impl<'a> Context<'a> { name = name, ); self.wasm_import_definitions.insert(id, js); - Ok(()) + Ok(true) + } + + fn representable_without_js_glue(&self, instrs: &[InstructionData]) -> bool { + use Instruction::*; + let standard_enabled = self.config.wasm_interface_types; + + let mut last_arg = None; + let mut saw_call = false; + for instr in instrs { + match instr.instr { + // Is an adapter section getting emitted? If so, then every + // standard operation is natively supported! + Standard(_) if standard_enabled => {} + + // Fetching arguments is just that, a fetch, so no need for + // glue. Note though that the arguments must be fetched in order + // for this to actually work, + Standard(wit_walrus::Instruction::ArgGet(i)) => { + if saw_call { + return false; + } + match (i, last_arg) { + (0, None) => last_arg = Some(0), + (n, Some(i)) if n == i + 1 => last_arg = Some(n), + _ => return false, + } + } + + // Similarly calling a function is the same as in JS, no glue + // needed. + CallAdapter(_) => saw_call = true, + + // Conversions to wasm integers are always supported since + // they're coerced into i32/f32/f64 appropriately. + Standard(wit_walrus::Instruction::IntToWasm { .. }) => {} + + // Converts from wasm to JS, however, only supports most + // integers. Converting into a u32 isn't supported because we + // need to generate glue to change the sign. + Standard(wit_walrus::Instruction::WasmToInt { + output: wit_walrus::ValType::U32, + .. + }) => return false, + Standard(wit_walrus::Instruction::WasmToInt { .. }) => {} + + // JS spec automatically coerces boolean values to i32 of 0 or 1 + // depending on true/false + I32FromBool => {} + + _ => return false, + } + } + + return true; } /// Generates a JS snippet appropriate for invoking `import`. @@ -2256,14 +2265,12 @@ impl<'a> Context<'a> { /// purpose of `AuxImport`, which depends on the kind of import. fn invoke_import( &mut self, - binding: &Binding, import: &AuxImport, - bindings: &NonstandardWebidlSection, + kind: AdapterJsImportKind, args: &[String], variadic: bool, prelude: &mut String, ) -> Result { - let webidl_ty: &ast::WebidlFunction = bindings.types.get(binding.webidl_ty).unwrap(); let variadic_args = |js_arguments: &[String]| { Ok(if !variadic { format!("{}", js_arguments.join(", ")) @@ -2280,15 +2287,15 @@ impl<'a> Context<'a> { }) }; match import { - AuxImport::Value(val) => match webidl_ty.kind { - ast::WebidlFunctionKind::Constructor => { + AuxImport::Value(val) => match kind { + AdapterJsImportKind::Constructor => { let js = match val { AuxValue::Bare(js) => self.import_name(js)?, _ => bail!("invalid import set for constructor"), }; Ok(format!("new {}({})", js, variadic_args(&args)?)) } - ast::WebidlFunctionKind::Method(_) => { + AdapterJsImportKind::Method => { let descriptor = |anchor: &str, extra: &str, field: &str, which: &str| { format!( "GetOwnOrInheritedPropertyDescriptor({}{}, '{}').{}", @@ -2320,7 +2327,7 @@ impl<'a> Context<'a> { }; Ok(format!("{}.call({})", js, variadic_args(&args)?)) } - ast::WebidlFunctionKind::Static => { + AdapterJsImportKind::Normal => { let js = match val { AuxValue::Bare(js) => self.import_name(js)?, _ => bail!("invalid import set for free function"), @@ -2335,7 +2342,7 @@ impl<'a> Context<'a> { } AuxImport::Instanceof(js) => { - assert!(webidl_ty.kind == ast::WebidlFunctionKind::Static); + assert!(kind == AdapterJsImportKind::Normal); assert!(!variadic); assert_eq!(args.len(), 1); let js = self.import_name(js)?; @@ -2343,7 +2350,7 @@ impl<'a> Context<'a> { } AuxImport::Static(js) => { - assert!(webidl_ty.kind == ast::WebidlFunctionKind::Static); + assert!(kind == AdapterJsImportKind::Normal); assert!(!variadic); assert_eq!(args.len(), 0); self.import_name(js) @@ -2352,10 +2359,10 @@ impl<'a> Context<'a> { AuxImport::Closure { dtor, mutable, - binding_idx, + adapter, nargs, } => { - assert!(webidl_ty.kind == ast::WebidlFunctionKind::Static); + assert!(kind == AdapterJsImportKind::Normal); assert!(!variadic); assert_eq!(args.len(), 3); let arg_names = (0..*nargs) @@ -2370,7 +2377,7 @@ impl<'a> Context<'a> { self.export_function_table()?; let dtor = format!("wasm.__wbg_function_table.get({})", dtor); - let call = format!("__wbg_elem_binding{}", binding_idx); + let call = self.adapter_name(*adapter); if *mutable { // For mutable closures they can't be invoked recursively. @@ -2423,7 +2430,7 @@ impl<'a> Context<'a> { } AuxImport::StructuralMethod(name) => { - assert!(webidl_ty.kind == ast::WebidlFunctionKind::Static); + assert!(kind == AdapterJsImportKind::Normal); let (receiver, args) = match args.split_first() { Some(pair) => pair, None => bail!("structural method calls must have at least one argument"), @@ -2432,14 +2439,14 @@ impl<'a> Context<'a> { } AuxImport::StructuralGetter(field) => { - assert!(webidl_ty.kind == ast::WebidlFunctionKind::Static); + assert!(kind == AdapterJsImportKind::Normal); assert!(!variadic); assert_eq!(args.len(), 1); Ok(format!("{}.{}", args[0], field)) } AuxImport::StructuralClassGetter(class, field) => { - assert!(webidl_ty.kind == ast::WebidlFunctionKind::Static); + assert!(kind == AdapterJsImportKind::Normal); assert!(!variadic); assert_eq!(args.len(), 0); let class = self.import_name(class)?; @@ -2447,14 +2454,14 @@ impl<'a> Context<'a> { } AuxImport::StructuralSetter(field) => { - assert!(webidl_ty.kind == ast::WebidlFunctionKind::Static); + assert!(kind == AdapterJsImportKind::Normal); assert!(!variadic); assert_eq!(args.len(), 2); Ok(format!("{}.{} = {}", args[0], field, args[1])) } AuxImport::StructuralClassSetter(class, field) => { - assert!(webidl_ty.kind == ast::WebidlFunctionKind::Static); + assert!(kind == AdapterJsImportKind::Normal); assert!(!variadic); assert_eq!(args.len(), 1); let class = self.import_name(class)?; @@ -2462,7 +2469,7 @@ impl<'a> Context<'a> { } AuxImport::IndexingGetterOfClass(class) => { - assert!(webidl_ty.kind == ast::WebidlFunctionKind::Static); + assert!(kind == AdapterJsImportKind::Normal); assert!(!variadic); assert_eq!(args.len(), 1); let class = self.import_name(class)?; @@ -2470,14 +2477,14 @@ impl<'a> Context<'a> { } AuxImport::IndexingGetterOfObject => { - assert!(webidl_ty.kind == ast::WebidlFunctionKind::Static); + assert!(kind == AdapterJsImportKind::Normal); assert!(!variadic); assert_eq!(args.len(), 2); Ok(format!("{}[{}]", args[0], args[1])) } AuxImport::IndexingSetterOfClass(class) => { - assert!(webidl_ty.kind == ast::WebidlFunctionKind::Static); + assert!(kind == AdapterJsImportKind::Normal); assert!(!variadic); assert_eq!(args.len(), 2); let class = self.import_name(class)?; @@ -2485,14 +2492,14 @@ impl<'a> Context<'a> { } AuxImport::IndexingSetterOfObject => { - assert!(webidl_ty.kind == ast::WebidlFunctionKind::Static); + assert!(kind == AdapterJsImportKind::Normal); assert!(!variadic); assert_eq!(args.len(), 3); Ok(format!("{}[{}] = {}", args[0], args[1], args[2])) } AuxImport::IndexingDeleterOfClass(class) => { - assert!(webidl_ty.kind == ast::WebidlFunctionKind::Static); + assert!(kind == AdapterJsImportKind::Normal); assert!(!variadic); assert_eq!(args.len(), 1); let class = self.import_name(class)?; @@ -2500,14 +2507,14 @@ impl<'a> Context<'a> { } AuxImport::IndexingDeleterOfObject => { - assert!(webidl_ty.kind == ast::WebidlFunctionKind::Static); + assert!(kind == AdapterJsImportKind::Normal); assert!(!variadic); assert_eq!(args.len(), 2); Ok(format!("delete {}[{}]", args[0], args[1])) } AuxImport::WrapInExportedClass(class) => { - assert!(webidl_ty.kind == ast::WebidlFunctionKind::Static); + assert!(kind == AdapterJsImportKind::Normal); assert!(!variadic); assert_eq!(args.len(), 1); self.require_class_wrap(class); @@ -2515,7 +2522,7 @@ impl<'a> Context<'a> { } AuxImport::Intrinsic(intrinsic) => { - assert!(webidl_ty.kind == ast::WebidlFunctionKind::Static); + assert!(kind == AdapterJsImportKind::Normal); assert!(!variadic); self.invoke_intrinsic(intrinsic, args, prelude) } @@ -2618,26 +2625,15 @@ impl<'a> Context<'a> { } Intrinsic::NumberGet => { - assert_eq!(args.len(), 2); - self.expose_uint8_memory(); + assert_eq!(args.len(), 1); prelude.push_str(&format!("const obj = {};\n", args[0])); - prelude.push_str("if (typeof(obj) === 'number') return obj;\n"); - prelude.push_str(&format!("getUint8Memory()[{}] = 1;\n", args[1])); - "0".to_string() + format!("typeof(obj) === 'number' ? obj : undefined") } Intrinsic::StringGet => { - self.expose_pass_string_to_wasm()?; - self.expose_uint32_memory(); - assert_eq!(args.len(), 2); + assert_eq!(args.len(), 1); prelude.push_str(&format!("const obj = {};\n", args[0])); - prelude.push_str("if (typeof(obj) !== 'string') return 0;\n"); - prelude.push_str("const ptr = passStringToWasm(obj);\n"); - prelude.push_str(&format!( - "getUint32Memory()[{} / 4] = WASM_VECTOR_LEN;\n", - args[1], - )); - "ptr".to_string() + format!("typeof(obj) === 'string' ? obj : undefined") } Intrinsic::BooleanGet => { @@ -2669,7 +2665,19 @@ impl<'a> Context<'a> { Intrinsic::Memory => { assert_eq!(args.len(), 0); - self.memory().to_string() + let mut memories = self.module.memories.iter(); + let memory = memories + .next() + .ok_or_else(|| anyhow!("no memory found to return in memory intrinsic"))? + .id(); + if memories.next().is_some() { + bail!( + "multiple memories found, unsure which to return \ + from memory intrinsic" + ); + } + drop(memories); + format!("wasm.{}", self.export_name_of(memory)) } Intrinsic::FunctionTable => { @@ -2732,16 +2740,22 @@ impl<'a> Context<'a> { } Intrinsic::InitAnyrefTable => { + let table = self + .aux + .anyref_table + .ok_or_else(|| anyhow!("must enable anyref to use anyref intrinsic"))?; + let name = self.export_name_of(table); // Grow the table to insert our initial values, and then also // set the 0th slot to `undefined` since that's what we've // historically used for our ABI which is that the index of 0 // returns `undefined` for types like `None` going out. let mut base = format!( " - const table = wasm.__wbg_anyref_table; + const table = wasm.{}; const offset = table.grow({}); table.set(0, undefined); ", + name, INITIAL_HEAP_VALUES.len(), ); for (i, value) in INITIAL_HEAP_VALUES.iter().enumerate() { @@ -2929,6 +2943,35 @@ impl<'a> Context<'a> { self.module.exports.add("__wbg_function_table", id); Ok(()) } + + fn export_name_of(&mut self, id: impl Into) -> String { + let id = id.into(); + let export = self.module.exports.iter().find(|e| { + use walrus::ExportItem::*; + + match (e.item, id) { + (Function(a), Function(b)) => a == b, + (Table(a), Table(b)) => a == b, + (Memory(a), Memory(b)) => a == b, + (Global(a), Global(b)) => a == b, + _ => false, + } + }); + if let Some(export) = export { + self.required_internal_exports + .insert(export.name.clone().into()); + return export.name.clone(); + } + let name = format!("__wbindgen_export_{}", self.next_export_idx); + self.next_export_idx += 1; + self.module.exports.add(&name, id); + self.required_internal_exports.insert(name.clone().into()); + return name; + } + + fn adapter_name(&self, id: AdapterId) -> String { + format!("__wbg_adapter_{}", id.0) + } } fn check_duplicated_getter_and_setter_names( @@ -3095,3 +3138,14 @@ fn test_generate_identifier() { "default2".to_string() ); } + +struct MemView { + name: &'static str, + num: usize, +} + +impl fmt::Display for MemView { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}{}", self.name, self.num) + } +} diff --git a/crates/cli-support/src/js/outgoing.rs b/crates/cli-support/src/js/outgoing.rs deleted file mode 100644 index eceece77..00000000 --- a/crates/cli-support/src/js/outgoing.rs +++ /dev/null @@ -1,451 +0,0 @@ -//! Implementation of translating a `NonstandardOutgoing` expression to an -//! actual JS shim and code snippet which ensures that bindings behave as we'd -//! expect. - -use crate::descriptor::VectorKind; -use crate::js::binding::JsBuilder; -use crate::js::Context; -use crate::webidl::NonstandardOutgoing; -use anyhow::{bail, Error}; -use wasm_webidl_bindings::ast; - -pub struct Outgoing<'a, 'b> { - cx: &'a mut Context<'b>, - js: &'a mut JsBuilder, -} - -impl<'a, 'b> Outgoing<'a, 'b> { - pub fn new(cx: &'a mut Context<'b>, js: &'a mut JsBuilder) -> Outgoing<'a, 'b> { - Outgoing { cx, js } - } - - pub fn process(&mut self, outgoing: &NonstandardOutgoing) -> Result { - let before = self.js.typescript_len(); - let ret = self.nonstandard(outgoing)?; - assert_eq!(before + 1, self.js.typescript_len()); - Ok(ret) - } - - fn nonstandard(&mut self, outgoing: &NonstandardOutgoing) -> Result { - match outgoing { - NonstandardOutgoing::Standard(expr) => self.standard(expr), - - // Converts the wasm argument, a single code unit, to a string. - NonstandardOutgoing::Char { idx } => { - self.js.typescript_required("string"); - Ok(format!("String.fromCodePoint({})", self.arg(*idx))) - } - - // Just need to wrap up the pointer we get from Rust into a JS type - // and then we can pass that along - NonstandardOutgoing::RustType { class, idx } => { - self.js.typescript_required(class); - self.cx.require_class_wrap(class); - Ok(format!("{}.__wrap({})", class, self.arg(*idx))) - } - - // Just a small wrapper around `getObject` - NonstandardOutgoing::BorrowedAnyref { idx } => { - self.js.typescript_required("any"); - self.cx.expose_get_object(); - Ok(format!("getObject({})", self.arg(*idx))) - } - - // given the low/high bits we get from Rust, store them into a - // temporary 64-bit conversion array and then load the BigInt out of - // it. - NonstandardOutgoing::Number64 { - lo_idx, - hi_idx, - signed, - } => { - self.js.typescript_required("BigInt"); - let f = if *signed { - self.cx.expose_int64_cvt_shim() - } else { - self.cx.expose_uint64_cvt_shim() - }; - let i = self.js.tmp(); - self.js.prelude(&format!( - "\ - u32CvtShim[0] = {low}; - u32CvtShim[1] = {high}; - const n{i} = {f}[0]; - ", - low = self.arg(*lo_idx), - high = self.arg(*hi_idx), - f = f, - i = i, - )); - Ok(format!("n{}", i)) - } - - // Similar to `View` below, except using 64-bit types which don't - // fit into webidl scalar types right now. - NonstandardOutgoing::View64 { - offset, - length, - signed, - } => { - let ptr = self.arg(*offset); - let len = self.arg(*length); - let kind = if *signed { - VectorKind::I64 - } else { - VectorKind::U64 - }; - self.js.typescript_required(kind.js_ty()); - let f = self.cx.expose_get_vector_from_wasm(kind)?; - Ok(format!("{}({}, {})", f, ptr, len)) - } - - // Similar to `View` below, except using anyref types which have - // fancy conversion functions on our end. - NonstandardOutgoing::ViewAnyref { offset, length } => { - let ptr = self.arg(*offset); - let len = self.arg(*length); - self.js.typescript_required(VectorKind::Anyref.js_ty()); - let f = self.cx.expose_get_vector_from_wasm(VectorKind::Anyref)?; - Ok(format!("{}({}, {})", f, ptr, len)) - } - - // Similar to `View` below, except we free the memory in JS right - // now. - // - // TODO: we should free the memory in Rust to allow using standard - // webidl bindings. - NonstandardOutgoing::Vector { - offset, - length, - kind, - } => { - let ptr = self.arg(*offset); - let len = self.arg(*length); - self.js.typescript_required(kind.js_ty()); - let f = self.cx.expose_get_vector_from_wasm(*kind)?; - let i = self.js.tmp(); - self.js - .prelude(&format!("const v{} = {}({}, {}).slice();", i, f, ptr, len)); - self.prelude_free_vector(*offset, *length, *kind)?; - Ok(format!("v{}", i)) - } - - NonstandardOutgoing::CachedString { - offset, - length, - owned, - optional, - } => { - let ptr = self.arg(*offset); - let len = self.arg(*length); - let tmp = self.js.tmp(); - - if *optional { - self.js.typescript_optional("string"); - } else { - self.js.typescript_required("string"); - } - - self.cx.expose_get_cached_string_from_wasm()?; - - self.js.prelude(&format!( - "const v{} = getCachedStringFromWasm({}, {});", - tmp, ptr, len - )); - - if *owned { - self.prelude_free_cached_string(&ptr, &len)?; - } - - Ok(format!("v{}", tmp)) - } - - NonstandardOutgoing::StackClosure { - a, - b, - binding_idx, - nargs, - mutable, - } => { - self.js.typescript_optional("any"); - let i = self.js.tmp(); - self.js.prelude(&format!( - "const state{} = {{a: {}, b: {}}};", - i, - self.arg(*a), - self.arg(*b), - )); - let args = (0..*nargs) - .map(|i| format!("arg{}", i)) - .collect::>() - .join(", "); - if *mutable { - // Mutable closures need protection against being called - // recursively, so ensure that we clear out one of the - // internal pointers while it's being invoked. - self.js.prelude(&format!( - "const cb{i} = ({args}) => {{ - const a = state{i}.a; - state{i}.a = 0; - try {{ - return __wbg_elem_binding{idx}(a, state{i}.b, {args}); - }} finally {{ - state{i}.a = a; - }} - }};", - i = i, - args = args, - idx = binding_idx, - )); - } else { - self.js.prelude(&format!( - "const cb{i} = ({args}) => __wbg_elem_binding{idx}(state{i}.a, state{i}.b, {args});", - i = i, - args = args, - idx = binding_idx, - )); - } - - // Make sure to null out our internal pointers when we return - // back to Rust to ensure that any lingering references to the - // closure will fail immediately due to null pointers passed in - // to Rust. - self.js.finally(&format!("state{}.a = state{0}.b = 0;", i)); - Ok(format!("cb{}", i)) - } - - NonstandardOutgoing::OptionBool { idx } => { - self.js.typescript_optional("boolean"); - Ok(format!( - "{0} === 0xFFFFFF ? undefined : {0} !== 0", - self.arg(*idx) - )) - } - - NonstandardOutgoing::OptionChar { idx } => { - self.js.typescript_optional("string"); - Ok(format!( - "{0} === 0xFFFFFF ? undefined : String.fromCodePoint({0})", - self.arg(*idx) - )) - } - - NonstandardOutgoing::OptionIntegerEnum { idx, hole } => { - self.js.typescript_optional("number"); - Ok(format!( - "{0} === {1} ? undefined : {0}", - self.arg(*idx), - hole - )) - } - - NonstandardOutgoing::OptionRustType { class, idx } => { - self.cx.require_class_wrap(class); - self.js.typescript_optional(class); - Ok(format!( - "{0} === 0 ? undefined : {1}.__wrap({0})", - self.arg(*idx), - class, - )) - } - - NonstandardOutgoing::OptionU32Sentinel { idx } => { - self.js.typescript_optional("number"); - Ok(format!( - "{0} === 0xFFFFFF ? undefined : {0}", - self.arg(*idx) - )) - } - - NonstandardOutgoing::OptionNative { - signed, - present, - val, - } => { - self.js.typescript_optional("number"); - Ok(format!( - "{} === 0 ? undefined : {}{}", - self.arg(*present), - self.arg(*val), - if *signed { "" } else { " >>> 0" }, - )) - } - - NonstandardOutgoing::OptionInt64 { - present, - _ignored, - lo, - hi, - signed, - } => { - self.js.typescript_optional("BigInt"); - let f = if *signed { - self.cx.expose_int64_cvt_shim() - } else { - self.cx.expose_uint64_cvt_shim() - }; - let i = self.js.tmp(); - self.js.prelude(&format!( - " - u32CvtShim[0] = {low}; - u32CvtShim[1] = {high}; - const n{i} = {present} === 0 ? undefined : {f}[0]; - ", - present = self.arg(*present), - low = self.arg(*lo), - high = self.arg(*hi), - f = f, - i = i, - )); - Ok(format!("n{}", i)) - } - - NonstandardOutgoing::OptionSlice { - kind, - offset, - length, - } => { - let ptr = self.arg(*offset); - let len = self.arg(*length); - self.js.typescript_optional(kind.js_ty()); - let f = self.cx.expose_get_vector_from_wasm(*kind)?; - Ok(format!( - "{ptr} === 0 ? undefined : {f}({ptr}, {len})", - ptr = ptr, - len = len, - f = f - )) - } - - NonstandardOutgoing::OptionVector { - offset, - length, - kind, - } => { - let ptr = self.arg(*offset); - let len = self.arg(*length); - self.js.typescript_optional(kind.js_ty()); - let f = self.cx.expose_get_vector_from_wasm(*kind)?; - let i = self.js.tmp(); - self.js.prelude(&format!("let v{};", i)); - self.js.prelude(&format!("if ({} !== 0) {{", ptr)); - self.js - .prelude(&format!("v{} = {}({}, {}).slice();", i, f, ptr, len)); - self.prelude_free_vector(*offset, *length, *kind)?; - self.js.prelude("}"); - Ok(format!("v{}", i)) - } - } - } - - /// Evaluates the `standard` binding expression, returning the JS expression - /// needed to evaluate the binding. - fn standard(&mut self, standard: &ast::OutgoingBindingExpression) -> Result { - match standard { - ast::OutgoingBindingExpression::As(expr) => match expr.ty { - ast::WebidlTypeRef::Scalar(ast::WebidlScalarType::Any) => { - self.js.typescript_required("any"); - if self.cx.config.anyref { - Ok(self.arg(expr.idx)) - } else { - self.cx.expose_take_object(); - Ok(format!("takeObject({})", self.arg(expr.idx))) - } - } - ast::WebidlTypeRef::Scalar(ast::WebidlScalarType::Boolean) => { - self.js.typescript_required("boolean"); - Ok(format!("{} !== 0", self.arg(expr.idx))) - } - ast::WebidlTypeRef::Scalar(ast::WebidlScalarType::UnsignedLong) => { - self.js.typescript_required("number"); - Ok(format!("{} >>> 0", self.arg(expr.idx))) - } - _ => { - self.js.typescript_required("number"); - Ok(self.arg(expr.idx)) - } - }, - ast::OutgoingBindingExpression::View(view) => { - // TODO: deduplicate with same match statement in incoming - // bindings - let scalar = match view.ty { - ast::WebidlTypeRef::Scalar(s) => s, - ast::WebidlTypeRef::Id(_) => { - bail!("unsupported type passed to `view` in webidl binding") - } - }; - let kind = match scalar { - ast::WebidlScalarType::Int8Array => VectorKind::I8, - ast::WebidlScalarType::Uint8Array => VectorKind::U8, - ast::WebidlScalarType::Uint8ClampedArray => VectorKind::ClampedU8, - ast::WebidlScalarType::Int16Array => VectorKind::I16, - ast::WebidlScalarType::Uint16Array => VectorKind::U16, - ast::WebidlScalarType::Int32Array => VectorKind::I32, - ast::WebidlScalarType::Uint32Array => VectorKind::U32, - ast::WebidlScalarType::Float32Array => VectorKind::F32, - ast::WebidlScalarType::Float64Array => VectorKind::F64, - _ => bail!("unsupported type passed to `view`: {:?}", scalar), - }; - self.js.typescript_required(kind.js_ty()); - let ptr = self.arg(view.offset); - let len = self.arg(view.length); - let f = self.cx.expose_get_vector_from_wasm(kind)?; - Ok(format!("{}({}, {})", f, ptr, len)) - } - - ast::OutgoingBindingExpression::Utf8Str(expr) => { - assert_eq!(expr.ty, ast::WebidlScalarType::DomString.into()); - self.js.typescript_required("string"); - let ptr = self.arg(expr.offset); - let len = self.arg(expr.length); - self.cx.expose_get_string_from_wasm()?; - Ok(format!("getStringFromWasm({}, {})", ptr, len)) - } - - ast::OutgoingBindingExpression::Utf8CStr(_) => { - bail!("unsupported `utf8-cstr` found in outgoing webidl bindings"); - } - ast::OutgoingBindingExpression::I32ToEnum(_) => { - bail!("unsupported `i32-to-enum` found in outgoing webidl bindings"); - } - ast::OutgoingBindingExpression::Copy(_) => { - bail!("unsupported `copy` found in outgoing webidl bindings"); - } - ast::OutgoingBindingExpression::Dict(_) => { - bail!("unsupported `dict` found in outgoing webidl bindings"); - } - ast::OutgoingBindingExpression::BindExport(_) => { - bail!("unsupported `bind-export` found in outgoing webidl bindings"); - } - } - } - - fn arg(&self, idx: u32) -> String { - self.js.arg(idx).to_string() - } - - fn prelude_free_vector( - &mut self, - offset: u32, - length: u32, - kind: VectorKind, - ) -> Result<(), Error> { - self.js.prelude(&format!( - "wasm.__wbindgen_free({0}, {1} * {size});", - self.arg(offset), - self.arg(length), - size = kind.size(), - )); - self.cx.require_internal_export("__wbindgen_free") - } - - fn prelude_free_cached_string(&mut self, ptr: &str, len: &str) -> Result<(), Error> { - self.js.prelude(&format!( - "if ({ptr} !== 0) {{ wasm.__wbindgen_free({ptr}, {len}); }}", - ptr = ptr, - len = len, - )); - - self.cx.require_internal_export("__wbindgen_free") - } -} diff --git a/crates/cli-support/src/lib.rs b/crates/cli-support/src/lib.rs index 20ee1e38..e3313998 100755 --- a/crates/cli-support/src/lib.rs +++ b/crates/cli-support/src/lib.rs @@ -16,8 +16,9 @@ mod descriptor; mod descriptors; mod intrinsic; mod js; +mod multivalue; pub mod wasm2es6js; -mod webidl; +mod wit; pub struct Bindgen { input: Input, @@ -268,7 +269,7 @@ impl Bindgen { .generate_dwarf(self.keep_debug) .generate_name_section(!self.remove_name_section) .generate_producers_section(!self.remove_producers_section) - .on_parse(wasm_webidl_bindings::binary::on_parse) + .on_parse(wit_walrus::on_parse) .parse(&contents) .context("failed to parse input file as wasm")?; let stem = match &self.out_name { @@ -329,11 +330,11 @@ impl Bindgen { // Process and remove our raw custom sections emitted by the // #[wasm_bindgen] macro and the compiler. In their stead insert a - // forward-compatible WebIDL bindings section (forward-compatible with - // the webidl bindings proposal) as well as an auxiliary section for all - // sorts of miscellaneous information and features #[wasm_bindgen] - // supports that aren't covered by WebIDL bindings. - webidl::process( + // forward-compatible wasm interface types section as well as an + // auxiliary section for all sorts of miscellaneous information and + // features #[wasm_bindgen] supports that aren't covered by wasm + // interface types. + wit::process( &mut module, self.anyref, self.wasm_interface_types, @@ -346,34 +347,32 @@ impl Bindgen { // currently off-by-default since `anyref` is still in development in // engines. if self.anyref { - anyref::process(&mut module, self.wasm_interface_types)?; + anyref::process(&mut module)?; } let aux = module .customs - .delete_typed::() + .delete_typed::() .expect("aux section should be present"); - let mut bindings = module + let mut adapters = module .customs - .delete_typed::() + .delete_typed::() .unwrap(); // Now that our module is massaged and good to go, feed it into the JS // shim generation which will actually generate JS for all this. let (npm_dependencies, (js, ts)) = { - let mut cx = js::Context::new(&mut module, self)?; - cx.generate(&aux, &bindings)?; + let mut cx = js::Context::new(&mut module, self, &adapters, &aux)?; + cx.generate()?; let npm_dependencies = cx.npm_dependencies.clone(); (npm_dependencies, cx.finalize(stem)?) }; if self.wasm_interface_types { - if self.multi_value { - webidl::standard::add_multi_value(&mut module, &mut bindings) - .context("failed to transform return pointers into multi-value Wasm")?; - } - webidl::standard::add_section(&mut module, &aux, &bindings) - .with_context(|| "failed to generate a standard wasm bindings custom section")?; + multivalue::run(&mut module, &mut adapters) + .context("failed to transform return pointers into multi-value Wasm")?; + wit::section::add(&mut module, &aux, &adapters) + .context("failed to generate a standard wasm bindings custom section")?; } else { if self.multi_value { anyhow::bail!( diff --git a/crates/cli-support/src/multivalue.rs b/crates/cli-support/src/multivalue.rs new file mode 100644 index 00000000..6e2f866c --- /dev/null +++ b/crates/cli-support/src/multivalue.rs @@ -0,0 +1,77 @@ +use crate::wit::{Adapter, NonstandardWitSection}; +use crate::wit::{AdapterKind, Instruction}; +use anyhow::Error; +use walrus::Module; +use wasm_bindgen_multi_value_xform as multi_value_xform; +use wasm_bindgen_wasm_conventions as wasm_conventions; + +pub fn run(module: &mut Module, adapters: &mut NonstandardWitSection) -> Result<(), Error> { + let mut to_xform = Vec::new(); + let mut slots = Vec::new(); + + for (_, adapter) in adapters.adapters.iter_mut() { + extract_xform(adapter, &mut to_xform, &mut slots); + } + if to_xform.is_empty() { + // Early exit to avoid failing if we don't have a memory or shadow stack + // pointer because this is a minimal module that doesn't use linear + // memory. + return Ok(()); + } + + let shadow_stack_pointer = wasm_conventions::get_shadow_stack_pointer(module)?; + let memory = wasm_conventions::get_memory(module)?; + let wrappers = multi_value_xform::run(module, memory, shadow_stack_pointer, &to_xform)?; + + for (slot, id) in slots.into_iter().zip(wrappers) { + *slot = id; + } + + Ok(()) +} + +fn extract_xform<'a>( + adapter: &'a mut Adapter, + to_xform: &mut Vec<(walrus::FunctionId, usize, Vec)>, + slots: &mut Vec<&'a mut walrus::FunctionId>, +) { + let instructions = match &mut adapter.kind { + AdapterKind::Local { instructions } => instructions, + AdapterKind::Import { .. } => return, + }; + + // If the first instruction is a `Retptr`, then this must be an exported + // adapter which calls a wasm-defined function. Something we'd like to + // adapt to multi-value! + if let Some(Instruction::Retptr) = instructions.first().map(|e| &e.instr) { + instructions.remove(0); + let mut types = Vec::new(); + instructions.retain(|instruction| match instruction.instr { + Instruction::LoadRetptr { ty, .. } => { + types.push(ty.to_wasm().unwrap()); + false + } + _ => true, + }); + let id = instructions + .iter_mut() + .filter_map(|i| match &mut i.instr { + Instruction::Standard(wit_walrus::Instruction::CallCore(f)) => Some(f), + _ => None, + }) + .next() + .expect("should have found call-core"); + + // LLVM currently always uses the first parameter for the return + // pointer. We hard code that here, since we have no better option. + to_xform.push((*id, 0, types)); + slots.push(id); + return; + } + + // If the last instruction is a `StoreRetptr`, then this must be an adapter + // which calls an imported function. + // + // FIXME(#1872) handle this + // if let Some(Instruction::StoreRetptr { .. }) = instructions.last() {} +} diff --git a/crates/cli-support/src/webidl/bindings.rs b/crates/cli-support/src/webidl/bindings.rs deleted file mode 100644 index cc6db92f..00000000 --- a/crates/cli-support/src/webidl/bindings.rs +++ /dev/null @@ -1,250 +0,0 @@ -//! Location where `Binding` structures are actually created. -//! -//! This module is tasked with converting `Descriptor::Function` instances to -//! `Binding`s. It uses the incoming/outgoing modules/builders to do most of the -//! heavy lifting, and then this is the glue around the edges to make sure -//! everything is processed, hooked up in the second, and then inserted into the -//! right map. -//! -//! This module is called from `src/webidl/mod.rs` exclusively to populate the -//! imports/exports/elements of the bindings section. Most of this module is -//! largely just connecting the dots! - -use crate::descriptor::Function; -use crate::webidl::incoming::IncomingBuilder; -use crate::webidl::outgoing::OutgoingBuilder; -use crate::webidl::{Binding, NonstandardWebidlSection}; -use anyhow::{format_err, Error}; -use walrus::{FunctionId, Module, ValType}; -use wasm_webidl_bindings::ast; - -/// Adds an element to the `bindings.imports` map for the `import` specified -/// that is supposed to have the signature specified in `binding`. This also -/// expects that the imported item is called as `kind`. -pub fn register_import( - module: &mut Module, - bindings: &mut NonstandardWebidlSection, - import: walrus::ImportId, - binding: Function, - kind: ast::WebidlFunctionKind, -) -> Result<(), Error> { - let import = module.imports.get(import); - let id = match import.kind { - walrus::ImportKind::Function(f) => f, - _ => unreachable!(), - }; - let import_id = import.id(); - - // Process the return value first to determine if we need a return pointer - // since that is always the first argument. - let mut incoming = IncomingBuilder::default(); - incoming.process(&binding.ret)?; - - // Next process all outgoing arguments, and configure the module/bindings - // section to be available to the builder so we can recursively register - // stack closures. - let mut outgoing = OutgoingBuilder::default(); - outgoing.module = Some(module); - outgoing.bindings_section = Some(bindings); - if incoming.wasm.len() > 1 { - outgoing.process_retptr(); - } - for arg in binding.arguments.iter() { - outgoing.process(arg)?; - } - - // A bit of destructuring to kill the borrow that the outgoing builder has - // on the module/bindings. - let OutgoingBuilder { - wasm: outgoing_wasm, - webidl: outgoing_webidl, - bindings: outgoing_bindings, - .. - } = outgoing; - - // Boilerplate to assemble the `webidl_ty` and `wasm_ty` values. - let webidl_ty = webidl_ty( - &mut bindings.types, - kind, - &outgoing_webidl, - &incoming.webidl, - ); - let (wasm_ty, return_via_outptr) = - assert_signature_match(module, id, &outgoing_wasm, &incoming.wasm); - - // ... and finally insert it into our map! - bindings.imports.insert( - import_id, - Binding { - return_via_outptr, - wasm_ty, - incoming: incoming.bindings, - outgoing: outgoing_bindings, - webidl_ty, - }, - ); - Ok(()) -} - -/// Adds an element to `bindings.exports` for the `export` specified to have the -/// `binding` given. -pub fn register_export( - module: &mut Module, - bindings: &mut NonstandardWebidlSection, - export: walrus::ExportId, - binding: Function, -) -> Result<(), Error> { - let export = module.exports.get(export); - let id = match export.item { - walrus::ExportItem::Function(f) => f, - _ => unreachable!(), - }; - let export_id = export.id(); - // Do the actual heavy lifting elsewhere to generate the `binding`. - let binding = register_wasm_export(module, bindings, id, binding)?; - bindings.exports.insert(export_id, binding); - Ok(()) -} - -/// Like `register_export` except registers a binding for a table element. In -/// this case ensures that the table element `idx` is specified to have the -/// `binding` signature specified, eventually updating `bindings.elems` list. -/// -/// Returns the index of the item added in the `bindings.elems` list. -pub fn register_table_element( - module: &mut Module, - bindings: &mut NonstandardWebidlSection, - idx: u32, - binding: Function, -) -> Result { - let table = module - .tables - .main_function_table()? - .ok_or_else(|| format_err!("no function table found"))?; - let table = module.tables.get(table); - let functions = match &table.kind { - walrus::TableKind::Function(f) => f, - _ => unreachable!(), - }; - let id = functions.elements[idx as usize].unwrap(); - let ret = bindings.elems.len() as u32; - // like above, largely just defer the work elsewhere - let binding = register_wasm_export(module, bindings, id, binding)?; - bindings.elems.push((idx, binding)); - Ok(ret) -} - -/// Common routine to create a `Binding` for an exported wasm function, using -/// incoming arguments and an outgoing return value. -fn register_wasm_export( - module: &mut Module, - bindings: &mut NonstandardWebidlSection, - id: walrus::FunctionId, - binding: Function, -) -> Result { - // Like imports, process the return value first to determine if we need a - // return pointer - let mut outgoing = OutgoingBuilder::default(); - outgoing.process(&binding.ret)?; - - // Afterwards process all arguments... - let mut incoming = IncomingBuilder::default(); - if outgoing.wasm.len() > 1 { - incoming.process_retptr(); - } - for arg in binding.arguments.iter() { - incoming.process(arg)?; - } - - // ... do similar boilerplate to imports (but with incoming/outgoing - // swapped) to produce some types ... - let webidl_ty = webidl_ty( - &mut bindings.types, - ast::WebidlFunctionKind::Static, - &incoming.webidl, - &outgoing.webidl, - ); - let (wasm_ty, return_via_outptr) = - assert_signature_match(module, id, &incoming.wasm, &outgoing.wasm); - - // ... and there's our `Binding`! - Ok(Binding { - wasm_ty, - incoming: incoming.bindings, - outgoing: outgoing.bindings, - webidl_ty, - return_via_outptr, - }) -} - -/// Asserts that the `params` and `results` we've determined from an -/// incoming/outgoing builder actually matches the signature of `id` in the -/// `module` provided. This is a somewhat loose comparison since `anyref` in the -/// expected lists will be present as `i32` in the actual module due to rustc -/// limitations. -/// -/// This at the end manufactures an actual `walrus::Type` that will be used to -/// describe a WebIDL value. This manufactured value actually has `anyref` types -/// in it and also respects the out ptr ABI that we currently use to handle -/// multiple-value returns. -fn assert_signature_match( - module: &mut Module, - id: FunctionId, - params: &[ValType], - mut results: &[ValType], -) -> (walrus::TypeId, Option>) { - let ty = module.funcs.get(id).ty(); - let ty = module.types.get(ty); - - fn assert_eq(expected: ValType, actual: ValType) { - match expected { - ValType::Anyref => assert_eq!(actual, ValType::I32), - _ => assert_eq!(expected, actual), - } - } - let mut ret_outptr = None; - - match results.len() { - 0 => assert_eq!(ty.results().len(), 0), - 1 => assert_eq(results[0], ty.results()[0]), - - // multi value isn't supported yet so all aggregate returns are done - // through an outptr as the first argument. This means that our - // signature should have no results. The new signature we create will - // also have no results. - _ => { - assert_eq!(ty.results().len(), 0); - ret_outptr = Some(results.to_vec()); - results = &[]; - } - } - - let mut iter = params.iter(); - for actual in ty.params().iter() { - let expected = iter.next().unwrap(); - assert_eq(*expected, *actual); - } - assert!(iter.next().is_none()); - - (module.types.add(params, results), ret_outptr) -} - -// boilerplate to convert arguments to a `WebidlFunctionId`. -fn webidl_ty( - types: &mut ast::WebidlTypes, - kind: ast::WebidlFunctionKind, - params: &[ast::WebidlScalarType], - results: &[ast::WebidlScalarType], -) -> ast::WebidlFunctionId { - let result = match results.len() { - 0 => None, - 1 => Some(results[0].into()), - _ => panic!("too many results in a webidl return value"), - }; - let func = ast::WebidlFunction { - kind, - params: params.iter().cloned().map(|x| x.into()).collect(), - result, - }; - types.insert(func) -} diff --git a/crates/cli-support/src/webidl/incoming.rs b/crates/cli-support/src/webidl/incoming.rs deleted file mode 100644 index 24791914..00000000 --- a/crates/cli-support/src/webidl/incoming.rs +++ /dev/null @@ -1,492 +0,0 @@ -//! Nonstandard and wasm-bindgen specific definition of incoming bindings to a -//! wasm module. -//! -//! This module provides a builder which is used to translate Rust types (aka a -//! `Descriptor`) to a `NonstandardIncoming` definition which describes how the -//! JS type is converted into a Rust type. We try to use standard webidl -//! bindings as much as possible, but we have quite a few other bindings which -//! require custom code and shims currently still. -//! -//! Note that the mirror operation, going from WebAssembly to JS, is found in -//! the `outgoing.rs` module. - -use crate::descriptor::{Descriptor, VectorKind}; -use anyhow::{bail, format_err, Error}; -use walrus::ValType; -use wasm_webidl_bindings::ast; - -/// A list of all incoming bindings from JS to WebAssembly that wasm-bindgen -/// will take advantage of. -#[derive(Debug, Clone)] -pub enum NonstandardIncoming { - /// This is a standard vanilla incoming binding. When WebIDL bindings are - /// implemented, this can be used as-is. - Standard(ast::IncomingBindingExpression), - - /// JS is passing a `BigInt` to Rust. - Int64 { - val: ast::IncomingBindingExpression, - /// Whether it's a `u64` or `i64` in Rust. - signed: bool, - }, - - /// JS is passing a `BigInt64Array` or `BigUint64Array` to Rust - /// - /// A copy of the array needs to be made into the Rust address space. - AllocCopyInt64 { - alloc_func_name: String, - expr: Box, - /// Whether or not this is for &[u64] or &[i64] - signed: bool, - }, - - /// JS is passing an array of anyref values into Rust, and all the values - /// need to be copied in. - AllocCopyAnyrefArray { - alloc_func_name: String, - expr: Box, - }, - - /// A mutable slice of values going from JS to Rust, and after Rust finishes - /// the JS slice is updated with the current value of the slice. - MutableSlice { - kind: VectorKind, - val: ast::IncomingBindingExpression, - }, - - /// This is either a slice or `undefined` being passed into Rust. - OptionSlice { - kind: VectorKind, - val: ast::IncomingBindingExpression, - mutable: bool, - }, - - /// This is either a vector or `undefined` being passed into Rust. - OptionVector { - kind: VectorKind, - val: ast::IncomingBindingExpression, - }, - - /// Not actually used for `JsValue` but used for imported types, this is - /// either `undefined` or the imported type getting passed into Rust. - OptionAnyref { val: ast::IncomingBindingExpression }, - - /// An optional "native type" which includes i32/u32/f32/f64, all of which - /// require a discriminant. - OptionNative { val: ast::IncomingBindingExpression }, - - /// An optional integer type which uses an 0xffffff sentinel value for - /// "none" - OptionU32Sentinel { val: ast::IncomingBindingExpression }, - - /// An optional boolean using a special ABI for communicating `undefined` - OptionBool { val: ast::IncomingBindingExpression }, - - /// An optional `char` which uses an ABI where `undefined` is a hole in the - /// range of valid values for a `char` in Rust. Note that in JS a string is - /// passed in. - OptionChar { val: ast::IncomingBindingExpression }, - - /// An optional integral enum where `undefined` is the hole specified. - OptionIntegerEnum { - val: ast::IncomingBindingExpression, - hole: u32, - }, - - /// An optional `BigInt`. - OptionInt64 { - val: ast::IncomingBindingExpression, - signed: bool, - }, - - /// An optional Rust-based type which internally has a pointer that's - /// wrapped up in a JS class. This transfers ownership from JS to Rust. - RustType { - class: String, - val: ast::IncomingBindingExpression, - }, - - /// A reference to a Rust-based type where Rust won't take ownership of the - /// value, it just has a temporary borrow on the input. - RustTypeRef { - class: String, - val: ast::IncomingBindingExpression, - }, - - /// An optional owned Rust type being transferred from JS to Rust. - OptionRustType { - class: String, - val: ast::IncomingBindingExpression, - }, - - /// A string from JS where the first character goes through to Rust. - Char { val: ast::IncomingBindingExpression }, - - /// An arbitrary `anyref` being passed into Rust, but explicitly one that's - /// borrowed and doesn't need to be persisted in a heap table. - BorrowedAnyref { val: ast::IncomingBindingExpression }, -} - -/// Builder used to create a incomig binding from a `Descriptor`. -#[derive(Default)] -pub struct IncomingBuilder { - /// The wasm types that needs to be used to represent all the descriptors in - /// Rust. - pub wasm: Vec, - /// The WebIDL scalar types which match what JS will be providing. - pub webidl: Vec, - /// The list of bindings necessary to connect `wasm` to `webidl` above. - pub bindings: Vec, -} - -impl IncomingBuilder { - /// Adds an initial argument which is passed through verbatim, currently - /// used to handle return pointers in Rust. - pub fn process_retptr(&mut self) { - self.number(ValType::I32, ast::WebidlScalarType::Long); - } - - /// Process a `Descriptor` as if it's being passed from JS to Rust. This - /// will skip `Unit` and otherwise internally add a `NonstandardIncoming` - /// binding necessary for the descriptor. - pub fn process(&mut self, arg: &Descriptor) -> Result<(), Error> { - if let Descriptor::Unit = arg { - return Ok(()); - } - // This is a wrapper around `_process` to have a number of sanity checks - // that we don't forget things. We should always produce at least one - // wasm arge and exactly one webidl arg. Additionally the number of - // bindings should always match the number of webidl types for now. - assert_eq!(self.webidl.len(), self.bindings.len()); - let wasm_before = self.wasm.len(); - let webidl_before = self.webidl.len(); - self._process(arg)?; - assert_eq!(self.webidl.len(), self.bindings.len()); - assert_eq!(webidl_before + 1, self.webidl.len()); - assert!(wasm_before < self.wasm.len()); - Ok(()) - } - - fn _process(&mut self, arg: &Descriptor) -> Result<(), Error> { - match arg { - Descriptor::Boolean => { - let binding = self.expr_as(ValType::I32); - self.wasm.push(ValType::I32); - self.webidl.push(ast::WebidlScalarType::Boolean); - self.bindings.push(NonstandardIncoming::Standard(binding)); - } - Descriptor::Char => { - let expr = self.expr_get(); - self.wasm.push(ValType::I32); - self.webidl.push(ast::WebidlScalarType::DomString); - self.bindings.push(NonstandardIncoming::Char { val: expr }); - } - Descriptor::Anyref => { - let expr = self.expr_as(ValType::Anyref); - self.wasm.push(ValType::Anyref); - self.webidl.push(ast::WebidlScalarType::Any); - self.bindings.push(NonstandardIncoming::Standard(expr)); - } - Descriptor::RustStruct(class) => { - let expr = self.expr_get(); - self.wasm.push(ValType::I32); - self.webidl.push(ast::WebidlScalarType::Any); - self.bindings.push(NonstandardIncoming::RustType { - val: expr, - class: class.to_string(), - }); - } - Descriptor::I8 => self.number(ValType::I32, ast::WebidlScalarType::Byte), - Descriptor::U8 => self.number(ValType::I32, ast::WebidlScalarType::Octet), - Descriptor::I16 => self.number(ValType::I32, ast::WebidlScalarType::Short), - Descriptor::U16 => self.number(ValType::I32, ast::WebidlScalarType::UnsignedShort), - Descriptor::I32 => self.number(ValType::I32, ast::WebidlScalarType::Long), - Descriptor::U32 => self.number(ValType::I32, ast::WebidlScalarType::UnsignedLong), - Descriptor::I64 => self.number64(true), - Descriptor::U64 => self.number64(false), - Descriptor::F32 => self.number(ValType::F32, ast::WebidlScalarType::Float), - Descriptor::F64 => self.number(ValType::F64, ast::WebidlScalarType::Double), - Descriptor::Enum { .. } => self.number(ValType::I32, ast::WebidlScalarType::Long), - Descriptor::Ref(d) => self.process_ref(false, d)?, - Descriptor::RefMut(d) => self.process_ref(true, d)?, - Descriptor::Option(d) => self.process_option(d)?, - - Descriptor::String | Descriptor::CachedString | Descriptor::Vector(_) => { - let kind = arg.vector_kind().ok_or_else(|| { - format_err!("unsupported argument type for calling Rust function from JS {:?}", arg) - })? ; - self.wasm.extend(&[ValType::I32; 2]); - self.alloc_copy_kind(kind) - } - - // Can't be passed from JS to Rust yet - Descriptor::Function(_) | - Descriptor::Closure(_) | - - // Always behind a `Ref` - Descriptor::Slice(_) => bail!( - "unsupported argument type for calling Rust function from JS: {:?}", - arg - ), - - // nothing to do - Descriptor::Unit => {} - - // Largely synthetic and can't show up - Descriptor::ClampedU8 => unreachable!(), - } - Ok(()) - } - - fn process_ref(&mut self, mutable: bool, arg: &Descriptor) -> Result<(), Error> { - match arg { - Descriptor::RustStruct(class) => { - let expr = self.expr_get(); - self.wasm.push(ValType::I32); - self.webidl.push(ast::WebidlScalarType::Any); - self.bindings.push(NonstandardIncoming::RustTypeRef { - val: expr, - class: class.to_string(), - }); - } - Descriptor::Anyref => { - let expr = self.expr_get(); - self.wasm.push(ValType::Anyref); - self.webidl.push(ast::WebidlScalarType::Any); - self.bindings - .push(NonstandardIncoming::BorrowedAnyref { val: expr }); - } - Descriptor::String | Descriptor::CachedString | Descriptor::Slice(_) => { - let kind = arg.vector_kind().ok_or_else(|| { - format_err!( - "unsupported slice type for calling Rust function from JS {:?}", - arg - ) - })?; - self.wasm.extend(&[ValType::I32; 2]); - if mutable { - self.bindings.push(NonstandardIncoming::MutableSlice { - kind, - val: self.expr_get(), - }); - self.webidl.push(ast::WebidlScalarType::Any); - } else { - self.alloc_copy_kind(kind) - } - } - _ => bail!( - "unsupported reference argument type for calling Rust function from JS: {:?}", - arg - ), - } - Ok(()) - } - - fn process_option(&mut self, arg: &Descriptor) -> Result<(), Error> { - match arg { - Descriptor::Anyref => { - self.wasm.push(ValType::I32); - self.bindings.push(NonstandardIncoming::OptionAnyref { - val: self.expr_get(), - }); - self.webidl.push(ast::WebidlScalarType::Any); - } - Descriptor::I8 => self.option_sentinel(), - Descriptor::U8 => self.option_sentinel(), - Descriptor::I16 => self.option_sentinel(), - Descriptor::U16 => self.option_sentinel(), - Descriptor::I32 => self.option_native(ValType::I32), - Descriptor::U32 => self.option_native(ValType::I32), - Descriptor::F32 => self.option_native(ValType::F32), - Descriptor::F64 => self.option_native(ValType::F64), - Descriptor::I64 | Descriptor::U64 => { - let expr = self.expr_get(); - let signed = match arg { - Descriptor::I64 => true, - _ => false, - }; - self.wasm.extend(&[walrus::ValType::I32; 4]); - self.webidl.push(ast::WebidlScalarType::Any); - self.bindings - .push(NonstandardIncoming::OptionInt64 { val: expr, signed }); - } - Descriptor::Boolean => { - let expr = self.expr_get(); - self.wasm.push(walrus::ValType::I32); - self.webidl.push(ast::WebidlScalarType::Any); - self.bindings - .push(NonstandardIncoming::OptionBool { val: expr }); - } - Descriptor::Char => { - let expr = self.expr_get(); - self.wasm.push(walrus::ValType::I32); - self.webidl.push(ast::WebidlScalarType::Any); - self.bindings - .push(NonstandardIncoming::OptionChar { val: expr }); - } - Descriptor::Enum { hole } => { - let expr = self.expr_get(); - self.wasm.push(walrus::ValType::I32); - self.webidl.push(ast::WebidlScalarType::Any); - self.bindings.push(NonstandardIncoming::OptionIntegerEnum { - val: expr, - hole: *hole, - }); - } - Descriptor::RustStruct(name) => { - let expr = self.expr_get(); - self.wasm.push(walrus::ValType::I32); - self.webidl.push(ast::WebidlScalarType::Any); - self.bindings.push(NonstandardIncoming::OptionRustType { - val: expr, - class: name.to_string(), - }); - } - - Descriptor::Ref(_) | Descriptor::RefMut(_) => { - let mutable = match arg { - Descriptor::Ref(_) => false, - _ => true, - }; - let kind = arg.vector_kind().ok_or_else(|| { - format_err!( - "unsupported optional slice type for calling Rust function from JS {:?}", - arg - ) - })?; - self.bindings.push(NonstandardIncoming::OptionSlice { - kind, - val: self.expr_get(), - mutable, - }); - self.wasm.extend(&[ValType::I32; 2]); - self.webidl.push(ast::WebidlScalarType::Any); - } - - Descriptor::String | Descriptor::CachedString | Descriptor::Vector(_) => { - let kind = arg.vector_kind().ok_or_else(|| { - format_err!( - "unsupported optional slice type for calling Rust function from JS {:?}", - arg - ) - })?; - self.bindings.push(NonstandardIncoming::OptionVector { - kind, - val: self.expr_get(), - }); - self.wasm.extend(&[ValType::I32; 2]); - self.webidl.push(ast::WebidlScalarType::Any); - } - - _ => bail!( - "unsupported optional argument type for calling Rust function from JS: {:?}", - arg - ), - } - Ok(()) - } - - fn expr_get(&self) -> ast::IncomingBindingExpression { - let idx = self.webidl.len() as u32; - ast::IncomingBindingExpressionGet { idx }.into() - } - - fn expr_as(&self, ty: ValType) -> ast::IncomingBindingExpression { - ast::IncomingBindingExpressionAs { - ty, - expr: Box::new(self.expr_get()), - } - .into() - } - - fn alloc_func_name(&self) -> String { - "__wbindgen_malloc".to_string() - } - - fn alloc_copy_kind(&mut self, kind: VectorKind) { - use wasm_webidl_bindings::ast::WebidlScalarType::*; - - match kind { - VectorKind::I8 => self.alloc_copy(Int8Array), - VectorKind::U8 => self.alloc_copy(Uint8Array), - VectorKind::ClampedU8 => self.alloc_copy(Uint8ClampedArray), - VectorKind::I16 => self.alloc_copy(Int16Array), - VectorKind::U16 => self.alloc_copy(Uint16Array), - VectorKind::I32 => self.alloc_copy(Int32Array), - VectorKind::U32 => self.alloc_copy(Uint32Array), - VectorKind::F32 => self.alloc_copy(Float32Array), - VectorKind::F64 => self.alloc_copy(Float64Array), - VectorKind::String => { - let expr = ast::IncomingBindingExpressionAllocUtf8Str { - alloc_func_name: self.alloc_func_name(), - expr: Box::new(self.expr_get()), - }; - self.webidl.push(DomString); - self.bindings - .push(NonstandardIncoming::Standard(expr.into())); - } - VectorKind::I64 | VectorKind::U64 => { - let signed = match kind { - VectorKind::I64 => true, - _ => false, - }; - self.bindings.push(NonstandardIncoming::AllocCopyInt64 { - alloc_func_name: self.alloc_func_name(), - expr: Box::new(self.expr_get()), - signed, - }); - self.webidl.push(Any); - } - VectorKind::Anyref => { - self.bindings - .push(NonstandardIncoming::AllocCopyAnyrefArray { - alloc_func_name: self.alloc_func_name(), - expr: Box::new(self.expr_get()), - }); - self.webidl.push(Any); - } - } - } - - fn alloc_copy(&mut self, webidl: ast::WebidlScalarType) { - let expr = ast::IncomingBindingExpressionAllocCopy { - alloc_func_name: self.alloc_func_name(), - expr: Box::new(self.expr_get()), - }; - self.webidl.push(webidl); - self.bindings - .push(NonstandardIncoming::Standard(expr.into())); - } - - fn number(&mut self, wasm: ValType, webidl: ast::WebidlScalarType) { - let binding = self.expr_as(wasm); - self.wasm.push(wasm); - self.webidl.push(webidl); - self.bindings.push(NonstandardIncoming::Standard(binding)); - } - - fn number64(&mut self, signed: bool) { - let expr = self.expr_get(); - self.wasm.extend(&[ValType::I32; 2]); - self.webidl.push(ast::WebidlScalarType::Any); - self.bindings - .push(NonstandardIncoming::Int64 { val: expr, signed }); - } - - fn option_native(&mut self, wasm: ValType) { - let expr = self.expr_get(); - self.wasm.push(ValType::I32); - self.wasm.push(wasm); - self.webidl.push(ast::WebidlScalarType::Any); - self.bindings - .push(NonstandardIncoming::OptionNative { val: expr }); - } - - fn option_sentinel(&mut self) { - let expr = self.expr_get(); - self.wasm.push(ValType::I32); - self.webidl.push(ast::WebidlScalarType::Any); - self.bindings - .push(NonstandardIncoming::OptionU32Sentinel { val: expr }); - } -} diff --git a/crates/cli-support/src/webidl/outgoing.rs b/crates/cli-support/src/webidl/outgoing.rs deleted file mode 100644 index ba57ab54..00000000 --- a/crates/cli-support/src/webidl/outgoing.rs +++ /dev/null @@ -1,556 +0,0 @@ -//! This module is used to define `NonstandardOutgoing`, a list of possible ways -//! values in Rust can be passed to JS. -//! -//! Like the `NonstandardIncoming` list we attempt to use a standard -//! `OutgoingBindingExpression` wherever possible but we naturally have a lot of -//! features in `wasm-bindgen` which haven't been upstreamed into the WebIDL -//! bindings standard yet (nor which are likely to ever get standardized). We -//! attempt to use standard bindings aggressively and wherever possible, but -//! sometimes we need to resort to our own custom bindings with our own custom -//! JS shims for now. -//! -//! This module also houses the definition of converting a `Descriptor` to a -//! `NonstandardOutgoing` binding, effectively defining how to translate from a -//! Rust type to an outgoing binding. - -use crate::descriptor::{Descriptor, VectorKind}; -use crate::webidl::NonstandardWebidlSection; -use anyhow::{bail, format_err, Error}; -use walrus::{Module, ValType}; -use wasm_webidl_bindings::ast; - -/// A list of all possible outgoing bindings which can be used when converting -/// Rust types to JS. This is predominantly used when calling an imported JS -/// function. -#[derive(Debug, Clone)] -pub enum NonstandardOutgoing { - /// This is a standard upstream WebIDL outgoing binding expression. Where - /// possible we can actually leave this in the wasm file and generate even - /// less JS shim code. - Standard(ast::OutgoingBindingExpression), - - /// We're returning a pointer from Rust to JS to get wrapped in a JS class - /// which has memory management around it. - RustType { class: String, idx: u32 }, - - /// A single rust `char` value which is converted to a `string` in JS. - Char { idx: u32 }, - - /// An `i64` or `u64` in Rust converted to a `BigInt` in JS - Number64 { - lo_idx: u32, - hi_idx: u32, - signed: bool, - }, - - /// A *borrowed* anyref value which has special meanings about ownership, - /// namely Rust is still using the underlying value after the call returns. - BorrowedAnyref { idx: u32 }, - - /// An owned vector is passed from Rust to JS. Note that this is currently a - /// special binding because it requires memory management via deallocation - /// in the JS shim. - /// - /// TODO: we should strive to not have this nonstandard binding and instead - /// do all the memory management in Rust. Ideally we'd use `AllocCopy` in - /// place of this. - Vector { - offset: u32, - length: u32, - kind: VectorKind, - }, - - /// A Rust String (or &str) which might be cached, or might be `None`. - /// - /// If `offset` is 0 then it is cached, and the cached JsValue's index is in `length`. - /// - /// If `offset` and `length` are both 0, then it is `None`. - CachedString { - offset: u32, - length: u32, - owned: bool, - optional: bool, - }, - - /// A `&[u64]` or `&[i64]` is being passed to JS, and the 64-bit sizes here - /// aren't supported by WebIDL bindings yet. - View64 { - offset: u32, - length: u32, - signed: bool, - }, - - /// A list of `anyref` is being passed to JS, and it's got a somewhat - /// magical representation with indics which doesn't map to WebIDL bindings. - ViewAnyref { offset: u32, length: u32 }, - - /// An optional owned vector of data is being passed to JS. - /// - /// TODO: with some cleverness this could probably use `AllocCopy`. - OptionVector { - offset: u32, - length: u32, - kind: VectorKind, - }, - - /// An optional slice of data is being passed into JS. - /// - /// TODO: with some cleverness this could probably use `AllocCopy`. - OptionSlice { - kind: VectorKind, - offset: u32, - length: u32, - }, - - /// An optional "native type" like i32/u32/f32/f64 is being passed to JS, - /// and this requires a discriminant in the ABI. - OptionNative { - present: u32, - val: u32, - signed: bool, - }, - - /// An optional number is being passed to JS where the number uses a - /// sentinel value to represent `None` - OptionU32Sentinel { idx: u32 }, - - /// An optional boolean with a special value for `None` - OptionBool { idx: u32 }, - - /// An optional character with a special value for `None` - OptionChar { idx: u32 }, - - /// An optional integral enum value with the specified `hole` being used for - /// `None`. - OptionIntegerEnum { idx: u32, hole: u32 }, - - /// An optional 64-bit integer being used. - OptionInt64 { - present: u32, - _ignored: u32, - lo: u32, - hi: u32, - signed: bool, - }, - - /// An optional owned Rust type being transferred from Rust to JS. - OptionRustType { class: String, idx: u32 }, - - /// A temporary stack closure being passed from Rust to JS. A JS function is - /// manufactured and then neutered just before the call returns. - StackClosure { - /// Argument index of the first data pointer Rust needs - a: u32, - /// Argument index of the second data pointer Rust needs - b: u32, - /// The index of the shim in the element bindings section that we're - /// going to be invoking. - binding_idx: u32, - /// Number of arguments to the closure - nargs: usize, - /// Whether or not this is a mutable closure (affects codegen and how - /// it's called recursively) - mutable: bool, - }, -} - -/// A definition of building `NonstandardOutgoing` expressions from a -/// `Descriptor`. -/// -/// This will internally keep track of wasm/webidl types generated as we visit -/// `Descriptor` arguments and add more for a function signature. -#[derive(Default)] -pub struct OutgoingBuilder<'a> { - /// All wasm types used so far to produce the resulting JS values. - pub wasm: Vec, - /// The WebIDL types that we're passing along out of wasm. - pub webidl: Vec, - /// The list of bindings we've created, currently 1:1 with the webidl above. - pub bindings: Vec, - - // These two arguments are optional and, if set, will enable creating - // `StackClosure` bindings. They're not present for return values from - // exported Rust functions, but they are available for the arguments of - // calling imported functions. - pub module: Option<&'a mut Module>, - pub bindings_section: Option<&'a mut NonstandardWebidlSection>, -} - -impl OutgoingBuilder<'_> { - /// Adds a dummy first argument which is passed through as an integer - /// representing the return pointer. - pub fn process_retptr(&mut self) { - self.standard_as(ValType::I32, ast::WebidlScalarType::Long); - } - - /// Processes one more `Descriptor` as an argument to a JS function that - /// wasm is calling. - /// - /// This will internally skip `Unit` and otherwise build up the `bindings` - /// map and ensure that it's correctly mapped from wasm to JS. - pub fn process(&mut self, arg: &Descriptor) -> Result<(), Error> { - if let Descriptor::Unit = arg { - return Ok(()); - } - assert_eq!(self.webidl.len(), self.bindings.len()); - let wasm_before = self.wasm.len(); - let webidl_before = self.webidl.len(); - self._process(arg)?; - assert_eq!(self.webidl.len(), self.bindings.len()); - assert_eq!(webidl_before + 1, self.webidl.len()); - assert!(wasm_before < self.wasm.len()); - Ok(()) - } - - fn _process(&mut self, arg: &Descriptor) -> Result<(), Error> { - match arg { - Descriptor::Boolean => self.standard_as(ValType::I32, ast::WebidlScalarType::Boolean), - Descriptor::Anyref => self.standard_as(ValType::Anyref, ast::WebidlScalarType::Any), - Descriptor::I8 => self.standard_as(ValType::I32, ast::WebidlScalarType::Byte), - Descriptor::U8 => self.standard_as(ValType::I32, ast::WebidlScalarType::Octet), - Descriptor::I16 => self.standard_as(ValType::I32, ast::WebidlScalarType::Short), - Descriptor::U16 => self.standard_as(ValType::I32, ast::WebidlScalarType::UnsignedShort), - Descriptor::I32 => self.standard_as(ValType::I32, ast::WebidlScalarType::Long), - Descriptor::U32 => self.standard_as(ValType::I32, ast::WebidlScalarType::UnsignedLong), - Descriptor::F32 => { - self.standard_as(ValType::F32, ast::WebidlScalarType::UnrestrictedFloat) - } - Descriptor::F64 => { - self.standard_as(ValType::F64, ast::WebidlScalarType::UnrestrictedDouble) - } - Descriptor::Enum { .. } => self.standard_as(ValType::I32, ast::WebidlScalarType::Long), - - Descriptor::Char => { - let idx = self.push_wasm(ValType::I32); - self.webidl.push(ast::WebidlScalarType::DomString); - self.bindings.push(NonstandardOutgoing::Char { idx }); - } - - Descriptor::I64 | Descriptor::U64 => { - let signed = match arg { - Descriptor::I64 => true, - _ => false, - }; - let lo_idx = self.push_wasm(ValType::I32); - let hi_idx = self.push_wasm(ValType::I32); - self.webidl.push(ast::WebidlScalarType::Any); - self.bindings.push(NonstandardOutgoing::Number64 { - lo_idx, - hi_idx, - signed, - }); - } - - Descriptor::RustStruct(class) => { - let idx = self.push_wasm(ValType::I32); - self.webidl.push(ast::WebidlScalarType::Any); - self.bindings.push(NonstandardOutgoing::RustType { - idx, - class: class.to_string(), - }); - } - Descriptor::Ref(d) => self.process_ref(false, d)?, - Descriptor::RefMut(d) => self.process_ref(true, d)?, - - Descriptor::CachedString => self.cached_string(false, true), - - Descriptor::Vector(_) | Descriptor::String => { - let kind = arg.vector_kind().ok_or_else(|| { - format_err!( - "unsupported argument type for calling JS function from Rust {:?}", - arg - ) - })?; - let offset = self.push_wasm(ValType::I32); - let length = self.push_wasm(ValType::I32); - self.webidl.push(ast::WebidlScalarType::Any); - self.bindings.push(NonstandardOutgoing::Vector { - offset, - kind, - length, - }) - } - - Descriptor::Option(d) => self.process_option(d)?, - - Descriptor::Function(_) | Descriptor::Closure(_) | Descriptor::Slice(_) => bail!( - "unsupported argument type for calling JS function from Rust: {:?}", - arg - ), - - // nothing to do - Descriptor::Unit => {} - - // Largely synthetic and can't show up - Descriptor::ClampedU8 => unreachable!(), - } - Ok(()) - } - - fn process_ref(&mut self, mutable: bool, arg: &Descriptor) -> Result<(), Error> { - match arg { - Descriptor::Anyref => { - let idx = self.push_wasm(ValType::Anyref); - self.webidl.push(ast::WebidlScalarType::Any); - self.bindings - .push(NonstandardOutgoing::BorrowedAnyref { idx }); - } - Descriptor::CachedString => self.cached_string(false, false), - Descriptor::Slice(_) | Descriptor::String => { - use wasm_webidl_bindings::ast::WebidlScalarType::*; - - let kind = arg.vector_kind().ok_or_else(|| { - format_err!( - "unsupported argument type for calling JS function from Rust {:?}", - arg - ) - })?; - let offset = self.push_wasm(ValType::I32); - let length = self.push_wasm(ValType::I32); - match kind { - VectorKind::I8 => self.standard_view(offset, length, Int8Array), - VectorKind::U8 => self.standard_view(offset, length, Uint8Array), - VectorKind::ClampedU8 => self.standard_view(offset, length, Uint8ClampedArray), - VectorKind::I16 => self.standard_view(offset, length, Int16Array), - VectorKind::U16 => self.standard_view(offset, length, Uint16Array), - VectorKind::I32 => self.standard_view(offset, length, Int32Array), - VectorKind::U32 => self.standard_view(offset, length, Uint32Array), - VectorKind::F32 => self.standard_view(offset, length, Float32Array), - VectorKind::F64 => self.standard_view(offset, length, Float64Array), - VectorKind::String => { - self.webidl.push(DomString); - let binding = ast::OutgoingBindingExpressionUtf8Str { - ty: ast::WebidlScalarType::DomString.into(), - offset, - length, - }; - self.bindings - .push(NonstandardOutgoing::Standard(binding.into())); - } - VectorKind::I64 | VectorKind::U64 => { - let signed = match kind { - VectorKind::I64 => true, - _ => false, - }; - self.webidl.push(Any); - self.bindings.push(NonstandardOutgoing::View64 { - offset, - length, - signed, - }); - } - VectorKind::Anyref => { - self.webidl.push(Any); - self.bindings - .push(NonstandardOutgoing::ViewAnyref { offset, length }); - } - } - } - - Descriptor::Function(descriptor) => { - let module = self - .module - .as_mut() - .ok_or_else(|| format_err!("cannot return a closure from Rust"))?; - let section = self.bindings_section.as_mut().unwrap(); - // synthesize the a/b arguments that aren't present in the - // signature from wasm-bindgen but are present in the wasm file. - let mut descriptor = (**descriptor).clone(); - let nargs = descriptor.arguments.len(); - descriptor.arguments.insert(0, Descriptor::I32); - descriptor.arguments.insert(0, Descriptor::I32); - let binding_idx = super::bindings::register_table_element( - module, - section, - descriptor.shim_idx, - descriptor, - )?; - let a = self.push_wasm(ValType::I32); - let b = self.push_wasm(ValType::I32); - self.webidl.push(ast::WebidlScalarType::Any); - self.bindings.push(NonstandardOutgoing::StackClosure { - a, - b, - binding_idx, - nargs, - mutable, - }); - } - - _ => bail!( - "unsupported reference argument type for calling JS function from Rust: {:?}", - arg - ), - } - Ok(()) - } - - fn process_option(&mut self, arg: &Descriptor) -> Result<(), Error> { - match arg { - Descriptor::Anyref => self.standard_as(ValType::Anyref, ast::WebidlScalarType::Any), - Descriptor::I8 => self.option_sentinel(), - Descriptor::U8 => self.option_sentinel(), - Descriptor::I16 => self.option_sentinel(), - Descriptor::U16 => self.option_sentinel(), - Descriptor::I32 => self.option_native(true, ValType::I32), - Descriptor::U32 => self.option_native(false, ValType::I32), - Descriptor::F32 => self.option_native(true, ValType::F32), - Descriptor::F64 => self.option_native(true, ValType::F64), - Descriptor::I64 | Descriptor::U64 => { - let signed = match arg { - Descriptor::I64 => true, - _ => false, - }; - let binding = NonstandardOutgoing::OptionInt64 { - present: self.push_wasm(ValType::I32), - _ignored: self.push_wasm(ValType::I32), - lo: self.push_wasm(ValType::I32), - hi: self.push_wasm(ValType::I32), - signed, - }; - self.webidl.push(ast::WebidlScalarType::Any); - self.bindings.push(binding); - } - Descriptor::Boolean => { - let idx = self.push_wasm(ValType::I32); - self.webidl.push(ast::WebidlScalarType::Any); - self.bindings.push(NonstandardOutgoing::OptionBool { idx }); - } - Descriptor::Char => { - let idx = self.push_wasm(ValType::I32); - self.webidl.push(ast::WebidlScalarType::Any); - self.bindings.push(NonstandardOutgoing::OptionChar { idx }); - } - Descriptor::Enum { hole } => { - let idx = self.push_wasm(ValType::I32); - self.webidl.push(ast::WebidlScalarType::Long); - self.bindings - .push(NonstandardOutgoing::OptionIntegerEnum { idx, hole: *hole }); - } - Descriptor::RustStruct(name) => { - let idx = self.push_wasm(ValType::I32); - self.webidl.push(ast::WebidlScalarType::Any); - self.bindings.push(NonstandardOutgoing::OptionRustType { - idx, - class: name.to_string(), - }); - } - Descriptor::Ref(d) => self.process_option_ref(false, d)?, - Descriptor::RefMut(d) => self.process_option_ref(true, d)?, - - Descriptor::CachedString => self.cached_string(true, true), - - Descriptor::String | Descriptor::Vector(_) => { - let kind = arg.vector_kind().ok_or_else(|| { - format_err!( - "unsupported optional slice type for calling JS function from Rust {:?}", - arg - ) - })?; - let offset = self.push_wasm(ValType::I32); - let length = self.push_wasm(ValType::I32); - self.webidl.push(ast::WebidlScalarType::Any); - self.bindings.push(NonstandardOutgoing::OptionVector { - kind, - offset, - length, - }) - } - - _ => bail!( - "unsupported optional argument type for calling JS function from Rust: {:?}", - arg - ), - } - Ok(()) - } - - fn process_option_ref(&mut self, _mutable: bool, arg: &Descriptor) -> Result<(), Error> { - match arg { - Descriptor::Anyref => { - let idx = self.push_wasm(ValType::Anyref); - self.webidl.push(ast::WebidlScalarType::Any); - self.bindings - .push(NonstandardOutgoing::BorrowedAnyref { idx }); - } - Descriptor::CachedString => self.cached_string(true, false), - Descriptor::String | Descriptor::Slice(_) => { - let kind = arg.vector_kind().ok_or_else(|| { - format_err!( - "unsupported optional slice type for calling JS function from Rust {:?}", - arg - ) - })?; - let offset = self.push_wasm(ValType::I32); - let length = self.push_wasm(ValType::I32); - self.webidl.push(ast::WebidlScalarType::Any); - self.bindings.push(NonstandardOutgoing::OptionSlice { - kind, - offset, - length, - }); - } - _ => bail!( - "unsupported optional ref argument type for calling JS function from Rust: {:?}", - arg - ), - } - Ok(()) - } - - fn push_wasm(&mut self, ty: ValType) -> u32 { - self.wasm.push(ty); - self.wasm.len() as u32 - 1 - } - - fn standard_as(&mut self, wasm: ValType, webidl: ast::WebidlScalarType) { - let binding = ast::OutgoingBindingExpressionAs { - ty: webidl.into(), - idx: self.push_wasm(wasm), - }; - self.webidl.push(webidl); - self.bindings - .push(NonstandardOutgoing::Standard(binding.into())); - } - - fn standard_view(&mut self, offset: u32, length: u32, ty: ast::WebidlScalarType) { - let binding = ast::OutgoingBindingExpressionView { - ty: ty.into(), - offset, - length, - }; - self.webidl.push(ty); - self.bindings - .push(NonstandardOutgoing::Standard(binding.into())); - } - - fn cached_string(&mut self, optional: bool, owned: bool) { - let offset = self.push_wasm(ValType::I32); - let length = self.push_wasm(ValType::I32); - self.webidl.push(ast::WebidlScalarType::DomString); - self.bindings.push(NonstandardOutgoing::CachedString { - offset, - length, - owned, - optional, - }) - } - - fn option_native(&mut self, signed: bool, ty: ValType) { - let present = self.push_wasm(ValType::I32); - let val = self.push_wasm(ty); - self.webidl.push(ast::WebidlScalarType::Any); - self.bindings.push(NonstandardOutgoing::OptionNative { - signed, - present, - val, - }); - } - - fn option_sentinel(&mut self) { - let idx = self.push_wasm(ValType::I32); - self.webidl.push(ast::WebidlScalarType::Any); - self.bindings - .push(NonstandardOutgoing::OptionU32Sentinel { idx }); - } -} diff --git a/crates/cli-support/src/webidl/standard.rs b/crates/cli-support/src/webidl/standard.rs deleted file mode 100644 index 228977ee..00000000 --- a/crates/cli-support/src/webidl/standard.rs +++ /dev/null @@ -1,562 +0,0 @@ -//! Support for generating a standard WebIDL custom section -//! -//! This module has all the necessary support for generating a full-fledged -//! standard WebIDL custom section as defined by the `wasm-webidl-bindings` -//! crate. This module also critically assumes that the WebAssembly module -//! being generated **must be standalone**. In this mode all sorts of features -//! supported by `#[wasm_bindgen]` aren't actually supported, such as closures, -//! imports of global js names, js getters/setters, exporting structs, etc. -//! These features may all eventually come to the standard bindings proposal, -//! but it will likely take some time. In the meantime this module simply focuses -//! on taking what's already a valid wasm module and letting it through with a -//! standard WebIDL custom section. All other modules generate an error during -//! this binding process. -//! -//! Note that when this function is called and used we're also not actually -//! generating any JS glue. Any JS glue currently generated is also invalid if -//! the module contains the wasm bindings section and it's actually respected. - -use crate::descriptor::VectorKind; -use crate::webidl::{AuxExportKind, AuxImport, AuxValue, JsImport, JsImportName}; -use crate::webidl::{NonstandardIncoming, NonstandardOutgoing}; -use crate::webidl::{NonstandardWebidlSection, WasmBindgenAux}; -use anyhow::{bail, Context, Error}; -use walrus::Module; -use wasm_bindgen_multi_value_xform as multi_value_xform; -use wasm_bindgen_wasm_conventions as wasm_conventions; -use wasm_webidl_bindings::ast; - -pub fn add_multi_value( - module: &mut Module, - bindings: &mut NonstandardWebidlSection, -) -> Result<(), Error> { - let mut to_xform = vec![]; - for (id, binding) in &bindings.exports { - if let Some(ref results) = binding.return_via_outptr { - // LLVM currently always uses the first parameter for the return - // pointer. We hard code that here, since we have no better option. - let return_pointer_index = 0; - to_xform.push((*id, return_pointer_index, &results[..])); - } - } - - if to_xform.is_empty() { - // Early exit to avoid failing if we don't have a memory or shadow stack - // pointer because this is a minimal module that doesn't use linear - // memory. - return Ok(()); - } - - let shadow_stack_pointer = wasm_conventions::get_shadow_stack_pointer(module)?; - let memory = wasm_conventions::get_memory(module)?; - multi_value_xform::run(module, memory, shadow_stack_pointer, &to_xform)?; - - // Finally, unset `return_via_outptr`, fix up its incoming bindings' - // argument numberings, and update its function type. - for (id, binding) in &mut bindings.exports { - if binding.return_via_outptr.take().is_none() { - continue; - } - if binding.incoming.is_empty() { - bail!("missing incoming binding expression for return pointer parameter"); - } - if !is_ret_ptr_bindings(binding.incoming.remove(0)) { - bail!("unexpected incoming binding expression for return pointer parameter"); - } - - fixup_binding_argument_gets(&mut binding.incoming)?; - - let func = match module.exports.get(*id).item { - walrus::ExportItem::Function(f) => f, - _ => unreachable!(), - }; - binding.wasm_ty = module.funcs.get(func).ty(); - - // Be sure to delete the out-param pointer from the WebIDL type as well. - let webidl_ty = bindings - .types - .get::(binding.webidl_ty) - .unwrap(); - let mut new_ty = webidl_ty.clone(); - new_ty.params.remove(0); - binding.webidl_ty = bindings.types.insert(new_ty); - } - - Ok(()) -} - -fn is_ret_ptr_bindings(b: NonstandardIncoming) -> bool { - match b { - NonstandardIncoming::Standard(ast::IncomingBindingExpression::As( - ast::IncomingBindingExpressionAs { - ty: walrus::ValType::I32, - expr, - }, - )) => match *expr { - ast::IncomingBindingExpression::Get(ast::IncomingBindingExpressionGet { idx: 0 }) => { - true - } - _ => false, - }, - _ => false, - } -} - -// Since we removed the first parameter (which was the return pointer) now all -// of the `Get` binding expression's are off by one. This function fixes these -// `Get`s. -fn fixup_binding_argument_gets(incoming: &mut [NonstandardIncoming]) -> Result<(), Error> { - for inc in incoming { - fixup_nonstandard_incoming(inc)?; - } - return Ok(()); - - fn fixup_nonstandard_incoming(inc: &mut NonstandardIncoming) -> Result<(), Error> { - match inc { - NonstandardIncoming::Standard(s) => fixup_standard_incoming(s), - _ => bail!("found usage of non-standard bindings when in standard-bindings-only mode"), - } - } - - fn fixup_standard_incoming(s: &mut ast::IncomingBindingExpression) -> Result<(), Error> { - match s { - ast::IncomingBindingExpression::Get(e) => { - if e.idx == 0 { - bail!( - "found usage of removed return pointer parameter in \ - non-return pointer bindings" - ); - } else { - e.idx -= 1; - Ok(()) - } - } - ast::IncomingBindingExpression::As(e) => fixup_standard_incoming(&mut e.expr), - ast::IncomingBindingExpression::AllocUtf8Str(e) => fixup_standard_incoming(&mut e.expr), - ast::IncomingBindingExpression::AllocCopy(e) => fixup_standard_incoming(&mut e.expr), - ast::IncomingBindingExpression::EnumToI32(e) => fixup_standard_incoming(&mut e.expr), - ast::IncomingBindingExpression::Field(e) => fixup_standard_incoming(&mut e.expr), - ast::IncomingBindingExpression::BindImport(e) => fixup_standard_incoming(&mut e.expr), - } - } -} - -pub fn add_section( - module: &mut Module, - aux: &WasmBindgenAux, - nonstandard: &NonstandardWebidlSection, -) -> Result<(), Error> { - let mut section = ast::WebidlBindings::default(); - let WasmBindgenAux { - extra_typescript: _, // ignore this even if it's specified - local_modules, - snippets, - package_jsons, - export_map, - import_map, - imports_with_catch, - imports_with_variadic, - imports_with_assert_no_shim: _, // not relevant for this purpose - enums, - structs, - } = aux; - - for (export, binding) in nonstandard.exports.iter() { - // First up make sure this is something that's actually valid to export - // form a vanilla WebAssembly module with WebIDL bindings. - match &export_map[export].kind { - AuxExportKind::Function(_) => {} - AuxExportKind::Constructor(name) => { - bail!( - "cannot export `{}` constructor function when generating \ - a standalone WebAssembly module with no JS glue", - name, - ); - } - AuxExportKind::Getter { class, field } => { - bail!( - "cannot export `{}::{}` getter function when generating \ - a standalone WebAssembly module with no JS glue", - class, - field, - ); - } - AuxExportKind::Setter { class, field } => { - bail!( - "cannot export `{}::{}` setter function when generating \ - a standalone WebAssembly module with no JS glue", - class, - field, - ); - } - AuxExportKind::StaticFunction { class, name } => { - bail!( - "cannot export `{}::{}` static function when \ - generating a standalone WebAssembly module with no \ - JS glue", - class, - name - ); - } - AuxExportKind::Method { class, name, .. } => { - bail!( - "cannot export `{}::{}` method when \ - generating a standalone WebAssembly module with no \ - JS glue", - class, - name - ); - } - } - - let name = &module.exports.get(*export).name; - let params = extract_incoming(&binding.incoming).with_context(|| { - format!( - "failed to map arguments for export `{}` to standard \ - binding expressions", - name - ) - })?; - let result = extract_outgoing(&binding.outgoing).with_context(|| { - format!( - "failed to map return value for export `{}` to standard \ - binding expressions", - name - ) - })?; - - assert!(binding.return_via_outptr.is_none()); - let binding = section.bindings.insert(ast::ExportBinding { - wasm_ty: binding.wasm_ty, - webidl_ty: copy_ty( - &mut section.types, - binding.webidl_ty.into(), - &nonstandard.types, - ), - params: ast::IncomingBindingMap { bindings: params }, - result: ast::OutgoingBindingMap { bindings: result }, - }); - let func = match module.exports.get(*export).item { - walrus::ExportItem::Function(f) => f, - _ => unreachable!(), - }; - section.binds.insert(ast::Bind { - func, - binding: binding.into(), - }); - } - - for (import, binding) in nonstandard.imports.iter() { - check_standard_import(&import_map[import])?; - let (module_name, name) = { - let import = module.imports.get(*import); - (&import.module, &import.name) - }; - let params = extract_outgoing(&binding.outgoing).with_context(|| { - format!( - "failed to map arguments of import `{}::{}` to standard \ - binding expressions", - module_name, name, - ) - })?; - let result = extract_incoming(&binding.incoming).with_context(|| { - format!( - "failed to map return value of import `{}::{}` to standard \ - binding expressions", - module_name, name, - ) - })?; - assert!(binding.return_via_outptr.is_none()); - let binding = section.bindings.insert(ast::ImportBinding { - wasm_ty: binding.wasm_ty, - webidl_ty: copy_ty( - &mut section.types, - binding.webidl_ty.into(), - &nonstandard.types, - ), - params: ast::OutgoingBindingMap { bindings: params }, - result: ast::IncomingBindingMap { bindings: result }, - }); - let func = match module.imports.get(*import).kind { - walrus::ImportKind::Function(f) => f, - _ => unreachable!(), - }; - section.binds.insert(ast::Bind { - func, - binding: binding.into(), - }); - } - - if let Some((name, _)) = local_modules.iter().next() { - bail!( - "generating a bindings section is currently incompatible with \ - local JS modules being specified as well, `{}` cannot be used \ - since a standalone wasm file is being generated", - name, - ); - } - - if let Some((name, _)) = snippets.iter().filter(|(_, v)| !v.is_empty()).next() { - bail!( - "generating a bindings section is currently incompatible with \ - local JS snippets being specified as well, `{}` cannot be used \ - since a standalone wasm file is being generated", - name, - ); - } - - if let Some(path) = package_jsons.iter().next() { - bail!( - "generating a bindings section is currently incompatible with \ - package.json being consumed as well, `{}` cannot be used \ - since a standalone wasm file is being generated", - path.display(), - ); - } - - if let Some(import) = imports_with_catch.iter().next() { - let import = module.imports.get(*import); - bail!( - "generating a bindings section is currently incompatible with \ - `#[wasm_bindgen(catch)]` on the `{}::{}` import because a \ - a standalone wasm file is being generated", - import.module, - import.name, - ); - } - - if let Some(import) = imports_with_variadic.iter().next() { - let import = module.imports.get(*import); - bail!( - "generating a bindings section is currently incompatible with \ - `#[wasm_bindgen(variadic)]` on the `{}::{}` import because a \ - a standalone wasm file is being generated", - import.module, - import.name, - ); - } - - if let Some(enum_) = enums.iter().next() { - bail!( - "generating a bindings section is currently incompatible with \ - exporting an `enum` from the wasm file, cannot export `{}`", - enum_.name, - ); - } - - if let Some(struct_) = structs.iter().next() { - bail!( - "generating a bindings section is currently incompatible with \ - exporting a `struct` from the wasm file, cannot export `{}`", - struct_.name, - ); - } - - if nonstandard.elems.len() > 0 { - // Note that this is a pretty cryptic error message, but we in theory - // shouldn't ever hit this since closures always show up as some form - // of nonstandard binding which was previously checked. - bail!("generating a standalone wasm file requires no table element bindings"); - } - - module.customs.add(section); - Ok(()) -} - -fn extract_incoming( - nonstandard: &[NonstandardIncoming], -) -> Result, Error> { - let mut exprs = Vec::new(); - for expr in nonstandard { - let desc = match expr { - NonstandardIncoming::Standard(e) => { - exprs.push(e.clone()); - continue; - } - NonstandardIncoming::Int64 { .. } => "64-bit integer", - NonstandardIncoming::AllocCopyInt64 { .. } => "64-bit integer array", - NonstandardIncoming::AllocCopyAnyrefArray { .. } => "array of JsValue", - NonstandardIncoming::MutableSlice { .. } => "mutable slice", - NonstandardIncoming::OptionSlice { .. } => "optional slice", - NonstandardIncoming::OptionVector { .. } => "optional vector", - NonstandardIncoming::OptionAnyref { .. } => "optional anyref", - NonstandardIncoming::OptionNative { .. } => "optional integer", - NonstandardIncoming::OptionU32Sentinel { .. } => "optional integer", - NonstandardIncoming::OptionBool { .. } => "optional bool", - NonstandardIncoming::OptionChar { .. } => "optional char", - NonstandardIncoming::OptionIntegerEnum { .. } => "optional enum", - NonstandardIncoming::OptionInt64 { .. } => "optional integer", - NonstandardIncoming::RustType { .. } => "native Rust type", - NonstandardIncoming::RustTypeRef { .. } => "reference to Rust type", - NonstandardIncoming::OptionRustType { .. } => "optional Rust type", - NonstandardIncoming::Char { .. } => "character", - NonstandardIncoming::BorrowedAnyref { .. } => "borrowed anyref", - }; - bail!( - "cannot represent {} with a standard bindings expression", - desc - ); - } - Ok(exprs) -} - -fn extract_outgoing( - nonstandard: &[NonstandardOutgoing], -) -> Result, Error> { - let mut exprs = Vec::new(); - for expr in nonstandard { - let desc = match expr { - NonstandardOutgoing::Standard(e) => { - exprs.push(e.clone()); - continue; - } - // ... yeah ... let's just leak strings - // see comment at top of this module about returning strings for - // what this is doing and why it's weird - NonstandardOutgoing::Vector { - offset, - length, - kind: VectorKind::String, - } => { - exprs.push( - ast::OutgoingBindingExpressionUtf8Str { - offset: *offset, - length: *length, - ty: ast::WebidlScalarType::DomString.into(), - } - .into(), - ); - continue; - } - - NonstandardOutgoing::RustType { .. } => "rust type", - NonstandardOutgoing::Char { .. } => "character", - NonstandardOutgoing::Number64 { .. } => "64-bit integer", - NonstandardOutgoing::BorrowedAnyref { .. } => "borrowed anyref", - NonstandardOutgoing::Vector { .. } => "vector", - NonstandardOutgoing::CachedString { .. } => "cached string", - NonstandardOutgoing::View64 { .. } => "64-bit slice", - NonstandardOutgoing::ViewAnyref { .. } => "anyref slice", - NonstandardOutgoing::OptionVector { .. } => "optional vector", - NonstandardOutgoing::OptionSlice { .. } => "optional slice", - NonstandardOutgoing::OptionNative { .. } => "optional integer", - NonstandardOutgoing::OptionU32Sentinel { .. } => "optional integer", - NonstandardOutgoing::OptionBool { .. } => "optional boolean", - NonstandardOutgoing::OptionChar { .. } => "optional character", - NonstandardOutgoing::OptionIntegerEnum { .. } => "optional enum", - NonstandardOutgoing::OptionInt64 { .. } => "optional 64-bit integer", - NonstandardOutgoing::OptionRustType { .. } => "optional rust type", - NonstandardOutgoing::StackClosure { .. } => "closures", - }; - bail!( - "cannot represent {} with a standard bindings expression", - desc - ); - } - Ok(exprs) -} - -/// Recursively clones `ty` into` dst` where it originally indexes values in -/// `src`, returning a new type ref which indexes inside of `dst`. -pub fn copy_ty( - dst: &mut ast::WebidlTypes, - ty: ast::WebidlTypeRef, - src: &ast::WebidlTypes, -) -> ast::WebidlTypeRef { - let id = match ty { - ast::WebidlTypeRef::Id(id) => id, - ast::WebidlTypeRef::Scalar(_) => return ty, - }; - let ty: &ast::WebidlCompoundType = src.get(id).unwrap(); - match ty { - ast::WebidlCompoundType::Function(f) => { - let params = f - .params - .iter() - .map(|param| copy_ty(dst, *param, src)) - .collect(); - let result = f.result.map(|ty| copy_ty(dst, ty, src)); - dst.insert(ast::WebidlFunction { - kind: f.kind.clone(), - params, - result, - }) - .into() - } - _ => unimplemented!(), - } -} - -fn check_standard_import(import: &AuxImport) -> Result<(), Error> { - let desc_js = |js: &JsImport| { - let mut extra = String::new(); - for field in js.fields.iter() { - extra.push_str("."); - extra.push_str(field); - } - match &js.name { - JsImportName::Global { name } | JsImportName::VendorPrefixed { name, .. } => { - format!("global `{}{}`", name, extra) - } - JsImportName::Module { module, name } => { - format!("`{}{}` from '{}'", name, extra, module) - } - JsImportName::LocalModule { module, name } => { - format!("`{}{}` from local module '{}'", name, extra, module) - } - JsImportName::InlineJs { - unique_crate_identifier, - name, - .. - } => format!( - "`{}{}` from inline js in '{}'", - name, extra, unique_crate_identifier - ), - } - }; - - let item = match import { - AuxImport::Value(AuxValue::Bare(js)) => { - if js.fields.len() == 0 { - if let JsImportName::Module { .. } = js.name { - return Ok(()); - } - } - desc_js(js) - } - AuxImport::Value(AuxValue::Getter(js, name)) - | AuxImport::Value(AuxValue::Setter(js, name)) - | AuxImport::Value(AuxValue::ClassGetter(js, name)) - | AuxImport::Value(AuxValue::ClassSetter(js, name)) => { - format!("field access of `{}` for {}", name, desc_js(js)) - } - AuxImport::ValueWithThis(js, method) => format!("method `{}.{}`", desc_js(js), method), - AuxImport::Instanceof(js) => format!("instance of check of {}", desc_js(js)), - AuxImport::Static(js) => format!("static js value {}", desc_js(js)), - AuxImport::StructuralMethod(name) => format!("structural method `{}`", name), - AuxImport::StructuralGetter(name) - | AuxImport::StructuralSetter(name) - | AuxImport::StructuralClassGetter(_, name) - | AuxImport::StructuralClassSetter(_, name) => { - format!("structural field access of `{}`", name) - } - AuxImport::IndexingDeleterOfClass(_) - | AuxImport::IndexingDeleterOfObject - | AuxImport::IndexingGetterOfClass(_) - | AuxImport::IndexingGetterOfObject - | AuxImport::IndexingSetterOfClass(_) - | AuxImport::IndexingSetterOfObject => format!("indexing getters/setters/deleters"), - AuxImport::WrapInExportedClass(name) => { - format!("wrapping a pointer in a `{}` js class wrapper", name) - } - AuxImport::Intrinsic(intrinsic) => { - format!("wasm-bindgen specific intrinsic `{}`", intrinsic.name()) - } - AuxImport::Closure { .. } => format!("creating a `Closure` wrapper"), - }; - bail!( - "cannot generate a standalone WebAssembly module which \ - contains an import of {} since it requires JS glue", - item - ); -} diff --git a/crates/cli-support/src/wit/incoming.rs b/crates/cli-support/src/wit/incoming.rs new file mode 100644 index 00000000..dc00b90c --- /dev/null +++ b/crates/cli-support/src/wit/incoming.rs @@ -0,0 +1,385 @@ +//! Definition of how to convert Rust types (`Description`) into wasm types +//! through adapter functions. +//! +//! Note that many Rust types use "nonstandard" instructions which only work in +//! the JS output, not for the "pure wasm interface types" output. +//! +//! Note that the mirror operation, going from WebAssembly to JS, is found in +//! the `outgoing.rs` module. + +use crate::descriptor::Descriptor; +use crate::wit::InstructionData; +use crate::wit::{AdapterType, Instruction, InstructionBuilder, StackChange}; +use anyhow::{bail, format_err, Error}; +use walrus::ValType; + +impl InstructionBuilder<'_, '_> { + /// Process a `Descriptor` as if it's being passed from JS to Rust. This + /// will skip `Unit` and otherwise internally add instructions necessary to + /// convert the foreign type into the Rust bits. + pub fn incoming(&mut self, arg: &Descriptor) -> Result<(), Error> { + if let Descriptor::Unit = arg { + return Ok(()); + } + // This is a wrapper around `_incoming` to have a number of sanity checks + // that we don't forget things. We should always produce at least one + // wasm arge and exactly one webidl arg. Additionally the number of + // bindings should always match the number of webidl types for now. + let input_before = self.input.len(); + let output_before = self.output.len(); + self._incoming(arg)?; + assert_eq!( + input_before + 1, + self.input.len(), + "didn't push an input {:?}", + arg + ); + assert!( + output_before < self.output.len(), + "didn't push more outputs {:?}", + arg + ); + Ok(()) + } + + fn _incoming(&mut self, arg: &Descriptor) -> Result<(), Error> { + use walrus::ValType as WasmVT; + use wit_walrus::ValType as WitVT; + match arg { + Descriptor::Boolean => { + self.instruction( + &[AdapterType::Bool], + Instruction::I32FromBool, + &[AdapterType::I32], + ); + } + Descriptor::Char => { + self.instruction( + &[AdapterType::String], + Instruction::I32FromStringFirstChar, + &[AdapterType::I32], + ); + } + Descriptor::Anyref => { + self.instruction( + &[AdapterType::Anyref], + Instruction::I32FromAnyrefOwned, + &[AdapterType::I32], + ); + } + Descriptor::RustStruct(class) => { + self.instruction( + &[AdapterType::Anyref], + Instruction::I32FromAnyrefRustOwned { + class: class.clone(), + }, + &[AdapterType::I32], + ); + } + Descriptor::I8 => self.number(WitVT::S8, WasmVT::I32), + Descriptor::U8 => self.number(WitVT::U8, WasmVT::I32), + Descriptor::I16 => self.number(WitVT::S16, WasmVT::I32), + Descriptor::U16 => self.number(WitVT::U16, WasmVT::I32), + Descriptor::I32 => self.number(WitVT::S32, WasmVT::I32), + Descriptor::U32 => self.number(WitVT::U32, WasmVT::I32), + Descriptor::I64 => self.number64(true), + Descriptor::U64 => self.number64(false), + Descriptor::F32 => { + self.get(AdapterType::F32); + self.output.push(AdapterType::F32); + } + Descriptor::F64 => { + self.get(AdapterType::F64); + self.output.push(AdapterType::F64); + } + Descriptor::Enum { .. } => self.number(WitVT::U32, WasmVT::I32), + Descriptor::Ref(d) => self.incoming_ref(false, d)?, + Descriptor::RefMut(d) => self.incoming_ref(true, d)?, + Descriptor::Option(d) => self.incoming_option(d)?, + + Descriptor::String | Descriptor::CachedString => { + let std = wit_walrus::Instruction::StringToMemory { + malloc: self.cx.malloc()?, + mem: self.cx.memory()?, + }; + self.instruction( + &[AdapterType::String], + Instruction::Standard(std), + &[AdapterType::I32, AdapterType::I32], + ); + } + + Descriptor::Vector(_) => { + let kind = arg.vector_kind().ok_or_else(|| { + format_err!("unsupported argument type for calling Rust function from JS {:?}", arg) + })?; + self.instruction( + &[AdapterType::Vector(kind)], + Instruction::VectorToMemory { + kind, + malloc: self.cx.malloc()?, + mem: self.cx.memory()?, + }, + &[AdapterType::I32, AdapterType::I32], + ); + } + + // Can't be passed from JS to Rust yet + Descriptor::Function(_) | + Descriptor::Closure(_) | + + // Always behind a `Ref` + Descriptor::Slice(_) => bail!( + "unsupported argument type for calling Rust function from JS: {:?}", + arg + ), + + // nothing to do + Descriptor::Unit => {} + + // Largely synthetic and can't show up + Descriptor::ClampedU8 => unreachable!(), + } + Ok(()) + } + + fn incoming_ref(&mut self, mutable: bool, arg: &Descriptor) -> Result<(), Error> { + match arg { + Descriptor::RustStruct(class) => { + self.instruction( + &[AdapterType::Anyref], + Instruction::I32FromAnyrefRustBorrow { + class: class.clone(), + }, + &[AdapterType::I32], + ); + } + Descriptor::Anyref => { + self.instruction( + &[AdapterType::Anyref], + Instruction::I32FromAnyrefBorrow, + &[AdapterType::I32], + ); + } + Descriptor::String | Descriptor::CachedString => { + // This allocation is cleaned up once it's received in Rust. + let std = wit_walrus::Instruction::StringToMemory { + malloc: self.cx.malloc()?, + mem: self.cx.memory()?, + }; + self.instruction( + &[AdapterType::String], + Instruction::Standard(std), + &[AdapterType::I32, AdapterType::I32], + ); + } + Descriptor::Slice(_) => { + // like strings, this allocation is cleaned up after being + // received in Rust. + let kind = arg.vector_kind().ok_or_else(|| { + format_err!( + "unsupported argument type for calling Rust function from JS {:?}", + arg + ) + })?; + if mutable { + self.instruction( + &[AdapterType::Vector(kind)], + Instruction::MutableSliceToMemory { + kind, + malloc: self.cx.malloc()?, + mem: self.cx.memory()?, + free: self.cx.free()?, + }, + &[AdapterType::I32, AdapterType::I32], + ); + } else { + self.instruction( + &[AdapterType::Vector(kind)], + Instruction::VectorToMemory { + kind, + malloc: self.cx.malloc()?, + mem: self.cx.memory()?, + }, + &[AdapterType::I32, AdapterType::I32], + ); + } + } + _ => bail!( + "unsupported reference argument type for calling Rust function from JS: {:?}", + arg + ), + } + Ok(()) + } + + fn incoming_option(&mut self, arg: &Descriptor) -> Result<(), Error> { + match arg { + Descriptor::Anyref => { + self.instruction( + &[AdapterType::Anyref], + Instruction::I32FromOptionAnyref, + &[AdapterType::I32], + ); + } + Descriptor::I8 => self.in_option_sentinel(), + Descriptor::U8 => self.in_option_sentinel(), + Descriptor::I16 => self.in_option_sentinel(), + Descriptor::U16 => self.in_option_sentinel(), + Descriptor::I32 => self.in_option_native(ValType::I32), + Descriptor::U32 => self.in_option_native(ValType::I32), + Descriptor::F32 => self.in_option_native(ValType::F32), + Descriptor::F64 => self.in_option_native(ValType::F64), + Descriptor::I64 | Descriptor::U64 => { + let signed = match arg { + Descriptor::I64 => true, + _ => false, + }; + self.instruction( + &[AdapterType::Anyref], + Instruction::I32SplitOption64 { signed }, + &[AdapterType::I32; 3], + ); + } + Descriptor::Boolean => { + self.instruction( + &[AdapterType::Anyref], + Instruction::I32FromOptionBool, + &[AdapterType::I32], + ); + } + Descriptor::Char => { + self.instruction( + &[AdapterType::Anyref], + Instruction::I32FromOptionChar, + &[AdapterType::I32], + ); + } + Descriptor::Enum { hole } => { + self.instruction( + &[AdapterType::Anyref], + Instruction::I32FromOptionEnum { hole: *hole }, + &[AdapterType::I32], + ); + } + Descriptor::RustStruct(name) => { + self.instruction( + &[AdapterType::Anyref], + Instruction::I32FromOptionRust { + class: name.to_string(), + }, + &[AdapterType::I32], + ); + } + + Descriptor::String | Descriptor::CachedString | Descriptor::Vector(_) => { + let kind = arg.vector_kind().ok_or_else(|| { + format_err!( + "unsupported optional slice type for calling Rust function from JS {:?}", + arg + ) + })?; + let malloc = self.cx.malloc()?; + let mem = self.cx.memory()?; + self.instruction( + &[AdapterType::Anyref], + Instruction::OptionVector { kind, malloc, mem }, + &[AdapterType::I32; 2], + ); + } + + _ => bail!( + "unsupported optional argument type for calling Rust function from JS: {:?}", + arg + ), + } + Ok(()) + } + + pub fn get(&mut self, ty: AdapterType) { + self.input.push(ty); + + // If we're generating instructions in the return position then the + // arguments are already on the stack to consume, otherwise we need to + // fetch them from the parameters. + if !self.return_position { + let idx = self.input.len() as u32 - 1; + let std = wit_walrus::Instruction::ArgGet(idx); + self.instructions.push(InstructionData { + instr: Instruction::Standard(std), + stack_change: StackChange::Modified { + pushed: 1, + popped: 0, + }, + }); + } + } + + pub fn instruction( + &mut self, + inputs: &[AdapterType], + instr: Instruction, + outputs: &[AdapterType], + ) { + // If we're generating instructions in the return position then the + // arguments are already on the stack to consume, otherwise we need to + // fetch them from the parameters. + if !self.return_position { + for input in inputs { + self.get(*input); + } + } else { + self.input.extend_from_slice(inputs); + } + + self.instructions.push(InstructionData { + instr, + stack_change: StackChange::Modified { + popped: inputs.len(), + pushed: outputs.len(), + }, + }); + self.output.extend_from_slice(outputs); + } + + fn number(&mut self, input: wit_walrus::ValType, output: walrus::ValType) { + let std = wit_walrus::Instruction::IntToWasm { + input, + output, + trap: false, + }; + self.instruction( + &[AdapterType::from_wit(input)], + Instruction::Standard(std), + &[AdapterType::from_wasm(output).unwrap()], + ); + } + + fn number64(&mut self, signed: bool) { + self.instruction( + &[if signed { + AdapterType::S64 + } else { + AdapterType::U64 + }], + Instruction::I32Split64 { signed }, + &[AdapterType::I32, AdapterType::I32], + ); + } + + fn in_option_native(&mut self, wasm: ValType) { + self.instruction( + &[AdapterType::Anyref], + Instruction::FromOptionNative { ty: wasm }, + &[AdapterType::I32, AdapterType::from_wasm(wasm).unwrap()], + ); + } + + fn in_option_sentinel(&mut self) { + self.instruction( + &[AdapterType::Anyref], + Instruction::I32FromOptionU32Sentinel, + &[AdapterType::I32], + ); + } +} diff --git a/crates/cli-support/src/webidl/mod.rs b/crates/cli-support/src/wit/mod.rs similarity index 51% rename from crates/cli-support/src/webidl/mod.rs rename to crates/cli-support/src/wit/mod.rs index da0ae730..2368d9e8 100644 --- a/crates/cli-support/src/webidl/mod.rs +++ b/crates/cli-support/src/wit/mod.rs @@ -1,482 +1,32 @@ -//! The polyfill for the WebIDL bindings proposal in wasm-bindgen. -//! -//! This module contains the polyfill (or at least the current state of and as -//! closely as we can match) the WebIDL bindings proposal. The module exports -//! one main function, `process`, which takes a `walrus::Module`. This module is -//! expected to have two items: -//! -//! * First it contains all of the raw wasm-bindgen modules emitted by the Rust -//! compiler. These raw custom sections are extracted, removed, decoded, and -//! handled here. They contain information such as what's exported where, -//! what's imported, comments, etc. -//! * Second, the `descriptors.rs` pass must have run previously to execute all -//! the descriptor functions in the wasm module. Through the synthesized -//! custom section there we learn the type information of all -//! functions/imports/exports in the module. -//! -//! The output of this function is then a new `walrus::Module` with the previous -//! custom sections all removed and two new ones inserted. One is the webidl -//! bindings custom section (or at least a close approximate) and the second is -//! an auxiliary section for wasm-bindgen itself. The goal is for this auxiliary -//! section to eventually be empty or inconsequential, allowing us to emit -//! something that doesn't even need a JS shim one day. For now we're still -//! pretty far away from that, so we'll settle for using webidl bindings as -//! aggressively as possible! - use crate::decode; use crate::descriptor::{Descriptor, Function}; use crate::descriptors::WasmBindgenDescriptorsSection; use crate::intrinsic::Intrinsic; use anyhow::{anyhow, bail, Error}; -use std::borrow::Cow; -use std::collections::{HashMap, HashSet}; -use std::path::PathBuf; +use std::collections::HashMap; use std::str; -use walrus::{ExportId, FunctionId, ImportId, Module, TypedCustomSectionId}; +use walrus::MemoryId; +use walrus::{ExportId, FunctionId, ImportId, Module}; use wasm_bindgen_shared::struct_function_export_name; -use wasm_webidl_bindings::ast; const PLACEHOLDER_MODULE: &str = "__wbindgen_placeholder__"; -mod bindings; mod incoming; +mod nonstandard; mod outgoing; -pub mod standard; - -pub use self::incoming::NonstandardIncoming; -pub use self::outgoing::NonstandardOutgoing; - -/// A nonstandard wasm-bindgen-specific WebIDL custom section. -/// -/// This nonstandard section is intended to convey all information that -/// wasm-bindgen itself needs to know about binding functions. This means that -/// it basically uses `NonstandardIncoming` instead of -/// `IncomingBindingExpression` and such. It's also in a bit easier to work with -/// format than the official WebIDL bindings custom section. -/// -/// Note that this is intended to be consumed during generation of JS shims and -/// bindings. There it can be transformed, however, into an actual WebIDL -/// binding section using all of the values it has internally. -#[derive(Default, Debug)] -pub struct NonstandardWebidlSection { - /// Store of all WebIDL types. Used currently to store all function types - /// specified in `Bindings`. This is intended to be passed through verbatim - /// to a final WebIDL bindings section. - pub types: ast::WebidlTypes, - - /// A mapping from all bound exported functions to the binding that we have - /// listed for them. This is the master list of every binding that will be - /// bound and have a shim generated for it in the wasm module. - pub exports: HashMap, - - /// Similar to `exports` above, except for imports. This will describe all - /// imports from the wasm module to indicate what the JS shim is expected to - /// do. - pub imports: HashMap, - - /// For closures and such we'll be calling entries in the function table - /// with rich arguments (just like we call exports) so to do that we - /// describe all the elem indices that we need to modify here as well. - /// - /// This is a list of pairs where the first element in the list is the - /// element index in the function table being described and the `Binding` - /// describes the signature that it's supposed to have. - /// - /// The index within this table itself is then used to call actually - /// transformed functions. - pub elems: Vec<(u32, Binding)>, -} - -pub type NonstandardWebidlSectionId = TypedCustomSectionId; - -/// A non-standard wasm-bindgen-specifi WebIDL binding. This is meant to vaguely -/// resemble a `FuctionBinding` in the official WebIDL bindings proposal, or at -/// least make it very easy to manufacture an official value from this one. -#[derive(Debug, Clone)] -pub struct Binding { - /// The WebAssembly type that the function is expected to have. Note that - /// this may not match the actual bound function's type! That's because this - /// type includes `anyref` but the Rust compiler never emits anyref. This - /// is, however, used for the `anyref` pass to know what to transform to - /// `anyref`. - pub wasm_ty: walrus::TypeId, - - /// The WebIDL type of this binding, which is an index into the webidl - /// binding section's `types` field. - pub webidl_ty: ast::WebidlFunctionId, - - /// A list of incoming bindings. For exports this is the list of arguments, - /// and for imports this is the return value. - pub incoming: Vec, - - /// A list of outgoing bindings. For exports this is the return value and - /// for imports this is the list of arguments. - pub outgoing: Vec, - - /// An unfortunate necessity of today's implementation. Ideally WebIDL - /// bindings are used with multi-value support in wasm everywhere, but today - /// few engines support multi-value and LLVM certainly doesn't. Aggregates - /// are then always returned through an out-ptr, so this indicates that if - /// an out-ptr is present what wasm types are being transmitted through it. - pub return_via_outptr: Option>, -} - -impl Binding { - /// Does this binding's wasm function signature have any `anyref`s? - pub fn contains_anyref(&self, module: &walrus::Module) -> bool { - let ty = module.types.get(self.wasm_ty); - ty.params() - .iter() - .chain(ty.results()) - .any(|ty| *ty == walrus::ValType::Anyref) - } -} - -/// A synthetic custom section which is not standardized, never will be, and -/// cannot be serialized or parsed. This is synthesized from all of the -/// compiler-emitted wasm-bindgen sections and then immediately removed to be -/// processed in the JS generation pass. -#[derive(Default, Debug)] -pub struct WasmBindgenAux { - /// Extra typescript annotations that should be appended to the generated - /// TypeScript file. This is provided via a custom attribute in Rust code. - pub extra_typescript: String, - - /// A map from identifier to the contents of each local module defined via - /// the `#[wasm_bindgen(module = "/foo.js")]` import options. - pub local_modules: HashMap, - - /// A map from unique crate identifier to the list of inline JS snippets for - /// that crate identifier. - pub snippets: HashMap>, - - /// A list of all `package.json` files that are intended to be included in - /// the final build. - pub package_jsons: HashSet, - - /// A map from exported function id to where it's expected to be exported - /// to. - pub export_map: HashMap, - - /// A map from imported function id to what it's expected to import. - pub import_map: HashMap, - - /// Small bits of metadata about imports. - pub imports_with_catch: HashSet, - pub imports_with_variadic: HashSet, - pub imports_with_assert_no_shim: HashSet, - - /// Auxiliary information to go into JS/TypeScript bindings describing the - /// exported enums from Rust. - pub enums: Vec, - - /// Auxiliary information to go into JS/TypeScript bindings describing the - /// exported structs from Rust and their fields they've got exported. - pub structs: Vec, -} - -pub type WasmBindgenAuxId = TypedCustomSectionId; - -#[derive(Debug)] -pub struct AuxExport { - /// When generating errors about this export, a helpful name to remember it - /// by. - pub debug_name: String, - /// Comments parsed in Rust and forwarded here to show up in JS bindings. - pub comments: String, - /// Argument names in Rust forwarded here to configure the names that show - /// up in TypeScript bindings. - pub arg_names: Option>, - /// What kind of function this is and where it shows up - pub kind: AuxExportKind, -} - -/// All possible kinds of exports from a wasm module. -/// -/// This `enum` says where to place an exported wasm function. For example it -/// may want to get hooked up to a JS class, or it may want to be exported as a -/// free function (etc). -/// -/// TODO: it feels like this should not really be here per se. We probably want -/// to either construct the JS object itself from within wasm or somehow move -/// more of this information into some other section. Really what this is is -/// sort of an "export map" saying how to wire up all the free functions from -/// the wasm module into the output expected JS module. All our functions here -/// currently take integer parameters and require a JS wrapper, but ideally -/// we'd change them one day to taking/receiving `anyref` which then use some -/// sort of webidl import to customize behavior or something like that. In any -/// case this doesn't feel quite right in terms of priviledge separation, so -/// we'll want to work on this. For now though it works. -#[derive(Debug)] -pub enum AuxExportKind { - /// A free function that's just listed on the exported module - Function(String), - - /// A function that's used to create an instane of a class. The function - /// actually return just an integer which is put on an JS object currently. - Constructor(String), - - /// This function is intended to be a getter for a field on a class. The - /// first argument is the internal pointer and the returned value is - /// expected to be the field. - Getter { class: String, field: String }, - - /// This function is intended to be a setter for a field on a class. The - /// first argument is the internal pointer and the second argument is - /// expected to be the field's new value. - Setter { class: String, field: String }, - - /// This is a free function (ish) but scoped inside of a class name. - StaticFunction { class: String, name: String }, - - /// This is a member function of a class where the first parameter is the - /// implicit integer stored in the class instance. - Method { - class: String, - name: String, - /// Whether or not this is calling a by-value method in Rust and should - /// clear the internal pointer in JS automatically. - consumed: bool, - }, -} - -#[derive(Debug)] -pub struct AuxEnum { - /// The name of this enum - pub name: String, - /// The copied Rust comments to forward to JS - pub comments: String, - /// A list of variants with their name and value - pub variants: Vec<(String, u32)>, -} - -#[derive(Debug)] -pub struct AuxStruct { - /// The name of this struct - pub name: String, - /// The copied Rust comments to forward to JS - pub comments: String, - /// Whether to generate helper methods for inspecting the class - pub is_inspectable: bool, -} - -/// All possible types of imports that can be imported by a wasm module. -/// -/// This `enum` is intended to map out what an imported value is. For example -/// this contains a ton of shims and various ways you can call a function. The -/// base variant here is `Value` which simply means "hook this up to the import" -/// and the signatures will match up. -/// -/// Note that this is *not* the same as the webidl bindings section. This is -/// intended to be coupled with that to map out what actually gets hooked up to -/// an import in the wasm module. The two work in tandem. -/// -/// Some of these items here are native to JS (like `Value`, indexing -/// operations, etc). Others are shims generated by wasm-bindgen (like `Closure` -/// or `Instanceof`). -#[derive(Debug)] -pub enum AuxImport { - /// This import is expected to simply be whatever is the value that's - /// imported - Value(AuxValue), - - /// A static method on a class is being imported, and the `this` of the - /// function call is expected to always be the class. - ValueWithThis(JsImport, String), - - /// This import is expected to be a function that takes an `anyref` and - /// returns a `bool`. It's expected that it tests if the argument is an - /// instance of (using `instanceof`) the name specified. - /// - /// TODO: can we use `Reflect` or something like that to avoid an extra kind - /// of import here? - Instanceof(JsImport), - - /// This import is expected to be a shim that returns the JS value named by - /// `JsImport`. - Static(JsImport), - - /// This import is intended to manufacture a JS closure with the given - /// signature and then return that back to Rust. - Closure { - mutable: bool, // whether or not this was a `FnMut` closure - dtor: u32, // table element index of the destructor function - binding_idx: u32, - nargs: usize, - }, - - /// This import is expected to be a shim that simply calls the `foo` method - /// on the first object, passing along all other parameters and returning - /// the resulting value. - StructuralMethod(String), - - /// This import is a "structural getter" which simply returns the `.field` - /// value of the first argument as an object. - /// - /// e.g. `function(x) { return x.foo; }` - StructuralGetter(String), - - /// This import is a "structural getter" which simply returns the `.field` - /// value of the specified class - /// - /// e.g. `function() { return TheClass.foo; }` - StructuralClassGetter(JsImport, String), - - /// This import is a "structural setter" which simply sets the `.field` - /// value of the first argument to the second argument. - /// - /// e.g. `function(x, y) { x.foo = y; }` - StructuralSetter(String), - - /// This import is a "structural setter" which simply sets the `.field` - /// value of the specified class to the first argument of the function. - /// - /// e.g. `function(x) { TheClass.foo = x; }` - StructuralClassSetter(JsImport, String), - - /// This import is expected to be a shim that is an indexing getter of the - /// JS class here, where the first argument of the function is the field to - /// look up. The return value is the value of the field. - /// - /// e.g. `function(x) { return TheClass[x]; }` - /// - /// TODO: can we use `Reflect` or something like that to avoid an extra kind - /// of import here? - IndexingGetterOfClass(JsImport), - - /// This import is expected to be a shim that is an indexing getter of the - /// first argument interpreted as an object where the field to look up is - /// the second argument. - /// - /// e.g. `function(x, y) { return x[y]; }` - /// - /// TODO: can we use `Reflect` or something like that to avoid an extra kind - /// of import here? - IndexingGetterOfObject, - - /// This import is expected to be a shim that is an indexing setter of the - /// JS class here, where the first argument of the function is the field to - /// set and the second is the value to set it to. - /// - /// e.g. `function(x, y) { TheClass[x] = y; }` - /// - /// TODO: can we use `Reflect` or something like that to avoid an extra kind - /// of import here? - IndexingSetterOfClass(JsImport), - - /// This import is expected to be a shim that is an indexing setter of the - /// first argument interpreted as an object where the next two fields are - /// the field to set and the value to set it to. - /// - /// e.g. `function(x, y, z) { x[y] = z; }` - /// - /// TODO: can we use `Reflect` or something like that to avoid an extra kind - /// of import here? - IndexingSetterOfObject, - - /// This import is expected to be a shim that is an indexing deleter of the - /// JS class here, where the first argument of the function is the field to - /// delete. - /// - /// e.g. `function(x) { delete TheClass[x]; }` - /// - /// TODO: can we use `Reflect` or something like that to avoid an extra kind - /// of import here? - IndexingDeleterOfClass(JsImport), - - /// This import is expected to be a shim that is an indexing deleter of the - /// first argument interpreted as an object where the second argument is - /// the field to delete. - /// - /// e.g. `function(x, y) { delete x[y]; }` - /// - /// TODO: can we use `Reflect` or something like that to avoid an extra kind - /// of import here? - IndexingDeleterOfObject, - - /// This import is a generated shim which will wrap the provided pointer in - /// a JS object corresponding to the Class name given here. The class name - /// is one that is exported from the Rust/wasm. - /// - /// TODO: sort of like the export map below we should ideally create the - /// `anyref` from within Rust itself and then return it directly rather than - /// requiring an intrinsic here to do so. - WrapInExportedClass(String), - - /// This is an intrinsic function expected to be implemented with a JS glue - /// shim. Each intrinsic has its own expected signature and implementation. - Intrinsic(Intrinsic), -} - -/// Values that can be imported verbatim to hook up to an import. -#[derive(Debug)] -pub enum AuxValue { - /// A bare JS value, no transformations, just put it in the slot. - Bare(JsImport), - - /// A getter function for the class listed for the field, acquired using - /// `getOwnPropertyDescriptor`. - Getter(JsImport, String), - - /// Like `Getter`, but accesses a field of a class instead of an instance - /// of the class. - ClassGetter(JsImport, String), - - /// Like `Getter`, except the `set` property. - Setter(JsImport, String), - - /// Like `Setter`, but for class fields instead of instance fields. - ClassSetter(JsImport, String), -} - -/// What can actually be imported and typically a value in each of the variants -/// above of `AuxImport` -/// -/// A `JsImport` is intended to indicate what exactly is being imported for a -/// particular operation. -#[derive(Debug, Hash, Eq, PartialEq, Clone)] -pub struct JsImport { - /// The base of whatever is being imported, either from a module, the global - /// namespace, or similar. - pub name: JsImportName, - /// Various field accesses (like `.foo.bar.baz`) to hang off the `name` - /// above. - pub fields: Vec, -} - -/// Return value of `determine_import` which is where we look at an imported -/// function AST and figure out where it's actually being imported from -/// (performing some validation checks and whatnot). -#[derive(Debug, Hash, Eq, PartialEq, Clone)] -pub enum JsImportName { - /// An item is imported from the global scope. The `name` is what's - /// imported. - Global { name: String }, - /// Same as `Global`, except the `name` is imported via an ESM import from - /// the specified `module` path. - Module { module: String, name: String }, - /// Same as `Module`, except we're importing from a local module defined in - /// a local JS snippet. - LocalModule { module: String, name: String }, - /// Same as `Module`, except we're importing from an `inline_js` attribute - InlineJs { - unique_crate_identifier: String, - snippet_idx_in_crate: usize, - name: String, - }, - /// A global import which may have a number of vendor prefixes associated - /// with it, like `webkitAudioPrefix`. The `name` is the name to test - /// whether it's prefixed. - VendorPrefixed { name: String, prefixes: Vec }, -} +pub mod section; +mod standard; +pub use self::nonstandard::*; +pub use self::standard::*; struct Context<'a> { start_found: bool, module: &'a mut Module, - bindings: NonstandardWebidlSection, + adapters: NonstandardWitSection, aux: WasmBindgenAux, function_exports: HashMap, function_imports: HashMap, + memory: Option, vendor_prefixes: HashMap>, unique_crate_identifier: &'a str, descriptors: HashMap, @@ -485,23 +35,32 @@ struct Context<'a> { support_start: bool, } +struct InstructionBuilder<'a, 'b> { + input: Vec, + output: Vec, + instructions: Vec, + cx: &'a mut Context<'b>, + return_position: bool, +} + pub fn process( module: &mut Module, anyref_enabled: bool, wasm_interface_types: bool, support_start: bool, -) -> Result<(NonstandardWebidlSectionId, WasmBindgenAuxId), Error> { +) -> Result<(NonstandardWitSectionId, WasmBindgenAuxId), Error> { let mut storage = Vec::new(); let programs = extract_programs(module, &mut storage)?; let mut cx = Context { - bindings: Default::default(), + adapters: Default::default(), aux: Default::default(), function_exports: Default::default(), function_imports: Default::default(), vendor_prefixes: Default::default(), descriptors: Default::default(), unique_crate_identifier: "", + memory: wasm_bindgen_wasm_conventions::get_memory(module).ok(), module, start_found: false, anyref_enabled, @@ -518,15 +77,15 @@ pub fn process( cx.discover_main()?; } - if let Some(standard) = cx.module.customs.delete_typed::() { + if let Some(standard) = cx.module.customs.delete_typed() { cx.standard(&standard)?; } cx.verify()?; - let bindings = cx.module.customs.add(cx.bindings); + let adapters = cx.module.customs.add(cx.adapters); let aux = cx.module.customs.add(cx.aux); - Ok((bindings, aux)) + Ok((adapters, aux)) } impl<'a> Context<'a> { @@ -578,29 +137,23 @@ impl<'a> Context<'a> { // to manufacture a particular type of closure. // // First we register the imported function shim which returns a - // `JsValue` for the closure. We manufacture this signature's - // binding since it's not listed anywhere. + // `JsValue` for the closure. We manufacture this signature + // since it's not listed anywhere. // - // Next we register the corresponding table element's binding in - // the webidl bindings section. This binding will later be used to + // Next we register the corresponding table element's signature in + // the interface types section. This adapter will later be used to // generate a shim (if necessary) for the table element. // // Finally we store all this metadata in the import map which we've // learned so when a binding for the import is generated we can // generate all the appropriate shims. for (id, descriptor) in closure_imports { - let binding = Function { + let signature = Function { shim_idx: 0, arguments: vec![Descriptor::I32; 3], ret: Descriptor::Anyref, }; - bindings::register_import( - self.module, - &mut self.bindings, - id, - binding, - ast::WebidlFunctionKind::Static, - )?; + let id = self.import_adapter(id, signature, AdapterJsImportKind::Normal)?; // Synthesize the two integer pointers we pass through which // aren't present in the signature but are present in the wasm // signature. @@ -608,19 +161,14 @@ impl<'a> Context<'a> { let nargs = function.arguments.len(); function.arguments.insert(0, Descriptor::I32); function.arguments.insert(0, Descriptor::I32); - let binding_idx = bindings::register_table_element( - self.module, - &mut self.bindings, - descriptor.shim_idx, - function, - )?; + let adapter = self.table_element_adapter(descriptor.shim_idx, function)?; self.aux.import_map.insert( id, AuxImport::Closure { dtor: descriptor.dtor_idx, mutable: descriptor.mutable, - binding_idx, nargs, + adapter, }, ); } @@ -699,13 +247,7 @@ impl<'a> Context<'a> { } fn bind_intrinsic(&mut self, id: ImportId, intrinsic: Intrinsic) -> Result<(), Error> { - bindings::register_import( - self.module, - &mut self.bindings, - id, - intrinsic.binding(), - ast::WebidlFunctionKind::Static, - )?; + let id = self.import_adapter(id, intrinsic.signature(), AdapterJsImportKind::Normal)?; self.aux .import_map .insert(id, AuxImport::Intrinsic(intrinsic)); @@ -833,8 +375,9 @@ impl<'a> Context<'a> { None => AuxExportKind::Function(export.function.name.to_string()), }; + let id = self.export_adapter(export_id, descriptor)?; self.aux.export_map.insert( - export_id, + id, AuxExport { debug_name: wasm_name, comments: concatenate_comments(&export.comments), @@ -842,7 +385,6 @@ impl<'a> Context<'a> { kind, }, ); - bindings::register_export(self.module, &mut self.bindings, export_id, descriptor)?; Ok(()) } @@ -908,60 +450,35 @@ impl<'a> Context<'a> { Some(d) => d.unwrap_function(), }; - // Record this for later as it affects JS binding generation, but note - // that this doesn't affect the WebIDL interface at all. - if *variadic { - self.aux.imports_with_variadic.insert(import_id); - } - if *catch { - self.aux.imports_with_catch.insert(import_id); - } - if *assert_no_shim { - self.aux.imports_with_assert_no_shim.insert(import_id); - } - - // Perform two functions here. First we're saving off our WebIDL - // bindings signature, indicating what we think our import is going to - // be. Next we're saving off other metadata indicating where this item - // is going to be imported from. The `import_map` table will record, for - // each import, what is getting hooked up to that slot of the import - // table to the WebAssembly instance. - let import = match method { + // Perform two functions here. First we're saving off our adapter + // signature, indicating what we think our import is going to be. Next + // we're saving off other metadata indicating where this item is going + // to be imported from. The `import_map` table will record, for each + // import, what is getting hooked up to that slot of the import table + // to the WebAssembly instance. + let (id, import) = match method { Some(data) => { let class = self.determine_import(import, &data.class)?; match &data.kind { // NB: `structural` is ignored for constructors since the // js type isn't expected to change anyway. decode::MethodKind::Constructor => { - bindings::register_import( - self.module, - &mut self.bindings, + let id = self.import_adapter( import_id, descriptor, - ast::WebidlFunctionKind::Constructor, + AdapterJsImportKind::Constructor, )?; - AuxImport::Value(AuxValue::Bare(class)) + (id, AuxImport::Value(AuxValue::Bare(class))) } decode::MethodKind::Operation(op) => { let (import, method) = self.determine_import_op(class, function, *structural, op)?; let kind = if method { - let kind = ast::WebidlFunctionKindMethod { - // TODO: what should this actually be? - ty: ast::WebidlScalarType::Any.into(), - }; - ast::WebidlFunctionKind::Method(kind) + AdapterJsImportKind::Method } else { - ast::WebidlFunctionKind::Static + AdapterJsImportKind::Normal }; - bindings::register_import( - self.module, - &mut self.bindings, - import_id, - descriptor, - kind, - )?; - import + (self.import_adapter(import_id, descriptor, kind)?, import) } } } @@ -969,19 +486,31 @@ impl<'a> Context<'a> { // NB: `structural` is ignored for free functions since it's // expected that the binding isn't changing anyway. None => { - bindings::register_import( - self.module, - &mut self.bindings, - import_id, - descriptor, - ast::WebidlFunctionKind::Static, - )?; + let id = self.import_adapter(import_id, descriptor, AdapterJsImportKind::Normal)?; let name = self.determine_import(import, function.name)?; - AuxImport::Value(AuxValue::Bare(name)) + (id, AuxImport::Value(AuxValue::Bare(name))) } }; - self.aux.import_map.insert(import_id, import); + // Record this for later as it affects JS binding generation, but note + // that this doesn't affect the WebIDL interface at all. + if *variadic { + self.aux.imports_with_variadic.insert(id); + } + + // Note that `catch`/`assert_no_shim` is applied not to the import + // itself but to the adapter shim we generated, so fetch that shim id + // and flag it as catch here. This basically just needs to be kept in + // sync with `js/mod.rs`. + let adapter = self.adapters.implements.last().unwrap().1; + if *catch { + self.aux.imports_with_catch.insert(adapter); + } + if *assert_no_shim { + self.aux.imports_with_assert_no_shim.insert(adapter); + } + + self.aux.import_map.insert(id, import); Ok(()) } @@ -1105,24 +634,20 @@ impl<'a> Context<'a> { }; // Register the signature of this imported shim - bindings::register_import( - self.module, - &mut self.bindings, + let id = self.import_adapter( import_id, Function { arguments: Vec::new(), shim_idx: 0, ret: descriptor, }, - ast::WebidlFunctionKind::Static, + AdapterJsImportKind::Normal, )?; // And then save off that this function is is an instanceof shim for an // imported item. let import = self.determine_import(import, &static_.name)?; - self.aux - .import_map - .insert(import_id, AuxImport::Static(import)); + self.aux.import_map.insert(id, AuxImport::Static(import)); Ok(()) } @@ -1137,16 +662,14 @@ impl<'a> Context<'a> { }; // Register the signature of this imported shim - bindings::register_import( - self.module, - &mut self.bindings, + let id = self.import_adapter( import_id, Function { arguments: vec![Descriptor::Ref(Box::new(Descriptor::Anyref))], shim_idx: 0, ret: Descriptor::Boolean, }, - ast::WebidlFunctionKind::Static, + AdapterJsImportKind::Normal, )?; // And then save off that this function is is an instanceof shim for an @@ -1154,7 +677,7 @@ impl<'a> Context<'a> { let import = self.determine_import(import, &type_.name)?; self.aux .import_map - .insert(import_id, AuxImport::Instanceof(import)); + .insert(id, AuxImport::Instanceof(import)); Ok(()) } @@ -1188,12 +711,7 @@ impl<'a> Context<'a> { shim_idx: 0, ret: descriptor.clone(), }; - bindings::register_export( - self.module, - &mut self.bindings, - getter_id, - getter_descriptor, - )?; + let getter_id = self.export_adapter(getter_id, getter_descriptor)?; self.aux.export_map.insert( getter_id, AuxExport { @@ -1218,12 +736,7 @@ impl<'a> Context<'a> { shim_idx: 0, ret: Descriptor::Unit, }; - bindings::register_export( - self.module, - &mut self.bindings, - setter_id, - setter_descriptor, - )?; + let setter_id = self.export_adapter(setter_id, setter_descriptor)?; self.aux.export_map.insert( setter_id, AuxExport { @@ -1245,23 +758,16 @@ impl<'a> Context<'a> { self.aux.structs.push(aux); let wrap_constructor = wasm_bindgen_shared::new_function(struct_.name); - if let Some((import_id, _id)) = self.function_imports.get(&wrap_constructor) { - self.aux.import_map.insert( - *import_id, - AuxImport::WrapInExportedClass(struct_.name.to_string()), - ); - let binding = Function { + if let Some((import_id, _id)) = self.function_imports.get(&wrap_constructor).cloned() { + let signature = Function { shim_idx: 0, arguments: vec![Descriptor::I32], ret: Descriptor::Anyref, }; - bindings::register_import( - self.module, - &mut self.bindings, - *import_id, - binding, - ast::WebidlFunctionKind::Static, - )?; + let id = self.import_adapter(import_id, signature, AdapterJsImportKind::Normal)?; + self.aux + .import_map + .insert(id, AuxImport::WrapInExportedClass(struct_.name.to_string())); } Ok(()) @@ -1352,144 +858,146 @@ impl<'a> Context<'a> { Ok(JsImport { name, fields }) } - /// Processes bindings from a standard WebIDL bindings custom section. - /// - /// No module coming out of the Rust compiler will have one of these, but - /// eventually there's going to be other producers of the WebIDL bindings - /// custom section as well. This functionality is intended to allow - /// `wasm-bindgen`-the-CLI-tool to act as a polyfill for those modules as - /// well as Rust modules. - /// - /// Here a standard `WebidlBindings` custom section is taken and we process - /// that into our own internal data structures to ensure that we have a - /// binding listed for all the described bindings. - /// - /// In other words, this is a glorified conversion from the "official" - /// WebIDL bindings custom section into the wasm-bindgen internal - /// representation. - fn standard(&mut self, std: &ast::WebidlBindings) -> Result<(), Error> { - for (_id, bind) in std.binds.iter() { - let binding = self.standard_binding(std, bind)?; - let func = self.module.funcs.get(bind.func); - match &func.kind { - walrus::FunctionKind::Import(i) => { - let id = i.import; - self.standard_import(binding, id)?; - } - walrus::FunctionKind::Local(_) => { - let export = self - .module - .exports - .iter() - .find(|e| match e.item { - walrus::ExportItem::Function(f) => f == bind.func, - _ => false, - }) - .ok_or_else(|| anyhow!("missing export function for webidl binding"))?; - let id = export.id(); - self.standard_export(binding, id)?; - } - walrus::FunctionKind::Uninitialized(_) => unreachable!(), + fn standard(&mut self, std: &wit_walrus::WasmInterfaceTypes) -> Result<(), Error> { + let mut walrus2us = HashMap::new(); + let params_and_results = |id: wit_walrus::TypeId| -> (Vec<_>, Vec<_>) { + let ty = std.types.get(id); + let params = ty + .params() + .iter() + .cloned() + .map(AdapterType::from_wit) + .collect(); + let results = ty + .results() + .iter() + .cloned() + .map(AdapterType::from_wit) + .collect(); + (params, results) + }; + + // Register all imports, allocating our own id for them and configuring + // where the JS value for the import is coming from. + for import in std.imports.iter() { + let func = std.funcs.get(import.func); + let (params, results) = params_and_results(func.ty); + let id = self.adapters.append( + params, + results, + AdapterKind::Import { + module: import.module.clone(), + name: import.name.clone(), + kind: AdapterJsImportKind::Normal, + }, + ); + walrus2us.insert(import.func, id); + let js = JsImport { + name: JsImportName::Module { + module: import.module.clone(), + name: import.name.clone(), + }, + fields: Vec::new(), + }; + let value = AuxValue::Bare(js); + assert!(self + .aux + .import_map + .insert(id, AuxImport::Value(value)) + .is_none()); + } + + // Register all functions, allocating our own id system for each of the + // functions. + for func in std.funcs.iter() { + if let wit_walrus::FuncKind::Import(_) = func.kind { + continue; + } + let (params, results) = params_and_results(func.ty); + walrus2us.insert( + func.id(), + self.adapters.append( + params, + results, + AdapterKind::Local { + instructions: Vec::new(), + }, + ), + ); + } + + // .. and then actually translate all functions using our id mapping, + // now that we're able to remap all the `CallAdapter` instructions. + for func in std.funcs.iter() { + let instrs = match &func.kind { + wit_walrus::FuncKind::Local(instrs) => instrs, + wit_walrus::FuncKind::Import(_) => continue, + }; + let instrs = instrs + .iter() + .map(|i| match i { + wit_walrus::Instruction::CallAdapter(f) => { + Instruction::CallAdapter(walrus2us[&f]) + } + other => Instruction::Standard(other.clone()), + }) + .map(|instr| InstructionData { + instr, + stack_change: StackChange::Unknown, + }) + .collect::>(); + + // Store the instrs into the adapter function directly. + let adapter = self + .adapters + .adapters + .get_mut(&walrus2us[&func.id()]) + .unwrap(); + match &mut adapter.kind { + AdapterKind::Local { instructions } => *instructions = instrs, + _ => unreachable!(), } } - Ok(()) - } - /// Creates a wasm-bindgen-internal `Binding` from an official `Bind` - /// structure specified in the upstream binary format. - /// - /// This will largely just copy some things into our own arenas but also - /// processes the list of binding expressions into our own representations. - fn standard_binding( - &mut self, - std: &ast::WebidlBindings, - bind: &ast::Bind, - ) -> Result { - let binding: &ast::FunctionBinding = std - .bindings - .get(bind.binding) - .ok_or_else(|| anyhow!("bad binding id"))?; - let (wasm_ty, webidl_ty, incoming, outgoing) = match binding { - ast::FunctionBinding::Export(e) => ( - e.wasm_ty, - e.webidl_ty, - e.params.bindings.as_slice(), - &e.result.bindings[..], - ), - ast::FunctionBinding::Import(e) => ( - e.wasm_ty, - e.webidl_ty, - &e.result.bindings[..], - e.params.bindings.as_slice(), - ), - }; - let webidl_ty = standard::copy_ty(&mut self.bindings.types, webidl_ty, &std.types); - let webidl_ty = match webidl_ty { - ast::WebidlTypeRef::Id(id) => ::wrap(id), - _ => bail!("invalid webidl type listed"), - }; + // next up register all exports, ensuring that our export map says + // what's happening as well for JS + for export in std.exports.iter() { + let id = walrus2us[&export.func]; + self.adapters.exports.push((export.name.clone(), id)); - Ok(Binding { - wasm_ty, - webidl_ty, - incoming: incoming - .iter() - .cloned() - .map(NonstandardIncoming::Standard) - .collect(), - outgoing: outgoing - .iter() - .cloned() - .map(NonstandardOutgoing::Standard) - .collect(), - return_via_outptr: None, - }) - } + let kind = AuxExportKind::Function(export.name.clone()); + let export = AuxExport { + debug_name: format!("standard export {:?}", id), + comments: String::new(), + arg_names: None, + kind, + }; + assert!(self.aux.export_map.insert(id, export).is_none()); + } - /// Registers that `id` has a `binding` which was read from a standard - /// webidl bindings section, so the source of `id` is its actual module/name - /// listed in the wasm module. - fn standard_import(&mut self, binding: Binding, id: walrus::ImportId) -> Result<(), Error> { - let import = self.module.imports.get(id); - let js = JsImport { - name: JsImportName::Module { - module: import.module.clone(), - name: import.name.clone(), - }, - fields: Vec::new(), - }; - let value = AuxValue::Bare(js); - assert!(self - .aux - .import_map - .insert(id, AuxImport::Value(value)) - .is_none()); - assert!(self.bindings.imports.insert(id, binding).is_none()); - - Ok(()) - } - - /// Registers that `id` has a `binding` and comes from a standard webidl - /// bindings section so it doesn't have any documentation or debug names we - /// can work with. - fn standard_export(&mut self, binding: Binding, id: walrus::ExportId) -> Result<(), Error> { - let export = self.module.exports.get(id); - let kind = AuxExportKind::Function(export.name.clone()); - let export = AuxExport { - debug_name: format!("standard export {:?}", id), - comments: String::new(), - arg_names: None, - kind, - }; - assert!(self.aux.export_map.insert(id, export).is_none()); - assert!(self.bindings.exports.insert(id, binding).is_none()); + // ... and finally the `implements` section + for i in std.implements.iter() { + let import_id = match &self.module.funcs.get(i.core_func).kind { + walrus::FunctionKind::Import(i) => i.import, + _ => panic!("malformed wasm interface typess section"), + }; + self.adapters + .implements + .push((import_id, walrus2us[&i.adapter_func])); + } Ok(()) } /// Perform a small verification pass over the module to perform some /// internal sanity checks. fn verify(&self) -> Result<(), Error> { - let mut imports_counted = 0; + // First up verify that all imports in the wasm module from our + // `$PLACEHOLDER_MODULE` are connected to an adapter via the + // `implements` section. + let mut implemented = HashMap::new(); + for (core, adapter) in self.adapters.implements.iter() { + implemented.insert(core, adapter); + } for import in self.module.imports.iter() { if import.module != PLACEHOLDER_MODULE { continue; @@ -1498,66 +1006,244 @@ impl<'a> Context<'a> { walrus::ImportKind::Function(_) => {} _ => bail!("import from `{}` was not a function", PLACEHOLDER_MODULE), } + if implemented.remove(&import.id()).is_none() { + bail!("import of `{}` doesn't have an adapter listed", import.name); + } + } + if implemented.len() != 0 { + bail!("more implementations listed than imports"); + } - // Ensure that everything imported from the `__wbindgen_placeholder__` - // module has a location listed as to where it's expected to be - // imported from. - if !self.aux.import_map.contains_key(&import.id()) { + // Next up verify that all imported adapter functions have a listing of + // where they're imported from. + let mut imports_counted = 0; + for (id, adapter) in self.adapters.adapters.iter() { + let name = match &adapter.kind { + AdapterKind::Import { name, .. } => name, + AdapterKind::Local { .. } => continue, + }; + if !self.aux.import_map.contains_key(id) { bail!( "import of `{}` doesn't have an import map item listed", - import.name + name ); } - // Also make sure there's a binding listed for it. - if !self.bindings.imports.contains_key(&import.id()) { - bail!("import of `{}` doesn't have a binding listed", import.name); - } imports_counted += 1; } - - // Make sure there's no extraneous bindings that weren't actually + // Make sure there's no extraneous adapters that weren't actually // imported in the module. if self.aux.import_map.len() != imports_counted { bail!("import map is larger than the number of imports"); } - if self.bindings.imports.len() != imports_counted { - bail!("import binding map is larger than the number of imports"); - } - // Make sure the export map and export bindings map contain the same + // Make sure the export map and export adapters map contain the same // number of entries. - for id in self.bindings.exports.keys() { + for (_, id) in self.adapters.exports.iter() { if !self.aux.export_map.contains_key(id) { - bail!("bindings map has an entry that the export map does not"); + bail!("adapters map has an entry that the export map does not"); } } - if self.bindings.exports.len() != self.aux.export_map.len() { - bail!("export map and export bindings map have different sizes"); + if self.adapters.exports.len() != self.aux.export_map.len() { + bail!("export map and export adapters map have different sizes"); } Ok(()) } -} -impl walrus::CustomSection for NonstandardWebidlSection { - fn name(&self) -> &str { - "webidl custom section" + /// Creates an import adapter for the `import` which will have the given + /// `signature`. + /// + /// Note that the JS function imported will be invoked as `kind`. + fn import_adapter( + &mut self, + import: ImportId, + signature: Function, + kind: AdapterJsImportKind, + ) -> Result { + let import = self.module.imports.get(import); + let (import_module, import_name) = (import.module.clone(), import.name.clone()); + let import_id = import.id(); + + // Process the returned type first to see if it needs an out-pointer. This + // happens if the results of the incoming arguments translated to wasm take + // up more than one type. + let mut ret = self.instruction_builder(true); + ret.incoming(&signature.ret)?; + let uses_retptr = ret.output.len() > 1; + + // Process the argument next, allocating space of the return value if one + // was present. Additionally configure the `module` and `adapters` to allow + // usage of closures going out to the import. + let mut args = ret.cx.instruction_builder(false); + if uses_retptr { + args.input.push(AdapterType::I32); + } + for arg in signature.arguments.iter() { + args.outgoing(arg)?; + } + + // Build up the list of instructions for our adapter function. We start out + // with all the outgoing instructions which convert all wasm params to the + // desired types to call our import... + let mut instructions = args.instructions; + + // ... and then we actually call our import. We synthesize an adapter + // definition for it with the appropriate types here on the fly. + let f = args.cx.adapters.append( + args.output, + ret.input, + AdapterKind::Import { + module: import_module, + name: import_name, + kind, + }, + ); + instructions.push(InstructionData { + instr: Instruction::CallAdapter(f), + stack_change: StackChange::Unknown, + }); + + // ... and then we follow up with a conversion of the incoming type + // back to wasm. + instructions.extend(ret.instructions); + + // ... and if a return pointer is in use then we need to store the types on + // the stack into the wasm return pointer. Note that we iterate in reverse + // here because the last result is the top value on the stack. + let results = if uses_retptr { + let mem = args.cx.memory()?; + for (i, ty) in ret.output.into_iter().enumerate().rev() { + instructions.push(InstructionData { + instr: Instruction::StoreRetptr { offset: i, ty, mem }, + stack_change: StackChange::Modified { + pushed: 0, + popped: 1, + }, + }); + } + Vec::new() + } else { + ret.output + }; + let id = args + .cx + .adapters + .append(args.input, results, AdapterKind::Local { instructions }); + args.cx.adapters.implements.push((import_id, id)); + Ok(f) } - fn data(&self, _: &walrus::IdsToIndices) -> Cow<[u8]> { - panic!("shouldn't emit custom sections just yet"); - } -} - -impl walrus::CustomSection for WasmBindgenAux { - fn name(&self) -> &str { - "wasm-bindgen custom section" + /// Creates an adapter function for the `export` given to have the + /// `signature` specified. + fn export_adapter( + &mut self, + export: ExportId, + signature: Function, + ) -> Result { + let export = self.module.exports.get(export); + let name = export.name.clone(); + // Do the actual heavy lifting elsewhere to generate the `binding`. + let call = Instruction::CallExport(export.id()); + let id = self.register_export_adapter(call, signature)?; + self.adapters.exports.push((name, id)); + Ok(id) } - fn data(&self, _: &walrus::IdsToIndices) -> Cow<[u8]> { - panic!("shouldn't emit custom sections just yet"); + fn table_element_adapter(&mut self, idx: u32, signature: Function) -> Result { + let call = Instruction::CallTableElement(idx); + // like above, largely just defer the work elsewhere + Ok(self.register_export_adapter(call, signature)?) + } + + fn register_export_adapter( + &mut self, + call: Instruction, + signature: Function, + ) -> Result { + // Figure out how to translate all the incoming arguments ... + let mut args = self.instruction_builder(false); + for arg in signature.arguments.iter() { + args.incoming(arg)?; + } + + // ... then the returned value being translated back + let mut ret = args.cx.instruction_builder(true); + ret.outgoing(&signature.ret)?; + let uses_retptr = ret.input.len() > 1; + + // Our instruction stream starts out with the return pointer as the first + // argument to the wasm function, if one is in use. Then we convert + // everything to wasm types. + // + // After calling the core wasm function we need to load all the return + // pointer arguments if there were any, otherwise we simply convert + // everything into the outgoing arguments. + let mut instructions = Vec::new(); + if uses_retptr { + instructions.push(InstructionData { + instr: Instruction::Retptr, + stack_change: StackChange::Modified { + pushed: 1, + popped: 0, + }, + }); + } + instructions.extend(args.instructions); + instructions.push(InstructionData { + instr: call, + stack_change: StackChange::Unknown, + }); + if uses_retptr { + let mem = ret.cx.memory()?; + for (i, ty) in ret.input.into_iter().enumerate() { + instructions.push(InstructionData { + instr: Instruction::LoadRetptr { offset: i, ty, mem }, + stack_change: StackChange::Modified { + pushed: 1, + popped: 0, + }, + }); + } + } + instructions.extend(ret.instructions); + + Ok(ret + .cx + .adapters + .append(args.input, ret.output, AdapterKind::Local { instructions })) + } + + fn instruction_builder<'b>(&'b mut self, return_position: bool) -> InstructionBuilder<'b, 'a> { + InstructionBuilder { + cx: self, + input: Vec::new(), + output: Vec::new(), + instructions: Vec::new(), + return_position, + } + } + + fn malloc(&self) -> Result { + self.function_exports + .get("__wbindgen_malloc") + .cloned() + .map(|p| p.1) + .ok_or_else(|| anyhow!("failed to find declaration of `__wbindgen_malloc` in module")) + } + + fn free(&self) -> Result { + self.function_exports + .get("__wbindgen_free") + .cloned() + .map(|p| p.1) + .ok_or_else(|| anyhow!("failed to find declaration of `__wbindgen_free` in module")) + } + + fn memory(&self) -> Result { + self.memory + .ok_or_else(|| anyhow!("failed to find memory declaration in module")) } } @@ -1689,77 +1375,3 @@ fn concatenate_comments(comments: &[&str]) -> String { .collect::>() .join("\n") } - -/// Do we need to generate JS glue shims for these incoming bindings? -pub fn incoming_do_not_require_glue( - exprs: &[NonstandardIncoming], - from_webidl_tys: &[ast::WebidlTypeRef], - to_wasm_tys: &[walrus::ValType], - standard_webidl_enabled: bool, -) -> bool { - // If anything is nonstandard, then we're unconditionally going to need a JS - // shim because, well, it's not standard. - if exprs.iter().any(|e| match e { - NonstandardIncoming::Standard(_) => false, - _ => true, - }) { - return false; - } - - // If everything is `Standard` and we've actually got WebIDL bindings fully - // enabled, then we don't require any glue at all! - if standard_webidl_enabled { - return true; - } - - exprs.len() == from_webidl_tys.len() - && exprs.len() == to_wasm_tys.len() - && exprs - .iter() - .zip(from_webidl_tys) - .zip(to_wasm_tys) - .enumerate() - .all(|(i, ((expr, from_webidl_ty), to_wasm_ty))| match expr { - NonstandardIncoming::Standard(e) => e.is_expressible_in_js_without_webidl_bindings( - *from_webidl_ty, - *to_wasm_ty, - i as u32, - ), - _ => false, - }) -} - -/// Do we need to generate JS glue shims for these outgoing bindings? -pub fn outgoing_do_not_require_glue( - exprs: &[NonstandardOutgoing], - from_wasm_tys: &[walrus::ValType], - to_webidl_tys: &[ast::WebidlTypeRef], - standard_webidl_enabled: bool, -) -> bool { - // Same short-circuits as above. - if exprs.iter().any(|e| match e { - NonstandardOutgoing::Standard(_) => false, - _ => true, - }) { - return false; - } - if standard_webidl_enabled { - return true; - } - - exprs.len() == from_wasm_tys.len() - && exprs.len() == to_webidl_tys.len() - && exprs - .iter() - .zip(from_wasm_tys) - .zip(to_webidl_tys) - .enumerate() - .all(|(i, ((expr, from_wasm_ty), to_webidl_ty))| match expr { - NonstandardOutgoing::Standard(e) => e.is_expressible_in_js_without_webidl_bindings( - *from_wasm_ty, - *to_webidl_ty, - i as u32, - ), - _ => false, - }) -} diff --git a/crates/cli-support/src/wit/nonstandard.rs b/crates/cli-support/src/wit/nonstandard.rs new file mode 100644 index 00000000..20f03cf5 --- /dev/null +++ b/crates/cli-support/src/wit/nonstandard.rs @@ -0,0 +1,360 @@ +use crate::intrinsic::Intrinsic; +use crate::wit::AdapterId; +use std::borrow::Cow; +use std::collections::{HashMap, HashSet}; +use std::path::PathBuf; +use walrus::TypedCustomSectionId; + +/// A synthetic custom section which is not standardized, never will be, and +/// cannot be serialized or parsed. This is synthesized from all of the +/// compiler-emitted wasm-bindgen sections and then immediately removed to be +/// processed in the JS generation pass. +#[derive(Default, Debug)] +pub struct WasmBindgenAux { + /// Extra typescript annotations that should be appended to the generated + /// TypeScript file. This is provided via a custom attribute in Rust code. + pub extra_typescript: String, + + /// A map from identifier to the contents of each local module defined via + /// the `#[wasm_bindgen(module = "/foo.js")]` import options. + pub local_modules: HashMap, + + /// A map from unique crate identifier to the list of inline JS snippets for + /// that crate identifier. + pub snippets: HashMap>, + + /// A list of all `package.json` files that are intended to be included in + /// the final build. + pub package_jsons: HashSet, + + /// A map from exported function id to where it's expected to be exported + /// to. + pub export_map: HashMap, + + /// A map from imported function id to what it's expected to import. + pub import_map: HashMap, + + /// Small bits of metadata about imports. + pub imports_with_catch: HashSet, + pub imports_with_variadic: HashSet, + pub imports_with_assert_no_shim: HashSet, + + /// Auxiliary information to go into JS/TypeScript bindings describing the + /// exported enums from Rust. + pub enums: Vec, + + /// Auxiliary information to go into JS/TypeScript bindings describing the + /// exported structs from Rust and their fields they've got exported. + pub structs: Vec, + + /// Information about various internal functions used to manage the `anyref` + /// table, later used to process JS bindings. + pub anyref_table: Option, + pub anyref_alloc: Option, + pub anyref_drop_slice: Option, +} + +pub type WasmBindgenAuxId = TypedCustomSectionId; + +#[derive(Debug)] +pub struct AuxExport { + /// When generating errors about this export, a helpful name to remember it + /// by. + pub debug_name: String, + /// Comments parsed in Rust and forwarded here to show up in JS bindings. + pub comments: String, + /// Argument names in Rust forwarded here to configure the names that show + /// up in TypeScript bindings. + pub arg_names: Option>, + /// What kind of function this is and where it shows up + pub kind: AuxExportKind, +} + +/// All possible kinds of exports from a wasm module. +/// +/// This `enum` says where to place an exported wasm function. For example it +/// may want to get hooked up to a JS class, or it may want to be exported as a +/// free function (etc). +/// +/// TODO: it feels like this should not really be here per se. We probably want +/// to either construct the JS object itself from within wasm or somehow move +/// more of this information into some other section. Really what this is is +/// sort of an "export map" saying how to wire up all the free functions from +/// the wasm module into the output expected JS module. All our functions here +/// currently take integer parameters and require a JS wrapper, but ideally +/// we'd change them one day to taking/receiving `anyref` which then use some +/// sort of webidl import to customize behavior or something like that. In any +/// case this doesn't feel quite right in terms of priviledge separation, so +/// we'll want to work on this. For now though it works. +#[derive(Debug)] +pub enum AuxExportKind { + /// A free function that's just listed on the exported module + Function(String), + + /// A function that's used to create an instane of a class. The function + /// actually return just an integer which is put on an JS object currently. + Constructor(String), + + /// This function is intended to be a getter for a field on a class. The + /// first argument is the internal pointer and the returned value is + /// expected to be the field. + Getter { class: String, field: String }, + + /// This function is intended to be a setter for a field on a class. The + /// first argument is the internal pointer and the second argument is + /// expected to be the field's new value. + Setter { class: String, field: String }, + + /// This is a free function (ish) but scoped inside of a class name. + StaticFunction { class: String, name: String }, + + /// This is a member function of a class where the first parameter is the + /// implicit integer stored in the class instance. + Method { + class: String, + name: String, + /// Whether or not this is calling a by-value method in Rust and should + /// clear the internal pointer in JS automatically. + consumed: bool, + }, +} + +#[derive(Debug)] +pub struct AuxEnum { + /// The name of this enum + pub name: String, + /// The copied Rust comments to forward to JS + pub comments: String, + /// A list of variants with their name and value + pub variants: Vec<(String, u32)>, +} + +#[derive(Debug)] +pub struct AuxStruct { + /// The name of this struct + pub name: String, + /// The copied Rust comments to forward to JS + pub comments: String, + /// Whether to generate helper methods for inspecting the class + pub is_inspectable: bool, +} + +/// All possible types of imports that can be imported by a wasm module. +/// +/// This `enum` is intended to map out what an imported value is. For example +/// this contains a ton of shims and various ways you can call a function. The +/// base variant here is `Value` which simply means "hook this up to the import" +/// and the signatures will match up. +/// +/// Note that this is *not* the same as the webidl bindings section. This is +/// intended to be coupled with that to map out what actually gets hooked up to +/// an import in the wasm module. The two work in tandem. +/// +/// Some of these items here are native to JS (like `Value`, indexing +/// operations, etc). Others are shims generated by wasm-bindgen (like `Closure` +/// or `Instanceof`). +#[derive(Debug)] +pub enum AuxImport { + /// This import is expected to simply be whatever is the value that's + /// imported + Value(AuxValue), + + /// A static method on a class is being imported, and the `this` of the + /// function call is expected to always be the class. + ValueWithThis(JsImport, String), + + /// This import is expected to be a function that takes an `anyref` and + /// returns a `bool`. It's expected that it tests if the argument is an + /// instance of (using `instanceof`) the name specified. + /// + /// TODO: can we use `Reflect` or something like that to avoid an extra kind + /// of import here? + Instanceof(JsImport), + + /// This import is expected to be a shim that returns the JS value named by + /// `JsImport`. + Static(JsImport), + + /// This import is intended to manufacture a JS closure with the given + /// signature and then return that back to Rust. + Closure { + mutable: bool, // whether or not this was a `FnMut` closure + dtor: u32, // table element index of the destructor function + adapter: AdapterId, // the adapter which translates the types for this closure + nargs: usize, + }, + + /// This import is expected to be a shim that simply calls the `foo` method + /// on the first object, passing along all other parameters and returning + /// the resulting value. + StructuralMethod(String), + + /// This import is a "structural getter" which simply returns the `.field` + /// value of the first argument as an object. + /// + /// e.g. `function(x) { return x.foo; }` + StructuralGetter(String), + + /// This import is a "structural getter" which simply returns the `.field` + /// value of the specified class + /// + /// e.g. `function() { return TheClass.foo; }` + StructuralClassGetter(JsImport, String), + + /// This import is a "structural setter" which simply sets the `.field` + /// value of the first argument to the second argument. + /// + /// e.g. `function(x, y) { x.foo = y; }` + StructuralSetter(String), + + /// This import is a "structural setter" which simply sets the `.field` + /// value of the specified class to the first argument of the function. + /// + /// e.g. `function(x) { TheClass.foo = x; }` + StructuralClassSetter(JsImport, String), + + /// This import is expected to be a shim that is an indexing getter of the + /// JS class here, where the first argument of the function is the field to + /// look up. The return value is the value of the field. + /// + /// e.g. `function(x) { return TheClass[x]; }` + /// + /// TODO: can we use `Reflect` or something like that to avoid an extra kind + /// of import here? + IndexingGetterOfClass(JsImport), + + /// This import is expected to be a shim that is an indexing getter of the + /// first argument interpreted as an object where the field to look up is + /// the second argument. + /// + /// e.g. `function(x, y) { return x[y]; }` + /// + /// TODO: can we use `Reflect` or something like that to avoid an extra kind + /// of import here? + IndexingGetterOfObject, + + /// This import is expected to be a shim that is an indexing setter of the + /// JS class here, where the first argument of the function is the field to + /// set and the second is the value to set it to. + /// + /// e.g. `function(x, y) { TheClass[x] = y; }` + /// + /// TODO: can we use `Reflect` or something like that to avoid an extra kind + /// of import here? + IndexingSetterOfClass(JsImport), + + /// This import is expected to be a shim that is an indexing setter of the + /// first argument interpreted as an object where the next two fields are + /// the field to set and the value to set it to. + /// + /// e.g. `function(x, y, z) { x[y] = z; }` + /// + /// TODO: can we use `Reflect` or something like that to avoid an extra kind + /// of import here? + IndexingSetterOfObject, + + /// This import is expected to be a shim that is an indexing deleter of the + /// JS class here, where the first argument of the function is the field to + /// delete. + /// + /// e.g. `function(x) { delete TheClass[x]; }` + /// + /// TODO: can we use `Reflect` or something like that to avoid an extra kind + /// of import here? + IndexingDeleterOfClass(JsImport), + + /// This import is expected to be a shim that is an indexing deleter of the + /// first argument interpreted as an object where the second argument is + /// the field to delete. + /// + /// e.g. `function(x, y) { delete x[y]; }` + /// + /// TODO: can we use `Reflect` or something like that to avoid an extra kind + /// of import here? + IndexingDeleterOfObject, + + /// This import is a generated shim which will wrap the provided pointer in + /// a JS object corresponding to the Class name given here. The class name + /// is one that is exported from the Rust/wasm. + /// + /// TODO: sort of like the export map below we should ideally create the + /// `anyref` from within Rust itself and then return it directly rather than + /// requiring an intrinsic here to do so. + WrapInExportedClass(String), + + /// This is an intrinsic function expected to be implemented with a JS glue + /// shim. Each intrinsic has its own expected signature and implementation. + Intrinsic(Intrinsic), +} + +/// Values that can be imported verbatim to hook up to an import. +#[derive(Debug)] +pub enum AuxValue { + /// A bare JS value, no transformations, just put it in the slot. + Bare(JsImport), + + /// A getter function for the class listed for the field, acquired using + /// `getOwnPropertyDescriptor`. + Getter(JsImport, String), + + /// Like `Getter`, but accesses a field of a class instead of an instance + /// of the class. + ClassGetter(JsImport, String), + + /// Like `Getter`, except the `set` property. + Setter(JsImport, String), + + /// Like `Setter`, but for class fields instead of instance fields. + ClassSetter(JsImport, String), +} + +/// What can actually be imported and typically a value in each of the variants +/// above of `AuxImport` +/// +/// A `JsImport` is intended to indicate what exactly is being imported for a +/// particular operation. +#[derive(Debug, Hash, Eq, PartialEq, Clone)] +pub struct JsImport { + /// The base of whatever is being imported, either from a module, the global + /// namespace, or similar. + pub name: JsImportName, + /// Various field accesses (like `.foo.bar.baz`) to hang off the `name` + /// above. + pub fields: Vec, +} + +/// Return value of `determine_import` which is where we look at an imported +/// function AST and figure out where it's actually being imported from +/// (performing some validation checks and whatnot). +#[derive(Debug, Hash, Eq, PartialEq, Clone)] +pub enum JsImportName { + /// An item is imported from the global scope. The `name` is what's + /// imported. + Global { name: String }, + /// Same as `Global`, except the `name` is imported via an ESM import from + /// the specified `module` path. + Module { module: String, name: String }, + /// Same as `Module`, except we're importing from a local module defined in + /// a local JS snippet. + LocalModule { module: String, name: String }, + /// Same as `Module`, except we're importing from an `inline_js` attribute + InlineJs { + unique_crate_identifier: String, + snippet_idx_in_crate: usize, + name: String, + }, + /// A global import which may have a number of vendor prefixes associated + /// with it, like `webkitAudioPrefix`. The `name` is the name to test + /// whether it's prefixed. + VendorPrefixed { name: String, prefixes: Vec }, +} + +impl walrus::CustomSection for WasmBindgenAux { + fn name(&self) -> &str { + "wasm-bindgen custom section" + } + + fn data(&self, _: &walrus::IdsToIndices) -> Cow<[u8]> { + panic!("shouldn't emit custom sections just yet"); + } +} diff --git a/crates/cli-support/src/wit/outgoing.rs b/crates/cli-support/src/wit/outgoing.rs new file mode 100644 index 00000000..36473193 --- /dev/null +++ b/crates/cli-support/src/wit/outgoing.rs @@ -0,0 +1,382 @@ +use crate::descriptor::Descriptor; +use crate::wit::{AdapterType, Instruction, InstructionBuilder}; +use crate::wit::{InstructionData, StackChange}; +use anyhow::{bail, format_err, Error}; +use walrus::ValType; + +impl InstructionBuilder<'_, '_> { + /// Processes one more `Descriptor` as an argument to a JS function that + /// wasm is calling. + /// + /// This will internally skip `Unit` and otherwise build up the `bindings` + /// map and ensure that it's correctly mapped from wasm to JS. + pub fn outgoing(&mut self, arg: &Descriptor) -> Result<(), Error> { + if let Descriptor::Unit = arg { + return Ok(()); + } + // Similar rationale to `incoming.rs` around these sanity checks. + let input_before = self.input.len(); + let output_before = self.output.len(); + self._outgoing(arg)?; + assert_eq!(output_before + 1, self.output.len()); + assert!(input_before < self.input.len()); + Ok(()) + } + + fn _outgoing(&mut self, arg: &Descriptor) -> Result<(), Error> { + match arg { + Descriptor::Boolean => { + self.instruction( + &[AdapterType::I32], + Instruction::BoolFromI32, + &[AdapterType::Bool], + ); + } + Descriptor::Anyref => { + self.instruction( + &[AdapterType::I32], + Instruction::AnyrefLoadOwned, + &[AdapterType::Anyref], + ); + } + Descriptor::I8 => self.outgoing_i32(AdapterType::S8), + Descriptor::U8 => self.outgoing_i32(AdapterType::U8), + Descriptor::I16 => self.outgoing_i32(AdapterType::S16), + Descriptor::U16 => self.outgoing_i32(AdapterType::U16), + Descriptor::I32 => self.outgoing_i32(AdapterType::S32), + Descriptor::U32 => self.outgoing_i32(AdapterType::U32), + Descriptor::F32 => { + self.get(AdapterType::F32); + self.output.push(AdapterType::F32); + } + Descriptor::F64 => { + self.get(AdapterType::F64); + self.output.push(AdapterType::F64); + } + Descriptor::Enum { .. } => self.outgoing_i32(AdapterType::U32), + + Descriptor::Char => { + self.instruction( + &[AdapterType::I32], + Instruction::StringFromChar, + &[AdapterType::String], + ); + } + + Descriptor::I64 | Descriptor::U64 => { + let signed = match arg { + Descriptor::I64 => true, + _ => false, + }; + self.instruction( + &[AdapterType::I32; 2], + Instruction::I64FromLoHi { signed }, + &[if signed { + AdapterType::S64 + } else { + AdapterType::U64 + }], + ); + } + + Descriptor::RustStruct(class) => { + self.instruction( + &[AdapterType::I32], + Instruction::RustFromI32 { + class: class.to_string(), + }, + &[AdapterType::Anyref], + ); + } + Descriptor::Ref(d) => self.outgoing_ref(false, d)?, + Descriptor::RefMut(d) => self.outgoing_ref(true, d)?, + + Descriptor::CachedString => self.cached_string(false, true)?, + + Descriptor::String => { + // fetch the ptr/length ... + self.get(AdapterType::I32); + self.get(AdapterType::I32); + + // ... then defer a call to `free` to happen later + let free = self.cx.free()?; + let std = wit_walrus::Instruction::DeferCallCore(free); + self.instructions.push(InstructionData { + instr: Instruction::Standard(std), + stack_change: StackChange::Modified { + popped: 2, + pushed: 2, + }, + }); + + // ... and then convert it to a string type + let std = wit_walrus::Instruction::MemoryToString(self.cx.memory()?); + self.instructions.push(InstructionData { + instr: Instruction::Standard(std), + stack_change: StackChange::Modified { + popped: 2, + pushed: 1, + }, + }); + self.output.push(AdapterType::String); + } + + Descriptor::Vector(_) => { + let kind = arg.vector_kind().ok_or_else(|| { + format_err!( + "unsupported argument type for calling JS function from Rust {:?}", + arg + ) + })?; + let mem = self.cx.memory()?; + let free = self.cx.free()?; + self.instruction( + &[AdapterType::I32; 2], + Instruction::VectorLoad { kind, mem, free }, + &[AdapterType::Vector(kind)], + ); + } + + Descriptor::Option(d) => self.outgoing_option(d)?, + + Descriptor::Function(_) | Descriptor::Closure(_) | Descriptor::Slice(_) => bail!( + "unsupported argument type for calling JS function from Rust: {:?}", + arg + ), + + // nothing to do + Descriptor::Unit => {} + + // Largely synthetic and can't show up + Descriptor::ClampedU8 => unreachable!(), + } + Ok(()) + } + + fn outgoing_ref(&mut self, mutable: bool, arg: &Descriptor) -> Result<(), Error> { + match arg { + Descriptor::Anyref => { + self.instruction( + &[AdapterType::I32], + Instruction::TableGet, + &[AdapterType::Anyref], + ); + } + Descriptor::CachedString => self.cached_string(false, false)?, + + Descriptor::String => { + let std = wit_walrus::Instruction::MemoryToString(self.cx.memory()?); + self.instruction( + &[AdapterType::I32; 2], + Instruction::Standard(std), + &[AdapterType::String], + ); + } + Descriptor::Slice(_) => { + let kind = arg.vector_kind().ok_or_else(|| { + format_err!( + "unsupported argument type for calling JS function from Rust {:?}", + arg + ) + })?; + let mem = self.cx.memory()?; + self.instruction( + &[AdapterType::I32; 2], + Instruction::View { kind, mem }, + &[AdapterType::Vector(kind)], + ); + } + + Descriptor::Function(descriptor) => { + // synthesize the a/b arguments that aren't present in the + // signature from wasm-bindgen but are present in the wasm file. + let mut descriptor = (**descriptor).clone(); + let nargs = descriptor.arguments.len(); + descriptor.arguments.insert(0, Descriptor::I32); + descriptor.arguments.insert(0, Descriptor::I32); + let adapter = self + .cx + .table_element_adapter(descriptor.shim_idx, descriptor)?; + self.instruction( + &[AdapterType::I32; 2], + Instruction::StackClosure { + adapter, + nargs, + mutable, + }, + &[AdapterType::Anyref], + ); + } + + _ => bail!( + "unsupported reference argument type for calling JS function from Rust: {:?}", + arg + ), + } + Ok(()) + } + + fn outgoing_option(&mut self, arg: &Descriptor) -> Result<(), Error> { + match arg { + Descriptor::Anyref => { + // This is set to `undefined` in the `None` case and otherwise + // is the valid owned index. + self.instruction( + &[AdapterType::I32], + Instruction::AnyrefLoadOwned, + &[AdapterType::Anyref], + ); + } + Descriptor::I8 => self.out_option_sentinel(), + Descriptor::U8 => self.out_option_sentinel(), + Descriptor::I16 => self.out_option_sentinel(), + Descriptor::U16 => self.out_option_sentinel(), + Descriptor::I32 => self.option_native(true, ValType::I32), + Descriptor::U32 => self.option_native(false, ValType::I32), + Descriptor::F32 => self.option_native(true, ValType::F32), + Descriptor::F64 => self.option_native(true, ValType::F64), + Descriptor::I64 | Descriptor::U64 => { + let signed = match arg { + Descriptor::I64 => true, + _ => false, + }; + self.instruction( + &[AdapterType::I32; 3], + Instruction::Option64FromI32 { signed }, + &[AdapterType::Anyref], + ); + } + Descriptor::Boolean => { + self.instruction( + &[AdapterType::I32], + Instruction::OptionBoolFromI32, + &[AdapterType::Anyref], + ); + } + Descriptor::Char => { + self.instruction( + &[AdapterType::I32], + Instruction::OptionCharFromI32, + &[AdapterType::Anyref], + ); + } + Descriptor::Enum { hole } => { + self.instruction( + &[AdapterType::I32], + Instruction::OptionEnumFromI32 { hole: *hole }, + &[AdapterType::Anyref], + ); + } + Descriptor::RustStruct(name) => { + self.instruction( + &[AdapterType::I32], + Instruction::OptionRustFromI32 { + class: name.to_string(), + }, + &[AdapterType::Anyref], + ); + } + Descriptor::Ref(d) => self.outgoing_option_ref(false, d)?, + Descriptor::RefMut(d) => self.outgoing_option_ref(true, d)?, + + Descriptor::CachedString => self.cached_string(true, true)?, + + Descriptor::String | Descriptor::Vector(_) => { + let kind = arg.vector_kind().ok_or_else(|| { + format_err!( + "unsupported optional slice type for calling JS function from Rust {:?}", + arg + ) + })?; + let mem = self.cx.memory()?; + let free = self.cx.free()?; + self.instruction( + &[AdapterType::I32; 2], + Instruction::OptionVectorLoad { kind, mem, free }, + &[AdapterType::Anyref], + ); + } + + _ => bail!( + "unsupported optional argument type for calling JS function from Rust: {:?}", + arg + ), + } + Ok(()) + } + + fn outgoing_option_ref(&mut self, _mutable: bool, arg: &Descriptor) -> Result<(), Error> { + match arg { + Descriptor::Anyref => { + // If this is `Some` then it's the index, otherwise if it's + // `None` then it's the index pointing to undefined. + self.instruction( + &[AdapterType::I32], + Instruction::TableGet, + &[AdapterType::Anyref], + ); + } + Descriptor::CachedString => self.cached_string(true, false)?, + Descriptor::String | Descriptor::Slice(_) => { + let kind = arg.vector_kind().ok_or_else(|| { + format_err!( + "unsupported optional slice type for calling JS function from Rust {:?}", + arg + ) + })?; + let mem = self.cx.memory()?; + self.instruction( + &[AdapterType::I32; 2], + Instruction::OptionView { kind, mem }, + &[AdapterType::Anyref], + ); + } + _ => bail!( + "unsupported optional ref argument type for calling JS function from Rust: {:?}", + arg + ), + } + Ok(()) + } + + fn outgoing_i32(&mut self, output: AdapterType) { + let std = wit_walrus::Instruction::WasmToInt { + input: walrus::ValType::I32, + output: output.to_wit().unwrap(), + trap: false, + }; + self.instruction(&[AdapterType::I32], Instruction::Standard(std), &[output]); + } + + fn cached_string(&mut self, optional: bool, owned: bool) -> Result<(), Error> { + let mem = self.cx.memory()?; + let free = self.cx.free()?; + self.instruction( + &[AdapterType::I32; 2], + Instruction::CachedStringLoad { + owned, + optional, + mem, + free, + }, + &[AdapterType::String], + ); + Ok(()) + } + + fn option_native(&mut self, signed: bool, ty: ValType) { + self.instruction( + &[AdapterType::I32, AdapterType::from_wasm(ty).unwrap()], + Instruction::ToOptionNative { signed, ty }, + &[AdapterType::Anyref], + ); + } + + fn out_option_sentinel(&mut self) { + self.instruction( + &[AdapterType::I32], + Instruction::OptionU32Sentinel, + &[AdapterType::Anyref], + ); + } +} diff --git a/crates/cli-support/src/wit/section.rs b/crates/cli-support/src/wit/section.rs new file mode 100644 index 00000000..54c89e1a --- /dev/null +++ b/crates/cli-support/src/wit/section.rs @@ -0,0 +1,414 @@ +//! Support for generating a standard wasm interface types +//! +//! This module has all the necessary support for generating a full-fledged +//! standard wasm interface types section as defined by the `wit_walrus` +//! crate. This module also critically assumes that the WebAssembly module +//! being generated **must be standalone**. In this mode all sorts of features +//! supported by `#[wasm_bindgen]` aren't actually supported, such as closures, +//! imports of global js names, js getters/setters, exporting structs, etc. +//! These features may all eventually come to the standard bindings proposal, +//! but it will likely take some time. In the meantime this module simply focuses +//! on taking what's already a valid wasm module and letting it through with a +//! standard WebIDL custom section. All other modules generate an error during +//! this binding process. +//! +//! Note that when this function is called and used we're also not actually +//! generating any JS glue. Any JS glue currently generated is also invalid if +//! the module contains the wasm bindings section and it's actually respected. + +use crate::wit::{AdapterId, AdapterJsImportKind, AdapterType, Instruction}; +use crate::wit::{AdapterKind, NonstandardWitSection, WasmBindgenAux}; +use crate::wit::{AuxExport, InstructionData}; +use crate::wit::{AuxExportKind, AuxImport, AuxValue, JsImport, JsImportName}; +use anyhow::{anyhow, bail, Context, Error}; +use std::collections::HashMap; +use walrus::Module; + +pub fn add( + module: &mut Module, + aux: &WasmBindgenAux, + nonstandard: &NonstandardWitSection, +) -> Result<(), Error> { + let mut section = wit_walrus::WasmInterfaceTypes::default(); + let WasmBindgenAux { + extra_typescript: _, // ignore this even if it's specified + local_modules, + snippets, + package_jsons, + export_map, + import_map, + imports_with_catch, + imports_with_variadic, + imports_with_assert_no_shim: _, // not relevant for this purpose + enums, + structs, + anyref_table: _, // not relevant + anyref_alloc: _, // not relevant + anyref_drop_slice: _, // not relevant + } = aux; + + let adapter_context = |id: AdapterId| { + if let Some((name, _)) = nonstandard.exports.iter().find(|p| p.1 == id) { + return format!("in function export `{}`", name); + } + if let Some((core, _)) = nonstandard.implements.iter().find(|p| p.1 == id) { + let import = module.imports.get(*core); + return format!( + "in function import from `{}::{}`", + import.module, import.name + ); + } + unreachable!() + }; + + let mut us2walrus = HashMap::new(); + for (us, func) in nonstandard.adapters.iter() { + if let Some(export) = export_map.get(us) { + check_standard_export(export).context(adapter_context(*us))?; + } + if let Some(import) = import_map.get(us) { + check_standard_import(import).context(adapter_context(*us))?; + } + let params = translate_tys(&func.params).context(adapter_context(*us))?; + let results = translate_tys(&func.results).context(adapter_context(*us))?; + let ty = section.types.add(params, results); + let walrus = match &func.kind { + AdapterKind::Local { .. } => section.funcs.add_local(ty, Vec::new()), + AdapterKind::Import { + module, + name, + kind: AdapterJsImportKind::Normal, + } => section.add_import_func(module, name, ty).0, + AdapterKind::Import { + module, + name, + kind: AdapterJsImportKind::Constructor, + } => { + bail!( + "interfaces types doesn't support import of `{}::{}` \ + as a constructor", + module, + name + ); + } + AdapterKind::Import { + module, + name, + kind: AdapterJsImportKind::Method, + } => { + bail!( + "interfaces types doesn't support import of `{}::{}` \ + as a method", + module, + name + ); + } + }; + us2walrus.insert(*us, walrus); + } + + for (core, adapter) in nonstandard.implements.iter() { + let core = match module.imports.get(*core).kind { + walrus::ImportKind::Function(f) => f, + _ => bail!("cannot implement a non-function"), + }; + section.implements.add(us2walrus[adapter], core); + } + + for (name, adapter) in nonstandard.exports.iter() { + section.exports.add(name, us2walrus[adapter]); + } + + for (id, func) in nonstandard.adapters.iter() { + let instructions = match &func.kind { + AdapterKind::Local { instructions } => instructions, + AdapterKind::Import { .. } => continue, + }; + let result = match &mut section.funcs.get_mut(us2walrus[id]).kind { + wit_walrus::FuncKind::Local(i) => i, + _ => unreachable!(), + }; + + for instruction in instructions { + result.push( + translate_instruction(instruction, &us2walrus, module) + .with_context(|| adapter_context(*id))?, + ); + } + } + + if let Some((name, _)) = local_modules.iter().next() { + bail!( + "generating a bindings section is currently incompatible with \ + local JS modules being specified as well, `{}` cannot be used \ + since a standalone wasm file is being generated", + name, + ); + } + + if let Some((name, _)) = snippets.iter().filter(|(_, v)| !v.is_empty()).next() { + bail!( + "generating a bindings section is currently incompatible with \ + local JS snippets being specified as well, `{}` cannot be used \ + since a standalone wasm file is being generated", + name, + ); + } + + if let Some(path) = package_jsons.iter().next() { + bail!( + "generating a bindings section is currently incompatible with \ + package.json being consumed as well, `{}` cannot be used \ + since a standalone wasm file is being generated", + path.display(), + ); + } + + if let Some(id) = imports_with_catch.iter().next() { + bail!( + "{}\ngenerating a bindings section is currently incompatible with \ + `#[wasm_bindgen(catch)]`", + adapter_context(*id), + ); + } + + if let Some(id) = imports_with_variadic.iter().next() { + bail!( + "{}\ngenerating a bindings section is currently incompatible with \ + `#[wasm_bindgen(variadic)]`", + adapter_context(*id), + ); + } + + if let Some(enum_) = enums.iter().next() { + bail!( + "generating a bindings section is currently incompatible with \ + exporting an `enum` from the wasm file, cannot export `{}`", + enum_.name, + ); + } + + if let Some(struct_) = structs.iter().next() { + bail!( + "generating a bindings section is currently incompatible with \ + exporting a `struct` from the wasm file, cannot export `{}`", + struct_.name, + ); + } + + module.customs.add(section); + Ok(()) +} + +fn translate_instruction( + instr: &InstructionData, + us2walrus: &HashMap, + module: &Module, +) -> Result { + use Instruction::*; + + match &instr.instr { + Standard(s) => Ok(s.clone()), + CallAdapter(id) => { + let id = us2walrus[id]; + Ok(wit_walrus::Instruction::CallAdapter(id)) + } + CallExport(e) => match module.exports.get(*e).item { + walrus::ExportItem::Function(f) => Ok(wit_walrus::Instruction::CallCore(f)), + _ => bail!("can only call exported functions"), + }, + CallTableElement(e) => { + let table = module + .tables + .main_function_table()? + .ok_or_else(|| anyhow!("no function table found in module"))?; + let functions = match &module.tables.get(table).kind { + walrus::TableKind::Function(f) => f, + _ => unreachable!(), + }; + match functions.elements.get(*e as usize) { + Some(Some(f)) => Ok(wit_walrus::Instruction::CallCore(*f)), + _ => bail!("expected to find an element of the function table"), + } + } + StoreRetptr { .. } | LoadRetptr { .. } | Retptr => { + bail!("return pointers aren't supported in wasm interface types"); + } + I32FromBool | BoolFromI32 => { + bail!("booleans aren't supported in wasm interface types"); + } + I32FromStringFirstChar | StringFromChar => { + bail!("chars aren't supported in wasm interface types"); + } + I32FromAnyrefOwned | I32FromAnyrefBorrow | AnyrefLoadOwned | TableGet => { + bail!("anyref pass failed to sink into wasm module"); + } + I32FromAnyrefRustOwned { .. } | I32FromAnyrefRustBorrow { .. } | RustFromI32 { .. } => { + bail!("rust types aren't supported in wasm interface types"); + } + I32Split64 { .. } | I64FromLoHi { .. } => { + bail!("64-bit integers aren't supported in wasm-bindgen"); + } + I32SplitOption64 { .. } + | I32FromOptionAnyref + | I32FromOptionU32Sentinel + | I32FromOptionRust { .. } + | I32FromOptionBool + | I32FromOptionChar + | I32FromOptionEnum { .. } + | FromOptionNative { .. } + | OptionVector { .. } + | OptionRustFromI32 { .. } + | OptionVectorLoad { .. } + | OptionView { .. } + | OptionU32Sentinel + | ToOptionNative { .. } + | OptionBoolFromI32 + | OptionCharFromI32 + | OptionEnumFromI32 { .. } + | Option64FromI32 { .. } => { + bail!("optional types aren't supported in wasm bindgen"); + } + MutableSliceToMemory { .. } | VectorToMemory { .. } | VectorLoad { .. } | View { .. } => { + bail!("vector slices aren't supported in wasm interface types yet"); + } + CachedStringLoad { .. } => { + bail!("cached strings aren't supported in wasm interface types"); + } + StackClosure { .. } => { + bail!("closures aren't supported in wasm interface types"); + } + } +} + +fn check_standard_import(import: &AuxImport) -> Result<(), Error> { + let desc_js = |js: &JsImport| { + let mut extra = String::new(); + for field in js.fields.iter() { + extra.push_str("."); + extra.push_str(field); + } + match &js.name { + JsImportName::Global { name } | JsImportName::VendorPrefixed { name, .. } => { + format!("global `{}{}`", name, extra) + } + JsImportName::Module { module, name } => { + format!("`{}{}` from '{}'", name, extra, module) + } + JsImportName::LocalModule { module, name } => { + format!("`{}{}` from local module '{}'", name, extra, module) + } + JsImportName::InlineJs { + unique_crate_identifier, + name, + .. + } => format!( + "`{}{}` from inline js in '{}'", + name, extra, unique_crate_identifier + ), + } + }; + + let item = match import { + AuxImport::Value(AuxValue::Bare(js)) => { + if js.fields.len() == 0 { + if let JsImportName::Module { .. } = js.name { + return Ok(()); + } + } + desc_js(js) + } + AuxImport::Value(AuxValue::Getter(js, name)) + | AuxImport::Value(AuxValue::Setter(js, name)) + | AuxImport::Value(AuxValue::ClassGetter(js, name)) + | AuxImport::Value(AuxValue::ClassSetter(js, name)) => { + format!("field access of `{}` for {}", name, desc_js(js)) + } + AuxImport::ValueWithThis(js, method) => format!("method `{}.{}`", desc_js(js), method), + AuxImport::Instanceof(js) => format!("instance of check of {}", desc_js(js)), + AuxImport::Static(js) => format!("static js value {}", desc_js(js)), + AuxImport::StructuralMethod(name) => format!("structural method `{}`", name), + AuxImport::StructuralGetter(name) + | AuxImport::StructuralSetter(name) + | AuxImport::StructuralClassGetter(_, name) + | AuxImport::StructuralClassSetter(_, name) => { + format!("structural field access of `{}`", name) + } + AuxImport::IndexingDeleterOfClass(_) + | AuxImport::IndexingDeleterOfObject + | AuxImport::IndexingGetterOfClass(_) + | AuxImport::IndexingGetterOfObject + | AuxImport::IndexingSetterOfClass(_) + | AuxImport::IndexingSetterOfObject => format!("indexing getters/setters/deleters"), + AuxImport::WrapInExportedClass(name) => { + format!("wrapping a pointer in a `{}` js class wrapper", name) + } + AuxImport::Intrinsic(intrinsic) => { + format!("wasm-bindgen specific intrinsic `{}`", intrinsic.name()) + } + AuxImport::Closure { .. } => format!("creating a `Closure` wrapper"), + }; + bail!( + "cannot generate a standalone WebAssembly module which \ + contains an import of {} since it requires JS glue", + item + ); +} + +fn check_standard_export(export: &AuxExport) -> Result<(), Error> { + // First up make sure this is something that's actually valid to export + // form a vanilla WebAssembly module with WebIDL bindings. + match &export.kind { + AuxExportKind::Function(_) => Ok(()), + AuxExportKind::Constructor(name) => { + bail!( + "cannot export `{}` constructor function when generating \ + a standalone WebAssembly module with no JS glue", + name, + ); + } + AuxExportKind::Getter { class, field } => { + bail!( + "cannot export `{}::{}` getter function when generating \ + a standalone WebAssembly module with no JS glue", + class, + field, + ); + } + AuxExportKind::Setter { class, field } => { + bail!( + "cannot export `{}::{}` setter function when generating \ + a standalone WebAssembly module with no JS glue", + class, + field, + ); + } + AuxExportKind::StaticFunction { class, name } => { + bail!( + "cannot export `{}::{}` static function when \ + generating a standalone WebAssembly module with no \ + JS glue", + class, + name + ); + } + AuxExportKind::Method { class, name, .. } => { + bail!( + "cannot export `{}::{}` method when \ + generating a standalone WebAssembly module with no \ + JS glue", + class, + name + ); + } + } +} + +fn translate_tys(tys: &[AdapterType]) -> Result, Error> { + tys.iter() + .map(|ty| { + ty.to_wit() + .ok_or_else(|| anyhow!("type {:?} isn't supported in standard interface types", ty)) + }) + .collect() +} diff --git a/crates/cli-support/src/wit/standard.rs b/crates/cli-support/src/wit/standard.rs new file mode 100644 index 00000000..0388d9d1 --- /dev/null +++ b/crates/cli-support/src/wit/standard.rs @@ -0,0 +1,359 @@ +use crate::descriptor::VectorKind; +use std::borrow::Cow; +use std::collections::HashMap; +use walrus::{ImportId, TypedCustomSectionId}; + +#[derive(Default, Debug)] +pub struct NonstandardWitSection { + /// A list of adapter functions, keyed by their id. + pub adapters: HashMap, + + /// A list of pairs for adapter functions that implement core wasm imports. + pub implements: Vec<(ImportId, AdapterId)>, + + /// A list of adapter functions and the names they're exported under. + pub exports: Vec<(String, AdapterId)>, +} + +pub type NonstandardWitSectionId = TypedCustomSectionId; + +#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, PartialOrd, Ord)] +pub struct AdapterId(pub usize); + +#[derive(Debug, Clone)] +pub struct Adapter { + pub id: AdapterId, + pub params: Vec, + pub results: Vec, + pub kind: AdapterKind, +} + +#[derive(Debug, Clone)] +pub enum AdapterKind { + Local { + instructions: Vec, + }, + Import { + module: String, + name: String, + kind: AdapterJsImportKind, + }, +} + +#[derive(Debug, Clone)] +pub struct InstructionData { + pub instr: Instruction, + pub stack_change: StackChange, +} + +#[derive(Debug, Clone)] +pub enum StackChange { + Modified { pushed: usize, popped: usize }, + Unknown, +} + +#[derive(Debug, Copy, Clone, PartialEq)] +pub enum AdapterJsImportKind { + /// The first argument is an `anyref` which is the `this` of the function + /// call + Method, + /// The value imported should be invoked as `new` + Constructor, + /// A bland function import + Normal, +} + +#[derive(Debug, Clone, Copy)] +pub enum AdapterType { + S8, + S16, + S32, + S64, + U8, + U16, + U32, + U64, + F32, + F64, + String, + Anyref, + Bool, + I32, + I64, + Vector(VectorKind), +} + +#[derive(Debug, Clone)] +pub enum Instruction { + /// A known instruction in the "standard" + Standard(wit_walrus::Instruction), + + /// A call to one of our own defined adapters, similar to the standard + /// call-adapter instruction + CallAdapter(AdapterId), + /// Call an exported function in the core module + CallExport(walrus::ExportId), + /// Call an element in the function table of the core module + CallTableElement(u32), + + /// An instruction to store `ty` at the `offset` index in the return pointer + StoreRetptr { + ty: AdapterType, + offset: usize, + mem: walrus::MemoryId, + }, + /// An instruction to load `ty` at the `offset` index from the return pointer + LoadRetptr { + ty: AdapterType, + offset: usize, + mem: walrus::MemoryId, + }, + /// An instruction which pushes the return pointer onto the stack. + Retptr, + + /// Pops a `bool` from the stack and pushes an `i32` equivalent + I32FromBool, + /// Pops a `string` from the stack and pushes the first character as `i32` + I32FromStringFirstChar, + /// Pops an `anyref` from the stack, allocates space in the anyref table, + /// returns the index it was stored at. + I32FromAnyrefOwned, + /// Pops an `anyref` from the stack, pushes it onto the anyref wasm table + /// stack, and returns the index it was stored at. + I32FromAnyrefBorrow, + /// Pops an `anyref` from the stack, assumes it's a Rust class given, and + /// deallocates the JS object and returns the i32 Rust pointer. + I32FromAnyrefRustOwned { + class: String, + }, + /// Pops an `anyref` from the stack, assumes it's a Rust class given, and + /// passes the pointer to Rust which will be borrowed for the duration of a + /// call + I32FromAnyrefRustBorrow { + class: String, + }, + /// Pops an `anyref` from the stack, pushes 0 if it's "none" or the + /// consumed pointer value if it's "some". + I32FromOptionRust { + class: String, + }, + /// Pops an `s64` or `u64` from the stack, pushing two `i32` values. + I32Split64 { + signed: bool, + }, + /// Pops an `s64` or `u64` from the stack, pushing three `i32` values. + /// First is the "some/none" bit, and the next is the low bits, and the + /// next is the high bits. + I32SplitOption64 { + signed: bool, + }, + /// Pops an `anyref` from the stack, pushes either 0 if it's "none" or and + /// index into the owned wasm table it was stored at if it's "some" + I32FromOptionAnyref, + /// Pops an `anyref` from the stack, pushes either a sentinel value if it's + /// "none" or the integer value of it if it's "some" + I32FromOptionU32Sentinel, + /// Pops an `anyref` from the stack, pushes 0 for "none", 1 for + /// "some(false)', and 2 for "some(true)" + I32FromOptionBool, + /// Pops an `anyref` from the stack, pushes a sentinel for "none" or the + /// value if it's "some" + I32FromOptionChar, + /// Pops an `anyref` from the stack, pushes `hole` for "none" or the + /// value if it's "some" + I32FromOptionEnum { + hole: u32, + }, + /// Pops any anyref from the stack and then pushes two values. First is a + /// 0/1 if it's none/some and second is `ty` value if it was there or 0 if + /// it wasn't there. + FromOptionNative { + ty: walrus::ValType, + }, + + /// Pops a vector value of `kind` from the stack, allocates memory with + /// `malloc`, and then copies all the data into `mem`. Pushes the pointer + /// and length as i32. + VectorToMemory { + kind: VectorKind, + malloc: walrus::FunctionId, + mem: walrus::MemoryId, + }, + MutableSliceToMemory { + kind: VectorKind, + malloc: walrus::FunctionId, + free: walrus::FunctionId, + mem: walrus::MemoryId, + }, + + /// Pops an anyref, pushes pointer/length or all zeros + OptionVector { + kind: VectorKind, + malloc: walrus::FunctionId, + mem: walrus::MemoryId, + }, + + /// pops a `i32`, pushes `bool` + BoolFromI32, + /// pops `i32`, loads anyref at that slot, dealloates anyref, pushes `anyref` + AnyrefLoadOwned, + /// pops `i32`, pushes string from that `char` + StringFromChar, + /// pops two `i32`, pushes a 64-bit number + I64FromLoHi { + signed: bool, + }, + /// pops `i32`, pushes an anyref for the wrapped rust class + RustFromI32 { + class: String, + }, + OptionRustFromI32 { + class: String, + }, + /// pops ptr/length i32, loads string from cache + CachedStringLoad { + owned: bool, + optional: bool, + mem: walrus::MemoryId, + free: walrus::FunctionId, + }, + /// pops ptr/length, pushes a vector, frees the original data + VectorLoad { + kind: VectorKind, + mem: walrus::MemoryId, + free: walrus::FunctionId, + }, + /// pops ptr/length, pushes a vector, frees the original data + OptionVectorLoad { + kind: VectorKind, + mem: walrus::MemoryId, + free: walrus::FunctionId, + }, + /// pops i32, loads anyref from anyref table + TableGet, + /// pops two i32 data pointers, pushes an anyref closure + StackClosure { + adapter: AdapterId, + nargs: usize, + mutable: bool, + }, + /// pops two i32 data pointers, pushes a vector view + View { + kind: VectorKind, + mem: walrus::MemoryId, + }, + /// pops two i32 data pointers, pushes a vector view + OptionView { + kind: VectorKind, + mem: walrus::MemoryId, + }, + /// pops i32, pushes it viewed as an optional value with a known sentinel + OptionU32Sentinel, + /// pops an i32, then `ty`, then pushes anyref + ToOptionNative { + ty: walrus::ValType, + signed: bool, + }, + OptionBoolFromI32, + OptionCharFromI32, + OptionEnumFromI32 { + hole: u32, + }, + Option64FromI32 { + signed: bool, + }, +} + +impl AdapterType { + pub fn from_wit(wit: wit_walrus::ValType) -> AdapterType { + match wit { + wit_walrus::ValType::S8 => AdapterType::S8, + wit_walrus::ValType::S16 => AdapterType::S16, + wit_walrus::ValType::S32 => AdapterType::S32, + wit_walrus::ValType::S64 => AdapterType::S64, + wit_walrus::ValType::U8 => AdapterType::U8, + wit_walrus::ValType::U16 => AdapterType::U16, + wit_walrus::ValType::U32 => AdapterType::U32, + wit_walrus::ValType::U64 => AdapterType::U64, + wit_walrus::ValType::F32 => AdapterType::F32, + wit_walrus::ValType::F64 => AdapterType::F64, + wit_walrus::ValType::String => AdapterType::String, + wit_walrus::ValType::Anyref => AdapterType::Anyref, + wit_walrus::ValType::I32 => AdapterType::I32, + wit_walrus::ValType::I64 => AdapterType::I64, + } + } + + pub fn from_wasm(wasm: walrus::ValType) -> Option { + Some(match wasm { + walrus::ValType::I32 => AdapterType::I32, + walrus::ValType::I64 => AdapterType::I64, + walrus::ValType::F32 => AdapterType::F32, + walrus::ValType::F64 => AdapterType::F64, + walrus::ValType::Anyref => AdapterType::Anyref, + walrus::ValType::V128 => return None, + }) + } + + pub fn to_wasm(&self) -> Option { + Some(match self { + AdapterType::I32 => walrus::ValType::I32, + AdapterType::I64 => walrus::ValType::I64, + AdapterType::F32 => walrus::ValType::F32, + AdapterType::F64 => walrus::ValType::F64, + AdapterType::Anyref => walrus::ValType::Anyref, + _ => return None, + }) + } + + pub fn to_wit(&self) -> Option { + Some(match self { + AdapterType::S8 => wit_walrus::ValType::S8, + AdapterType::S16 => wit_walrus::ValType::S16, + AdapterType::S32 => wit_walrus::ValType::S32, + AdapterType::S64 => wit_walrus::ValType::S64, + AdapterType::U8 => wit_walrus::ValType::U8, + AdapterType::U16 => wit_walrus::ValType::U16, + AdapterType::U32 => wit_walrus::ValType::U32, + AdapterType::U64 => wit_walrus::ValType::U64, + AdapterType::F32 => wit_walrus::ValType::F32, + AdapterType::F64 => wit_walrus::ValType::F64, + AdapterType::String => wit_walrus::ValType::String, + AdapterType::Anyref => wit_walrus::ValType::Anyref, + AdapterType::I32 => wit_walrus::ValType::I32, + AdapterType::I64 => wit_walrus::ValType::I64, + AdapterType::Bool | AdapterType::Vector(_) => return None, + }) + } +} + +impl NonstandardWitSection { + pub fn append( + &mut self, + params: Vec, + results: Vec, + kind: AdapterKind, + ) -> AdapterId { + let id = AdapterId(self.adapters.len()); + self.adapters.insert( + id, + Adapter { + id, + params, + results, + kind, + }, + ); + return id; + } +} + +impl walrus::CustomSection for NonstandardWitSection { + fn name(&self) -> &str { + "nonstandard wit section" + } + + fn data(&self, _: &walrus::IdsToIndices) -> Cow<[u8]> { + panic!("shouldn't emit custom sections just yet"); + } +} diff --git a/crates/cli/Cargo.toml b/crates/cli/Cargo.toml index 0fb5c867..489e31b6 100644 --- a/crates/cli/Cargo.toml +++ b/crates/cli/Cargo.toml @@ -24,7 +24,7 @@ rouille = { version = "3.0.0", default-features = false } serde = { version = "1.0", features = ['derive'] } serde_derive = "1.0" serde_json = "1.0" -walrus = { version = "0.13.0", features = ['parallel'] } +walrus = { version = "0.14.0", features = ['parallel'] } wasm-bindgen-cli-support = { path = "../cli-support", version = "=0.2.55" } wasm-bindgen-shared = { path = "../shared", version = "=0.2.55" } diff --git a/crates/multi-value-xform/Cargo.toml b/crates/multi-value-xform/Cargo.toml index 18399d18..16a35edd 100644 --- a/crates/multi-value-xform/Cargo.toml +++ b/crates/multi-value-xform/Cargo.toml @@ -13,4 +13,14 @@ edition = "2018" [dependencies] anyhow = "1.0" -walrus = "0.13.0" +walrus = "0.14.0" + +[dev-dependencies] +rayon = "1.0" +wasmprinter = "0.2" +wast = "3.0" +wat = "1.0" + +[[test]] +name = "all" +harness = false diff --git a/crates/multi-value-xform/src/lib.rs b/crates/multi-value-xform/src/lib.rs index 2bcd974a..75e7c262 100644 --- a/crates/multi-value-xform/src/lib.rs +++ b/crates/multi-value-xform/src/lib.rs @@ -108,23 +108,27 @@ /// return pointer parameter that will be removed. The `Vec` /// is the new result type that will be returned directly instead of via the /// return pointer. +/// +/// Returns a list of wrappers which have multi value signatures and call the +/// corresponding element in the `to_xform` list. pub fn run( module: &mut walrus::Module, memory: walrus::MemoryId, shadow_stack_pointer: walrus::GlobalId, - to_xform: &[(walrus::ExportId, usize, &[walrus::ValType])], -) -> Result<(), anyhow::Error> { - for &(export, return_pointer_index, results) in to_xform { - xform_one( + to_xform: &[(walrus::FunctionId, usize, Vec)], +) -> Result, anyhow::Error> { + let mut wrappers = Vec::new(); + for (func, return_pointer_index, results) in to_xform { + wrappers.push(xform_one( module, memory, shadow_stack_pointer, - export, - return_pointer_index, + *func, + *return_pointer_index, results, - )?; + )?); } - Ok(()) + Ok(wrappers) } // Ensure that `n` is aligned to `align`, rounding up as necessary. @@ -137,19 +141,14 @@ fn xform_one( module: &mut walrus::Module, memory: walrus::MemoryId, shadow_stack_pointer: walrus::GlobalId, - export: walrus::ExportId, + func: walrus::FunctionId, return_pointer_index: usize, results: &[walrus::ValType], -) -> Result<(), anyhow::Error> { +) -> Result { if module.globals.get(shadow_stack_pointer).ty != walrus::ValType::I32 { anyhow::bail!("shadow stack pointer global does not have type `i32`"); } - let func = match module.exports.get(export).item { - walrus::ExportItem::Function(f) => f, - _ => anyhow::bail!("can only multi-value transform exported functions, found non-function"), - }; - // Compute the total size of all results, potentially with padding to ensure // that each result is aligned. let mut results_size = 0; @@ -300,13 +299,7 @@ fn xform_one( module.funcs.get_mut(wrapper).name = Some(format!("{} multivalue shim", name)); } - // Replace the old export with our new multi-value wrapper for it! - match module.exports.get_mut(export).item { - walrus::ExportItem::Function(ref mut f) => *f = wrapper, - _ => unreachable!(), - } - - Ok(()) + Ok(wrapper) } #[cfg(test)] diff --git a/crates/multi-value-xform/tests/align.wat b/crates/multi-value-xform/tests/align.wat new file mode 100644 index 00000000..384d31da --- /dev/null +++ b/crates/multi-value-xform/tests/align.wat @@ -0,0 +1,37 @@ +;; @xform export "foo" (f64 i32 i64) + +(module + (global (mut i32) (i32.const 0)) + (memory 1) + + (func $foo (export "foo") (param i32)) +) + +(; CHECK-ALL: +(module + (type (;0;) (func (result f64 i32 i64))) + (type (;1;) (func (param i32))) + (func $foo multivalue shim (type 0) (result f64 i32 i64) + (local i32) + global.get 0 + i32.const 32 + i32.sub + local.tee 0 + global.set 0 + local.get 0 + call $foo + local.get 0 + f64.load + local.get 0 + i32.load offset=8 + local.get 0 + i64.load offset=16 + local.get 0 + i32.const 32 + i32.add + global.set 0) + (func $foo (type 1) (param i32)) + (memory (;0;) 1) + (global (;0;) (mut i32) (i32.const 0)) + (export "foo" (func $foo multivalue shim))) +;) diff --git a/crates/multi-value-xform/tests/all.rs b/crates/multi-value-xform/tests/all.rs new file mode 100644 index 00000000..588abd26 --- /dev/null +++ b/crates/multi-value-xform/tests/all.rs @@ -0,0 +1,225 @@ +//! A small test framework to execute a test function over all files in a +//! directory. +//! +//! Each file in the directory has its own `CHECK-ALL` annotation indicating the +//! expected output of the test. That can be automatically updated with +//! `BLESS=1` in the environment. Otherwise the test are checked against the +//! listed expectation. + +use anyhow::{bail, Context, Result}; +use rayon::prelude::*; +use std::env; +use std::fs; +use std::path::{Path, PathBuf}; +use wast::parser::{Parse, Parser}; + +fn main() { + run("tests".as_ref(), runtest); +} + +fn runtest(test: &Test) -> Result { + let wasm = wat::parse_file(&test.file)?; + let mut walrus = walrus::Module::from_buffer(&wasm)?; + let mut exports = Vec::new(); + let mut xforms = Vec::new(); + for directive in test.directives.iter() { + let export = walrus + .exports + .iter() + .find(|e| e.name == directive.name) + .unwrap(); + let id = match export.item { + walrus::ExportItem::Function(id) => id, + _ => panic!("must be function export"), + }; + exports.push(export.id()); + xforms.push((id, 0, directive.tys.clone())); + } + let memory = walrus.memories.iter().next().unwrap().id(); + let stack_pointer = walrus.globals.iter().next().unwrap().id(); + let ret = wasm_bindgen_multi_value_xform::run(&mut walrus, memory, stack_pointer, &xforms)?; + for (export, id) in exports.into_iter().zip(ret) { + walrus.exports.get_mut(export).item = walrus::ExportItem::Function(id); + } + walrus::passes::gc::run(&mut walrus); + let printed = wasmprinter::print_bytes(&walrus.emit_wasm())?; + Ok(printed) +} + +fn run(dir: &Path, run: fn(&Test) -> Result) { + let mut tests = Vec::new(); + find_tests(dir, &mut tests); + let filter = std::env::args().nth(1); + + let bless = env::var("BLESS").is_ok(); + let tests = tests + .iter() + .filter(|test| { + if let Some(filter) = &filter { + if let Some(s) = test.file_name().and_then(|s| s.to_str()) { + if !s.contains(filter) { + return false; + } + } + } + true + }) + .collect::>(); + + println!("\nrunning {} tests\n", tests.len()); + + let errors = tests + .par_iter() + .filter_map(|test| run_test(test, bless, run).err()) + .collect::>(); + + if !errors.is_empty() { + for msg in errors.iter() { + eprintln!("error: {:?}", msg); + } + + panic!("{} tests failed", errors.len()) + } + + println!("test result: ok. {} passed\n", tests.len()); +} + +fn run_test(test: &Path, bless: bool, run: fn(&Test) -> anyhow::Result) -> Result<()> { + (|| -> Result<_> { + let expected = Test::from_file(test)?; + let actual = run(&expected)?; + expected.check(&actual, bless)?; + Ok(()) + })() + .context(format!("test failed - {}", test.display()))?; + Ok(()) +} + +fn find_tests(path: &Path, tests: &mut Vec) { + for f in path.read_dir().unwrap() { + let f = f.unwrap(); + if f.file_type().unwrap().is_dir() { + find_tests(&f.path(), tests); + continue; + } + match f.path().extension().and_then(|s| s.to_str()) { + Some("wat") => {} + _ => continue, + } + tests.push(f.path()); + } +} + +struct Test { + file: PathBuf, + directives: Vec, + assertion: Option, +} + +struct Directive { + name: String, + tys: Vec, +} + +impl Test { + fn from_file(path: &Path) -> Result { + let contents = fs::read_to_string(path)?; + let mut iter = contents.lines(); + let mut assertion = None; + let mut directives = Vec::new(); + while let Some(line) = iter.next() { + if line.starts_with("(; CHECK-ALL:") { + let mut pattern = String::new(); + while let Some(line) = iter.next() { + if line == ";)" { + break; + } + pattern.push_str(line); + pattern.push_str("\n"); + } + while pattern.ends_with("\n") { + pattern.pop(); + } + if iter.next().is_some() { + bail!("CHECK-ALL must be at the end of the file"); + } + assertion = Some(pattern); + continue; + } + + if !line.starts_with(";; @xform") { + continue; + } + let directive = &line[9..]; + let buf = wast::parser::ParseBuffer::new(directive)?; + directives.push(wast::parser::parse::(&buf)?); + } + Ok(Test { + file: path.to_path_buf(), + directives, + assertion, + }) + } + + fn check(&self, output: &str, bless: bool) -> Result<()> { + if bless { + update_output(&self.file, output) + } else if let Some(pattern) = &self.assertion { + if output == pattern { + return Ok(()); + } + bail!( + "expected\n {}\n\nactual\n {}", + pattern.replace("\n", "\n "), + output.replace("\n", "\n ") + ); + } else { + bail!( + "no test assertions were found in this file, but you can \ + rerun tests with `BLESS=1` to automatically add assertions \ + to this file" + ); + } + } +} + +fn update_output(path: &Path, output: &str) -> Result<()> { + let contents = fs::read_to_string(path)?; + let start = contents.find("(; CHECK-ALL:").unwrap_or(contents.len()); + + let mut new_output = String::new(); + for line in output.lines() { + new_output.push_str(line); + new_output.push_str("\n"); + } + let new = format!( + "{}\n\n(; CHECK-ALL:\n{}\n;)\n", + contents[..start].trim(), + new_output.trim_end() + ); + fs::write(path, new)?; + Ok(()) +} + +impl<'a> Parse<'a> for Directive { + fn parse(parser: Parser<'a>) -> wast::parser::Result { + use wast::{kw, ValType}; + + parser.parse::()?; + let name = parser.parse()?; + let mut tys = Vec::new(); + parser.parens(|p| { + while !p.is_empty() { + tys.push(match p.parse()? { + ValType::I32 => walrus::ValType::I32, + ValType::I64 => walrus::ValType::I64, + ValType::F32 => walrus::ValType::F32, + ValType::F64 => walrus::ValType::F64, + _ => panic!(), + }); + } + Ok(()) + })?; + Ok(Directive { name, tys }) + } +} diff --git a/crates/multi-value-xform/tests/many.wat b/crates/multi-value-xform/tests/many.wat new file mode 100644 index 00000000..ea48d8ae --- /dev/null +++ b/crates/multi-value-xform/tests/many.wat @@ -0,0 +1,39 @@ +;; @xform export "foo" (i32 f32 f64 i64) + +(module + (global (mut i32) (i32.const 0)) + (memory 1) + + (func $foo (export "foo") (param i32)) +) + +(; CHECK-ALL: +(module + (type (;0;) (func (result i32 f32 f64 i64))) + (type (;1;) (func (param i32))) + (func $foo multivalue shim (type 0) (result i32 f32 f64 i64) + (local i32) + global.get 0 + i32.const 32 + i32.sub + local.tee 0 + global.set 0 + local.get 0 + call $foo + local.get 0 + i32.load + local.get 0 + f32.load offset=4 + local.get 0 + f64.load offset=8 + local.get 0 + i64.load offset=16 + local.get 0 + i32.const 32 + i32.add + global.set 0) + (func $foo (type 1) (param i32)) + (memory (;0;) 1) + (global (;0;) (mut i32) (i32.const 0)) + (export "foo" (func $foo multivalue shim))) +;) diff --git a/crates/multi-value-xform/tests/simple.wat b/crates/multi-value-xform/tests/simple.wat new file mode 100644 index 00000000..c1ebc23d --- /dev/null +++ b/crates/multi-value-xform/tests/simple.wat @@ -0,0 +1,93 @@ +;; @xform export "i32" (i32) +;; @xform export "i64" (i64) +;; @xform export "f32" (f32) +;; @xform export "f64" (f64) + +(module + (global (mut i32) (i32.const 0)) + (memory 1) + + (func $i32 (export "i32") (param i32)) + (func $i64 (export "i64") (param i32)) + (func $f32 (export "f32") (param i32)) + (func $f64 (export "f64") (param i32)) +) + +(; CHECK-ALL: +(module + (type (;0;) (func (result i32))) + (type (;1;) (func (result i64))) + (type (;2;) (func (result f32))) + (type (;3;) (func (result f64))) + (type (;4;) (func (param i32))) + (func $i32 multivalue shim (type 0) (result i32) + (local i32) + global.get 0 + i32.const 16 + i32.sub + local.tee 0 + global.set 0 + local.get 0 + call $i32 + local.get 0 + i32.load + local.get 0 + i32.const 16 + i32.add + global.set 0) + (func $i64 multivalue shim (type 1) (result i64) + (local i32) + global.get 0 + i32.const 16 + i32.sub + local.tee 0 + global.set 0 + local.get 0 + call $i64 + local.get 0 + i64.load + local.get 0 + i32.const 16 + i32.add + global.set 0) + (func $f32 multivalue shim (type 2) (result f32) + (local i32) + global.get 0 + i32.const 16 + i32.sub + local.tee 0 + global.set 0 + local.get 0 + call $f32 + local.get 0 + f32.load + local.get 0 + i32.const 16 + i32.add + global.set 0) + (func $f64 multivalue shim (type 3) (result f64) + (local i32) + global.get 0 + i32.const 16 + i32.sub + local.tee 0 + global.set 0 + local.get 0 + call $f64 + local.get 0 + f64.load + local.get 0 + i32.const 16 + i32.add + global.set 0) + (func $i32 (type 4) (param i32)) + (func $i64 (type 4) (param i32)) + (func $f32 (type 4) (param i32)) + (func $f64 (type 4) (param i32)) + (memory (;0;) 1) + (global (;0;) (mut i32) (i32.const 0)) + (export "i32" (func $i32 multivalue shim)) + (export "i64" (func $i64 multivalue shim)) + (export "f32" (func $f32 multivalue shim)) + (export "f64" (func $f64 multivalue shim))) +;) diff --git a/crates/threads-xform/Cargo.toml b/crates/threads-xform/Cargo.toml index a104af45..e3132153 100644 --- a/crates/threads-xform/Cargo.toml +++ b/crates/threads-xform/Cargo.toml @@ -13,5 +13,5 @@ edition = "2018" [dependencies] anyhow = "1.0" -walrus = "0.13.0" +walrus = "0.14.0" wasm-bindgen-wasm-conventions = { path = "../wasm-conventions", version = "=0.2.55" } diff --git a/crates/wasm-conventions/Cargo.toml b/crates/wasm-conventions/Cargo.toml index f6d41564..3ece2d96 100644 --- a/crates/wasm-conventions/Cargo.toml +++ b/crates/wasm-conventions/Cargo.toml @@ -10,5 +10,5 @@ description = "Utilities for working with Wasm codegen conventions (usually esta edition = "2018" [dependencies] -walrus = "0.13.0" +walrus = "0.14.0" anyhow = "1.0" diff --git a/crates/wasm-interpreter/Cargo.toml b/crates/wasm-interpreter/Cargo.toml index 4fe0c6f1..f2ac7b8f 100644 --- a/crates/wasm-interpreter/Cargo.toml +++ b/crates/wasm-interpreter/Cargo.toml @@ -14,7 +14,7 @@ edition = '2018' [dependencies] anyhow = "1.0" log = "0.4" -walrus = "0.13.0" +walrus = "0.14.0" [dev-dependencies] tempfile = "3" diff --git a/src/convert/impls.rs b/src/convert/impls.rs index 91d08dec..89f74dbf 100644 --- a/src/convert/impls.rs +++ b/src/convert/impls.rs @@ -51,7 +51,6 @@ unsafe impl WasmAbi for Wasm64 {} #[repr(C)] pub struct WasmOptional64 { pub present: u32, - pub padding: u32, pub low: u32, pub high: u32, } @@ -177,13 +176,11 @@ macro_rules! type_64 { match self { None => WasmOptional64 { present: 0, - padding: 0, low: 0 as u32, high: 0 as u32, }, Some(me) => WasmOptional64 { present: 1, - padding: 0, low: me as u32, high: (me >> 32) as u32, }, diff --git a/src/convert/mod.rs b/src/convert/mod.rs index fc249948..ce2c0b2c 100644 --- a/src/convert/mod.rs +++ b/src/convert/mod.rs @@ -6,5 +6,6 @@ mod impls; mod slices; mod traits; +pub use self::impls::*; pub use self::slices::WasmSlice; pub use self::traits::*; diff --git a/src/lib.rs b/src/lib.rs index 2d662fd9..3b54fb47 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -14,7 +14,7 @@ use core::marker; use core::mem; use core::ops::{Deref, DerefMut}; -use crate::convert::FromWasmAbi; +use crate::convert::{FromWasmAbi, WasmOptionalF64, WasmSlice}; macro_rules! if_std { ($($i:item)*) => ($( @@ -226,10 +226,8 @@ impl JsValue { T: for<'a> serde::de::Deserialize<'a>, { unsafe { - let mut ret = [0usize; 2]; - __wbindgen_json_serialize(&mut ret, self.idx); - let s = Vec::from_raw_parts(ret[0] as *mut u8, ret[1], ret[1]); - let s = String::from_utf8_unchecked(s); + let ret = __wbindgen_json_serialize(self.idx); + let s = String::from_abi(ret); serde_json::from_str(&s) } } @@ -240,15 +238,7 @@ impl JsValue { /// If this JS value is not an instance of a number then this returns /// `None`. pub fn as_f64(&self) -> Option { - let mut invalid = 0; - unsafe { - let ret = __wbindgen_number_get(self.idx, &mut invalid); - if invalid == 1 { - None - } else { - Some(ret) - } - } + unsafe { FromWasmAbi::from_abi(__wbindgen_number_get(self.idx)) } } /// Tests whether this JS value is a JS string. @@ -278,16 +268,7 @@ impl JsValue { /// [caveats]: https://rustwasm.github.io/docs/wasm-bindgen/reference/types/str.html #[cfg(feature = "std")] pub fn as_string(&self) -> Option { - unsafe { - let mut len = 0; - let ptr = __wbindgen_string_get(self.idx, &mut len); - if ptr.is_null() { - None - } else { - let data = Vec::from_raw_parts(ptr, len, len); - Some(String::from_utf8_unchecked(data)) - } - } + unsafe { FromWasmAbi::from_abi(__wbindgen_string_get(self.idx)) } } /// Returns the `bool` value of this JS value if it's an instance of a @@ -523,9 +504,9 @@ externs! { fn __wbindgen_is_string(idx: u32) -> u32; fn __wbindgen_is_falsy(idx: u32) -> u32; - fn __wbindgen_number_get(idx: u32, invalid: *mut u8) -> f64; + fn __wbindgen_number_get(idx: u32) -> WasmOptionalF64; fn __wbindgen_boolean_get(idx: u32) -> u32; - fn __wbindgen_string_get(idx: u32, len: *mut usize) -> *mut u8; + fn __wbindgen_string_get(idx: u32) -> WasmSlice; fn __wbindgen_debug_string(ret: *mut [usize; 2], idx: u32) -> (); @@ -539,7 +520,7 @@ externs! { fn __wbindgen_describe_closure(a: u32, b: u32, c: u32) -> u32; fn __wbindgen_json_parse(ptr: *const u8, len: usize) -> u32; - fn __wbindgen_json_serialize(ret: *mut [usize; 2], idx: u32) -> (); + fn __wbindgen_json_serialize(idx: u32) -> WasmSlice; fn __wbindgen_jsval_eq(a: u32, b: u32) -> u32; fn __wbindgen_memory() -> u32; diff --git a/tests/wasm/imports.js b/tests/wasm/imports.js index 6d11ccb5..44182a38 100644 --- a/tests/wasm/imports.js +++ b/tests/wasm/imports.js @@ -115,3 +115,15 @@ class StaticMethodCheck { } exports.StaticMethodCheck = StaticMethodCheck; + +exports.receive_undefined = val => { + assert.strictEqual(val, undefined); +}; + +const VAL = {}; + +exports.receive_some = val => { + assert.strictEqual(val, VAL); +}; + +exports.get_some_val = () => VAL; diff --git a/tests/wasm/imports.rs b/tests/wasm/imports.rs index 4dc2b401..116c7aff 100644 --- a/tests/wasm/imports.rs +++ b/tests/wasm/imports.rs @@ -57,6 +57,18 @@ extern "C" { fn static_method_of_right_this(); static STATIC_STRING: String; + + #[derive(Clone)] + type PassOutOptionUndefined; + fn get_some_val() -> PassOutOptionUndefined; + #[wasm_bindgen(js_name = "receive_undefined")] + fn receive_undefined_ref(arg: Option<&PassOutOptionUndefined>); + #[wasm_bindgen(js_name = "receive_undefined")] + fn receive_undefined_owned(arg: Option); + #[wasm_bindgen(js_name = "receive_some")] + fn receive_some_ref(arg: Option<&PassOutOptionUndefined>); + #[wasm_bindgen(js_name = "receive_some")] + fn receive_some_owned(arg: Option); } #[wasm_bindgen] @@ -248,3 +260,17 @@ fn static_string_ok() { fn static_method_of_has_right_this() { StaticMethodCheck::static_method_of_right_this(); } + +#[wasm_bindgen_test] +fn pass_out_options_as_undefined() { + receive_undefined_ref(None); + receive_undefined_ref(None); + receive_undefined_owned(None); + receive_undefined_owned(None); + + let v = get_some_val(); + receive_some_ref(Some(&v)); + receive_some_ref(Some(&v)); + receive_some_owned(Some(v.clone())); + receive_some_owned(Some(v)); +} diff --git a/tests/wasm/slice.js b/tests/wasm/slice.js index 373e3859..c23535e9 100644 --- a/tests/wasm/slice.js +++ b/tests/wasm/slice.js @@ -41,63 +41,27 @@ exports.js_export = () => { assert.deepStrictEqual(wasm.export_f64(f64), f64); }; -exports.import_js_i8 = a => { +const test_import = (a, b, c) => { assert.strictEqual(a.length, 2); assert.strictEqual(a[0], 1); assert.strictEqual(a[1], 2); + assert.strictEqual(b.length, 2); + assert.strictEqual(b[0], 1); + assert.strictEqual(b[1], 2); + assert.strictEqual(c, undefined); return a; }; -exports.import_js_u8 = a => { - assert.strictEqual(a.length, 2); - assert.strictEqual(a[0], 1); - assert.strictEqual(a[1], 2); - return a; -}; - -exports.import_js_i16 = a => { - assert.strictEqual(a.length, 2); - assert.strictEqual(a[0], 1); - assert.strictEqual(a[1], 2); - return a; -}; - -exports.import_js_u16 = a => { - assert.strictEqual(a.length, 2); - assert.strictEqual(a[0], 1); - assert.strictEqual(a[1], 2); - return a; -}; - -exports.import_js_i32 = a => { - assert.strictEqual(a.length, 2); - assert.strictEqual(a[0], 1); - assert.strictEqual(a[1], 2); - return a; -}; -exports.import_js_isize = exports.import_js_i32; - -exports.import_js_u32 = a => { - assert.strictEqual(a.length, 2); - assert.strictEqual(a[0], 1); - assert.strictEqual(a[1], 2); - return a; -}; -exports.import_js_usize = exports.import_js_u32; - -exports.import_js_f32 = a => { - assert.strictEqual(a.length, 2); - assert.strictEqual(a[0], 1); - assert.strictEqual(a[1], 2); - return a; -}; - -exports.import_js_f64 = a => { - assert.strictEqual(a.length, 2); - assert.strictEqual(a[0], 1); - assert.strictEqual(a[1], 2); - return a; -}; +exports.import_js_i8 = test_import; +exports.import_js_u8 = test_import; +exports.import_js_i16 = test_import; +exports.import_js_u16 = test_import; +exports.import_js_i32 = test_import; +exports.import_js_isize = test_import; +exports.import_js_u32 = test_import; +exports.import_js_usize = test_import; +exports.import_js_f32 = test_import; +exports.import_js_f64 = test_import; exports.js_import = () => { const i8 = new Int8Array(2); @@ -152,12 +116,19 @@ exports.js_pass_array = () => { wasm.pass_array_rust_f64([1, 2]); }; -const import_mut_foo = a => { +const import_mut_foo = (a, b, c) => { assert.strictEqual(a.length, 3); assert.strictEqual(a[0], 1); assert.strictEqual(a[1], 2); a[0] = 4; a[1] = 5; + assert.strictEqual(b.length, 3); + assert.strictEqual(b[0], 4); + assert.strictEqual(b[1], 5); + assert.strictEqual(b[2], 6); + b[0] = 8; + b[1] = 7; + assert.strictEqual(c, undefined); }; exports.import_mut_js_i8 = import_mut_foo; diff --git a/tests/wasm/slice.rs b/tests/wasm/slice.rs index 8e299c0f..2f2457bc 100644 --- a/tests/wasm/slice.rs +++ b/tests/wasm/slice.rs @@ -55,7 +55,7 @@ macro_rules! import_macro { ($(($rust:ident, $js:ident, $i:ident))*) => ($( #[wasm_bindgen(module = "tests/wasm/slice.js")] extern "C" { - fn $js(a: &[$i]) -> Vec<$i>; + fn $js(a: &[$i], b: Option<&[$i]>, c: Option<&[$i]>) -> Vec<$i>; } #[wasm_bindgen] @@ -63,7 +63,7 @@ macro_rules! import_macro { assert_eq!(a.len(), 2); assert_eq!(a[0], 1 as $i); assert_eq!(a[1], 2 as $i); - $js(a) + $js(a, Some(a), None) } )*) } @@ -120,19 +120,27 @@ macro_rules! import_mut_macro { $( #[wasm_bindgen(module = "tests/wasm/slice.js")] extern "C" { - fn $js(a: &mut [$i]); + fn $js(a: &mut [$i], b: Option<&mut [$i]>, c: Option<&mut [$i]>); } fn $rust() { - let mut buf = [ + let mut buf1 = [ 1 as $i, 2 as $i, 3 as $i, ]; - $js(&mut buf); - assert_eq!(buf[0], 4 as $i); - assert_eq!(buf[1], 5 as $i); - assert_eq!(buf[2], 3 as $i); + let mut buf2 = [ + 4 as $i, + 5 as $i, + 6 as $i, + ]; + $js(&mut buf1, Some(&mut buf2), None); + assert_eq!(buf1[0], 4 as $i); + assert_eq!(buf1[1], 5 as $i); + assert_eq!(buf1[2], 3 as $i); + assert_eq!(buf2[0], 8 as $i); + assert_eq!(buf2[1], 7 as $i); + assert_eq!(buf2[2], 6 as $i); } )*