diff --git a/Cargo.lock b/Cargo.lock index 1ee756ad3..be6f3ea3d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -287,7 +287,7 @@ dependencies = [ [[package]] name = "cranelift-bforest" version = "0.90.0" -source = "git+https://github.com/bytecodealliance/wasmtime?branch=main#f96491f3337a8ccb792b2769b016de6c16022ede" +source = "git+https://github.com/bytecodealliance/wasmtime?branch=main#ff0c45b4a069a84b131c8265d8d14c2f7d0566d7" dependencies = [ "cranelift-entity", ] @@ -295,7 +295,7 @@ dependencies = [ [[package]] name = "cranelift-codegen" version = "0.90.0" -source = "git+https://github.com/bytecodealliance/wasmtime?branch=main#f96491f3337a8ccb792b2769b016de6c16022ede" +source = "git+https://github.com/bytecodealliance/wasmtime?branch=main#ff0c45b4a069a84b131c8265d8d14c2f7d0566d7" dependencies = [ "arrayvec", "bumpalo", @@ -315,7 +315,7 @@ dependencies = [ [[package]] name = "cranelift-codegen-meta" version = "0.90.0" -source = "git+https://github.com/bytecodealliance/wasmtime?branch=main#f96491f3337a8ccb792b2769b016de6c16022ede" +source = "git+https://github.com/bytecodealliance/wasmtime?branch=main#ff0c45b4a069a84b131c8265d8d14c2f7d0566d7" dependencies = [ "cranelift-codegen-shared", ] @@ -323,12 +323,12 @@ dependencies = [ [[package]] name = "cranelift-codegen-shared" version = "0.90.0" -source = "git+https://github.com/bytecodealliance/wasmtime?branch=main#f96491f3337a8ccb792b2769b016de6c16022ede" +source = "git+https://github.com/bytecodealliance/wasmtime?branch=main#ff0c45b4a069a84b131c8265d8d14c2f7d0566d7" [[package]] name = "cranelift-egraph" version = "0.90.0" -source = "git+https://github.com/bytecodealliance/wasmtime?branch=main#f96491f3337a8ccb792b2769b016de6c16022ede" +source = "git+https://github.com/bytecodealliance/wasmtime?branch=main#ff0c45b4a069a84b131c8265d8d14c2f7d0566d7" dependencies = [ "cranelift-entity", "fxhash", @@ -341,7 +341,7 @@ dependencies = [ [[package]] name = "cranelift-entity" version = "0.90.0" -source = "git+https://github.com/bytecodealliance/wasmtime?branch=main#f96491f3337a8ccb792b2769b016de6c16022ede" +source = "git+https://github.com/bytecodealliance/wasmtime?branch=main#ff0c45b4a069a84b131c8265d8d14c2f7d0566d7" dependencies = [ "serde", ] @@ -349,7 +349,7 @@ dependencies = [ [[package]] name = "cranelift-frontend" version = "0.90.0" -source = "git+https://github.com/bytecodealliance/wasmtime?branch=main#f96491f3337a8ccb792b2769b016de6c16022ede" +source = "git+https://github.com/bytecodealliance/wasmtime?branch=main#ff0c45b4a069a84b131c8265d8d14c2f7d0566d7" dependencies = [ "cranelift-codegen", "log", @@ -360,7 +360,7 @@ dependencies = [ [[package]] name = "cranelift-isle" version = "0.90.0" -source = "git+https://github.com/bytecodealliance/wasmtime?branch=main#f96491f3337a8ccb792b2769b016de6c16022ede" +source = "git+https://github.com/bytecodealliance/wasmtime?branch=main#ff0c45b4a069a84b131c8265d8d14c2f7d0566d7" dependencies = [ "rayon", ] @@ -368,7 +368,7 @@ dependencies = [ [[package]] name = "cranelift-native" version = "0.90.0" -source = "git+https://github.com/bytecodealliance/wasmtime?branch=main#f96491f3337a8ccb792b2769b016de6c16022ede" +source = "git+https://github.com/bytecodealliance/wasmtime?branch=main#ff0c45b4a069a84b131c8265d8d14c2f7d0566d7" dependencies = [ "cranelift-codegen", "libc", @@ -378,7 +378,7 @@ dependencies = [ [[package]] name = "cranelift-wasm" version = "0.90.0" -source = "git+https://github.com/bytecodealliance/wasmtime?branch=main#f96491f3337a8ccb792b2769b016de6c16022ede" +source = "git+https://github.com/bytecodealliance/wasmtime?branch=main#ff0c45b4a069a84b131c8265d8d14c2f7d0566d7" dependencies = [ "cranelift-codegen", "cranelift-entity", @@ -1405,7 +1405,9 @@ name = "test-helpers" version = "0.3.0" dependencies = [ "test-helpers-macros", + "wat", "wit-bindgen-core", + "wit-component", "wit-parser", ] @@ -1620,7 +1622,7 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasi-cap-std-sync" version = "3.0.0" -source = "git+https://github.com/bytecodealliance/wasmtime?branch=main#f96491f3337a8ccb792b2769b016de6c16022ede" +source = "git+https://github.com/bytecodealliance/wasmtime?branch=main#ff0c45b4a069a84b131c8265d8d14c2f7d0566d7" dependencies = [ "anyhow", "async-trait", @@ -1643,7 +1645,7 @@ dependencies = [ [[package]] name = "wasi-common" version = "3.0.0" -source = "git+https://github.com/bytecodealliance/wasmtime?branch=main#f96491f3337a8ccb792b2769b016de6c16022ede" +source = "git+https://github.com/bytecodealliance/wasmtime?branch=main#ff0c45b4a069a84b131c8265d8d14c2f7d0566d7" dependencies = [ "anyhow", "bitflags", @@ -1696,7 +1698,7 @@ dependencies = [ [[package]] name = "wasmtime" version = "3.0.0" -source = "git+https://github.com/bytecodealliance/wasmtime?branch=main#f96491f3337a8ccb792b2769b016de6c16022ede" +source = "git+https://github.com/bytecodealliance/wasmtime?branch=main#ff0c45b4a069a84b131c8265d8d14c2f7d0566d7" dependencies = [ "anyhow", "async-trait", @@ -1729,7 +1731,7 @@ dependencies = [ [[package]] name = "wasmtime-asm-macros" version = "3.0.0" -source = "git+https://github.com/bytecodealliance/wasmtime?branch=main#f96491f3337a8ccb792b2769b016de6c16022ede" +source = "git+https://github.com/bytecodealliance/wasmtime?branch=main#ff0c45b4a069a84b131c8265d8d14c2f7d0566d7" dependencies = [ "cfg-if", ] @@ -1737,7 +1739,7 @@ dependencies = [ [[package]] name = "wasmtime-cache" version = "3.0.0" -source = "git+https://github.com/bytecodealliance/wasmtime?branch=main#f96491f3337a8ccb792b2769b016de6c16022ede" +source = "git+https://github.com/bytecodealliance/wasmtime?branch=main#ff0c45b4a069a84b131c8265d8d14c2f7d0566d7" dependencies = [ "anyhow", "base64", @@ -1756,7 +1758,7 @@ dependencies = [ [[package]] name = "wasmtime-component-macro" version = "3.0.0" -source = "git+https://github.com/bytecodealliance/wasmtime?branch=main#f96491f3337a8ccb792b2769b016de6c16022ede" +source = "git+https://github.com/bytecodealliance/wasmtime?branch=main#ff0c45b4a069a84b131c8265d8d14c2f7d0566d7" dependencies = [ "proc-macro2", "quote", @@ -1767,12 +1769,12 @@ dependencies = [ [[package]] name = "wasmtime-component-util" version = "3.0.0" -source = "git+https://github.com/bytecodealliance/wasmtime?branch=main#f96491f3337a8ccb792b2769b016de6c16022ede" +source = "git+https://github.com/bytecodealliance/wasmtime?branch=main#ff0c45b4a069a84b131c8265d8d14c2f7d0566d7" [[package]] name = "wasmtime-cranelift" version = "3.0.0" -source = "git+https://github.com/bytecodealliance/wasmtime?branch=main#f96491f3337a8ccb792b2769b016de6c16022ede" +source = "git+https://github.com/bytecodealliance/wasmtime?branch=main#ff0c45b4a069a84b131c8265d8d14c2f7d0566d7" dependencies = [ "anyhow", "cranelift-codegen", @@ -1792,7 +1794,7 @@ dependencies = [ [[package]] name = "wasmtime-environ" version = "3.0.0" -source = "git+https://github.com/bytecodealliance/wasmtime?branch=main#f96491f3337a8ccb792b2769b016de6c16022ede" +source = "git+https://github.com/bytecodealliance/wasmtime?branch=main#ff0c45b4a069a84b131c8265d8d14c2f7d0566d7" dependencies = [ "anyhow", "cranelift-entity", @@ -1813,7 +1815,7 @@ dependencies = [ [[package]] name = "wasmtime-fiber" version = "3.0.0" -source = "git+https://github.com/bytecodealliance/wasmtime?branch=main#f96491f3337a8ccb792b2769b016de6c16022ede" +source = "git+https://github.com/bytecodealliance/wasmtime?branch=main#ff0c45b4a069a84b131c8265d8d14c2f7d0566d7" dependencies = [ "cc", "cfg-if", @@ -1825,7 +1827,7 @@ dependencies = [ [[package]] name = "wasmtime-jit" version = "3.0.0" -source = "git+https://github.com/bytecodealliance/wasmtime?branch=main#f96491f3337a8ccb792b2769b016de6c16022ede" +source = "git+https://github.com/bytecodealliance/wasmtime?branch=main#ff0c45b4a069a84b131c8265d8d14c2f7d0566d7" dependencies = [ "addr2line", "anyhow", @@ -1850,7 +1852,7 @@ dependencies = [ [[package]] name = "wasmtime-jit-debug" version = "3.0.0" -source = "git+https://github.com/bytecodealliance/wasmtime?branch=main#f96491f3337a8ccb792b2769b016de6c16022ede" +source = "git+https://github.com/bytecodealliance/wasmtime?branch=main#ff0c45b4a069a84b131c8265d8d14c2f7d0566d7" dependencies = [ "object", "once_cell", @@ -1860,7 +1862,7 @@ dependencies = [ [[package]] name = "wasmtime-jit-icache-coherence" version = "2.0.0" -source = "git+https://github.com/bytecodealliance/wasmtime?branch=main#f96491f3337a8ccb792b2769b016de6c16022ede" +source = "git+https://github.com/bytecodealliance/wasmtime?branch=main#ff0c45b4a069a84b131c8265d8d14c2f7d0566d7" dependencies = [ "cfg-if", "libc", @@ -1870,7 +1872,7 @@ dependencies = [ [[package]] name = "wasmtime-runtime" version = "3.0.0" -source = "git+https://github.com/bytecodealliance/wasmtime?branch=main#f96491f3337a8ccb792b2769b016de6c16022ede" +source = "git+https://github.com/bytecodealliance/wasmtime?branch=main#ff0c45b4a069a84b131c8265d8d14c2f7d0566d7" dependencies = [ "anyhow", "cc", @@ -1896,7 +1898,7 @@ dependencies = [ [[package]] name = "wasmtime-types" version = "3.0.0" -source = "git+https://github.com/bytecodealliance/wasmtime?branch=main#f96491f3337a8ccb792b2769b016de6c16022ede" +source = "git+https://github.com/bytecodealliance/wasmtime?branch=main#ff0c45b4a069a84b131c8265d8d14c2f7d0566d7" dependencies = [ "cranelift-entity", "serde", @@ -1907,7 +1909,7 @@ dependencies = [ [[package]] name = "wasmtime-wasi" version = "3.0.0" -source = "git+https://github.com/bytecodealliance/wasmtime?branch=main#f96491f3337a8ccb792b2769b016de6c16022ede" +source = "git+https://github.com/bytecodealliance/wasmtime?branch=main#ff0c45b4a069a84b131c8265d8d14c2f7d0566d7" dependencies = [ "anyhow", "wasi-cap-std-sync", @@ -1949,7 +1951,7 @@ dependencies = [ [[package]] name = "wiggle" version = "3.0.0" -source = "git+https://github.com/bytecodealliance/wasmtime?branch=main#f96491f3337a8ccb792b2769b016de6c16022ede" +source = "git+https://github.com/bytecodealliance/wasmtime?branch=main#ff0c45b4a069a84b131c8265d8d14c2f7d0566d7" dependencies = [ "anyhow", "async-trait", @@ -1963,7 +1965,7 @@ dependencies = [ [[package]] name = "wiggle-generate" version = "3.0.0" -source = "git+https://github.com/bytecodealliance/wasmtime?branch=main#f96491f3337a8ccb792b2769b016de6c16022ede" +source = "git+https://github.com/bytecodealliance/wasmtime?branch=main#ff0c45b4a069a84b131c8265d8d14c2f7d0566d7" dependencies = [ "anyhow", "heck", @@ -1977,7 +1979,7 @@ dependencies = [ [[package]] name = "wiggle-macro" version = "3.0.0" -source = "git+https://github.com/bytecodealliance/wasmtime?branch=main#f96491f3337a8ccb792b2769b016de6c16022ede" +source = "git+https://github.com/bytecodealliance/wasmtime?branch=main#ff0c45b4a069a84b131c8265d8d14c2f7d0566d7" dependencies = [ "proc-macro2", "quote", @@ -2076,6 +2078,7 @@ version = "0.3.0" dependencies = [ "anyhow", "clap", + "wat", "wit-bindgen-core", "wit-bindgen-gen-guest-c", "wit-bindgen-gen-guest-rust", @@ -2098,6 +2101,8 @@ dependencies = [ name = "wit-bindgen-demo" version = "0.3.0" dependencies = [ + "anyhow", + "test-helpers", "wasmprinter", "wit-bindgen-core", "wit-bindgen-gen-guest-c", @@ -2108,6 +2113,7 @@ dependencies = [ "wit-bindgen-gen-host-wasmtime-rust", "wit-bindgen-gen-markdown", "wit-bindgen-guest-rust", + "wit-component", ] [[package]] @@ -2151,10 +2157,15 @@ dependencies = [ name = "wit-bindgen-gen-host-js" version = "0.3.0" dependencies = [ + "anyhow", "clap", "heck", + "indexmap", "test-helpers", + "wasmparser", + "wasmtime-environ", "wit-bindgen-core", + "wit-component", ] [[package]] @@ -2265,6 +2276,7 @@ dependencies = [ "indexmap", "log", "pretty_assertions", + "test-helpers", "wasm-encoder", "wasmparser", "wasmprinter", @@ -2289,7 +2301,7 @@ dependencies = [ [[package]] name = "witx" version = "0.9.1" -source = "git+https://github.com/bytecodealliance/wasmtime?branch=main#f96491f3337a8ccb792b2769b016de6c16022ede" +source = "git+https://github.com/bytecodealliance/wasmtime?branch=main#ff0c45b4a069a84b131c8265d8d14c2f7d0566d7" dependencies = [ "anyhow", "log", diff --git a/Cargo.toml b/Cargo.toml index 97ab9f8f8..6cf554475 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,6 +28,7 @@ env_logger = "0.9.1" wasmtime = { git = "https://github.com/bytecodealliance/wasmtime", branch = "main" , features = ["component-model"] } wasmtime-wasi = { git = "https://github.com/bytecodealliance/wasmtime", branch = "main" } +wasmtime-environ = { git = "https://github.com/bytecodealliance/wasmtime", branch = "main" } wasmprinter = "0.2.41" wasmparser = "0.92.0" wasm-encoder = "0.18.0" @@ -62,3 +63,4 @@ wit-bindgen-gen-host-js = { path = 'crates/gen-host-js', features = ['clap'] } wit-bindgen-gen-guest-c = { path = 'crates/gen-guest-c', features = ['clap'] } wit-bindgen-gen-markdown = { path = 'crates/gen-markdown', features = ['clap'] } wit-bindgen-gen-guest-teavm-java = { path = 'crates/gen-guest-teavm-java', features = ['clap'] } +wat = { workspace = true } diff --git a/crates/bindgen-core/src/lib.rs b/crates/bindgen-core/src/lib.rs index 4ee2a00c2..472f630f4 100644 --- a/crates/bindgen-core/src/lib.rs +++ b/crates/bindgen-core/src/lib.rs @@ -397,7 +397,11 @@ impl Source { self.indent += 1; } if trimmed.starts_with('}') { - self.indent -= 1; + // Note that a `saturating_sub` is used here to prevent a panic + // here in the case of invalid code being generated in debug + // mode. It's typically easier to debug those issues through + // looking at the source code rather than getting a panic. + self.indent = self.indent.saturating_sub(1); } if i != lines.len() - 1 || src.ends_with("\n") { self.newline(); @@ -526,3 +530,50 @@ mod tests { fn _assert(_: &dyn Generator) {} } } + +/// This is a possible replacement for the `Generator` trait above, currently +/// only used by the JS bindings for generating bindings for a component. +/// +/// The current plan is to see how things shake out with worlds and various +/// other generators to see if everything can be updated to a less +/// per-`*.wit`-file centric interface in the future. Even this will probably +/// change for JS though. In any case it's something that was useful for JS and +/// is suitable to replace otherwise at any time. +pub trait InterfaceGenerator<'a> { + fn iface(&self) -> &'a Interface; + + fn type_record(&mut self, id: TypeId, name: &str, record: &Record, docs: &Docs); + fn type_flags(&mut self, id: TypeId, name: &str, flags: &Flags, docs: &Docs); + fn type_tuple(&mut self, id: TypeId, name: &str, flags: &Tuple, docs: &Docs); + fn type_variant(&mut self, id: TypeId, name: &str, variant: &Variant, docs: &Docs); + fn type_option(&mut self, id: TypeId, name: &str, payload: &Type, docs: &Docs); + fn type_result(&mut self, id: TypeId, name: &str, result: &Result_, docs: &Docs); + fn type_union(&mut self, id: TypeId, name: &str, union: &Union, docs: &Docs); + fn type_enum(&mut self, id: TypeId, name: &str, enum_: &Enum, docs: &Docs); + fn type_alias(&mut self, id: TypeId, name: &str, ty: &Type, docs: &Docs); + fn type_list(&mut self, id: TypeId, name: &str, ty: &Type, docs: &Docs); + fn type_builtin(&mut self, id: TypeId, name: &str, ty: &Type, docs: &Docs); + + fn types(&mut self) { + for (id, ty) in self.iface().types.iter() { + let name = match &ty.name { + Some(name) => name, + None => continue, + }; + match &ty.kind { + TypeDefKind::Record(record) => self.type_record(id, name, record, &ty.docs), + TypeDefKind::Flags(flags) => self.type_flags(id, name, flags, &ty.docs), + TypeDefKind::Tuple(tuple) => self.type_tuple(id, name, tuple, &ty.docs), + TypeDefKind::Enum(enum_) => self.type_enum(id, name, enum_, &ty.docs), + TypeDefKind::Variant(variant) => self.type_variant(id, name, variant, &ty.docs), + TypeDefKind::Option(t) => self.type_option(id, name, t, &ty.docs), + TypeDefKind::Result(r) => self.type_result(id, name, r, &ty.docs), + TypeDefKind::Union(u) => self.type_union(id, name, u, &ty.docs), + TypeDefKind::List(t) => self.type_list(id, name, t, &ty.docs), + TypeDefKind::Type(t) => self.type_alias(id, name, t, &ty.docs), + TypeDefKind::Future(_) => todo!("generate for future"), + TypeDefKind::Stream(_) => todo!("generate for stream"), + } + } + } +} diff --git a/crates/gen-host-js/Cargo.toml b/crates/gen-host-js/Cargo.toml index d4bd4ce9d..88d2edf26 100644 --- a/crates/gen-host-js/Cargo.toml +++ b/crates/gen-host-js/Cargo.toml @@ -9,9 +9,14 @@ doctest = false test = false [dependencies] +anyhow = { workspace = true } wit-bindgen-core = { workspace = true } heck = { workspace = true } clap = { workspace = true, optional = true } +wasmtime-environ = { workspace = true, features = ['component-model'] } +wasmparser = { workspace = true } +wit-component = { workspace = true } +indexmap = "1.0" [dev-dependencies] test-helpers = { path = '../test-helpers' } diff --git a/crates/gen-host-js/src/lib.rs b/crates/gen-host-js/src/lib.rs index c20fcc287..8ffefe981 100644 --- a/crates/gen-host-js/src/lib.rs +++ b/crates/gen-host-js/src/lib.rs @@ -1,35 +1,35 @@ +use anyhow::{Context, Result}; use heck::*; -use std::collections::{BTreeMap, BTreeSet, HashMap}; +use indexmap::IndexMap; +use std::collections::{BTreeMap, BTreeSet}; use std::fmt::Write; use std::mem; +use wasmparser::{Validator, WasmFeatures}; +use wasmtime_environ::component::{ + CanonicalOptions, Component, ComponentTypesBuilder, CoreDef, CoreExport, Export, ExportItem, + GlobalInitializer, InstantiateModule, LowerImport, RuntimeInstanceIndex, StaticModuleIndex, + StringEncoding, Translator, +}; +use wasmtime_environ::{EntityIndex, ModuleTranslation, PrimaryMap, ScopeVec, Tunables}; use wit_bindgen_core::wit_parser::abi::{ AbiVariant, Bindgen, Bitcast, Instruction, LiftLower, WasmType, }; -use wit_bindgen_core::{wit_parser::*, Direction, Files, Generator}; +use wit_bindgen_core::{uwrite, uwriteln, wit_parser::*, Files, InterfaceGenerator}; +use wit_component::ComponentInterfaces; #[derive(Default)] -pub struct Js { +struct Js { + /// The source code for the "main" file that's going to be created for the + /// component we're generating bindings for. This is incrementally added to + /// over time and primarily contains the main `instantiate` function as well + /// as a type-description of the input/output interfaces. src: Source, - in_import: bool, - opts: Opts, - guest_imports: HashMap, - guest_exports: HashMap, - sizes: SizeAlign, - intrinsics: BTreeMap, - all_intrinsics: BTreeSet, - needs_get_export: bool, - needs_ty_option: bool, - needs_ty_result: bool, -} -#[derive(Default)] -struct Imports { - freestanding_funcs: Vec<(String, Source)>, -} + /// Various options for code generation. + opts: Opts, -#[derive(Default)] -struct Exports { - freestanding_funcs: Vec, + /// List of all intrinsics emitted to `src` so far. + all_intrinsics: BTreeSet, } #[derive(Default, Debug, Clone)] @@ -39,11 +39,107 @@ pub struct Opts { pub no_typescript: bool, } +/// Use to generate a `*.d.ts` file for each imported and exported interface for +/// a component. +/// +/// This generated source does not contain any actual JS runtime code, it's just +/// typescript definitions. +struct JsInterface<'a> { + src: Source, + gen: &'a mut Js, + iface: &'a Interface, + needs_ty_option: bool, + needs_ty_result: bool, +} + impl Opts { - pub fn build(self) -> Js { - let mut r = Js::new(); - r.opts = self; - r + /// Top-level entrypoint to generating JS bindings for a component. + /// + /// This function takes a number of parameters: + /// + /// * `name` - the name of the JS module that will be generated for the + /// bindings in `binary`. + /// * `binary` - this is the actual component binary. Bindings will be + /// generated for this component and this component alone. + /// * `files` - where to place generated files. + /// + /// This function will generate JS and TypeScript definitions (optionally) + /// for the `binary` specified. The JS will export a single `instantiate` + /// function which takes a core-wasm-instantiate method and an import + /// object. The return value from `instantiate` is the "export object" of + /// the component. + /// + /// Internally `instantiate` will weave together core wasm files as + /// specified in the component `binary` provided here. It will lower + /// functions provided in the import object using canonical ABI liftings and + /// lowerings. Instantiations are offloaded to the caller by providing a + /// wasm module path name and import object. This ideally allows using + /// embedding-specific methods like `instantiateStreaming` on the web or + /// other node.js-specific things in node. + /// + /// # Errors + /// + /// This function will return an error if `binary` is not a valid component + /// or if it is not of a "suitable shape" to infer interfaces from. At this + /// time this basically means that the component needs to be generated from + /// `wit-component` to have suitable type exports/imports/etc to infer an + /// `Interface` from the structure of the component. + pub fn generate(self, name: &str, binary: &[u8], files: &mut Files) -> Result<()> { + // Use the `wit-component` crate here to parse `binary` and discover + // the type-level descriptions and `Interface`s corresponding to the + // component binary. This is effectively a step that infers a "world" of + // a component. Right now `interfaces` is a world-like thing and this + // will likely change as worlds are iterated on in the component model + // standard. Regardless though this is the step where types are learned + // and `Interface`s are constructed for further code generation below. + let interfaces = wit_component::decode_interface_component(binary) + .context("failed to extract interface information from component")?; + + // Components are complicated, there's no real way around that. To + // handle all the work of parsing a component and figuring out how to + // instantiate core wasm modules and such all the work is offloaded to + // Wasmtime itself. This crate generator is based on Wasmtime's + // low-level `wasmtime-environ` crate which is technically not a public + // dependency but the same author who worked on that in Wasmtime wrote + // this as well so... "seems fine". + // + // Note that we're not pulling in the entire Wasmtime engine here, + // moreso just the "spine" of validating a component. This enables using + // Wasmtime's internal `Component` representation as a much easier to + // process version of a component that has decompiled everything + // internal to a component to a straight linear list of initializers + // that need to be executed to instantiate a component. + let scope = ScopeVec::new(); + let tunables = Tunables::default(); + let mut types = ComponentTypesBuilder::default(); + let mut validator = Validator::new_with_features(WasmFeatures { + component_model: true, + ..WasmFeatures::default() + }); + let (component, modules) = Translator::new(&tunables, &mut validator, &mut types, &scope) + .translate(binary) + .context("failed to parse the input component")?; + + // Components are largely just a collection of core wasm modules after + // all so each core wasm module found within the component is emitted to + // the output `files` as a module itself. These modules are then + // identified by name when instantiations are requested via the + // `instantiateCore` callback provided to the main `instantiate` + // function. + for (i, module) in modules.iter() { + let i = i.as_u32(); + let name = format!("module{i}.wasm"); + files.push(&name, module.wasm); + } + + // And with all that prep work out of the way it's time to hand + // everything over to this crate itself, moving into the world of + // `wit-bindgen` to generate bindings for JS. + let mut gen = Js::default(); + gen.opts = self; + gen.generate(name, &component, &modules, &interfaces, files); + + Ok(()) } } @@ -78,8 +174,6 @@ enum Intrinsic { Utf8Decoder, Utf8Encode, Utf8EncodedLen, - Slab, - Promises, ThrowInvalidBool, } @@ -106,56 +200,764 @@ impl Intrinsic { Intrinsic::Utf8Decoder => "UTF8_DECODER", Intrinsic::Utf8Encode => "utf8_encode", Intrinsic::Utf8EncodedLen => "UTF8_ENCODED_LEN", - Intrinsic::Slab => "Slab", - Intrinsic::Promises => "PROMISES", Intrinsic::ThrowInvalidBool => "throw_invalid_bool", } } } -impl Js { - pub fn new() -> Js { - Js::default() +impl Js { + fn generate( + &mut self, + name: &str, + component: &Component, + modules: &PrimaryMap>, + interfaces: &ComponentInterfaces<'_>, + files: &mut Files, + ) { + // Generate a TypeScript description of all interfaces found within the + // component. Each interface gets its own TypeScript file to prevent + // name clashes between them. + let to_gen = interfaces + .imports + .iter() + .map(|(name, iface)| (true, name, iface)) + .chain( + interfaces + .exports + .iter() + .map(|(name, iface)| (false, name, iface)), + ); + for (is_import, name, iface) in to_gen { + let camel = name.to_upper_camel_case(); + assert_eq!(iface.name, *name); + let mut gen = self.js_interface(iface); + gen.types(); + gen.post_types(); + + uwriteln!(gen.src.ts, "export interface {camel} {{"); + for func in iface.functions.iter() { + gen.ts_func(func); + } + uwriteln!(gen.src.ts, "}}"); + + assert!(gen.src.js.is_empty()); + let dir = if is_import { "imports" } else { "exports" }; + if !gen.gen.opts.no_typescript { + files.push(&format!("{dir}/{name}.d.ts"), gen.src.ts.as_bytes()); + } + + let extra = if is_import { "Imports" } else { "Exports" }; + uwriteln!( + self.src.ts, + "import {{ {camel} as {camel}{extra} }} from \"./{dir}/{name}\";" + ); + } + + // Generate the TypeScript definition of the `instantiate` function + // which is the main workhorse of the generated bindings. + let camel = name.to_upper_camel_case(); + uwriteln!( + self.src.ts, + " + /** + * Instantiates this component with the provided imports and + * returns a map of all the exports of the component. + * + * This function is intended to be similar to the + * `WebAssembly.instantiate` function. The second `imports` + * argument is the \"import object\" for wasm, except here it + * uses component-model-layer types instead of core wasm + * integers/numbers/etc. + * + * The first argument to this function, `instantiateCore`, is + * used to instantiate core wasm modules within the component. + * Components are composed of core wasm modules and this callback + * will be invoked per core wasm instantiation. The caller of + * this function is responsible for reading the core wasm module + * identified by `path` and instantiating it with the core wasm + * import object provided. This would use `instantiateStreaming` + * on the web, for example. + */ + export function instantiate( + instantiateCore: (path: string, imports: any) => Promise, + imports: ImportObject, + ): Promise<{camel}>; + ", + ); + + // Generate a type definition for the import object to type-check + // all imports to the component. + // + // With the current representation of a "world" this is an import object + // per-imported-interface where the type of that field is defined by the + // interface itself. + uwriteln!(self.src.ts, "export interface ImportObject {{"); + for (name, _iface) in interfaces.imports.iter() { + let camel = name.to_upper_camel_case(); + uwriteln!(self.src.ts, "{name}: {camel}Imports;"); + } + uwriteln!(self.src.ts, "}}"); + + // Generate a type definition for the export object from instantiating + // the component. This notably inlines the "default export" into the + // exported object here and otherwise has delegations for each exported + // interface to their own type descriptions. + uwriteln!(self.src.ts, "export interface {camel} {{",); + for (name, _iface) in interfaces.exports.iter() { + let camel = name.to_upper_camel_case(); + uwriteln!(self.src.ts, "{name}: {camel}Exports;"); + } + match &interfaces.default { + Some(iface) => { + let mut gen = self.js_interface(iface); + for func in iface.functions.iter() { + gen.ts_func(func); + } + gen.gen.src.ts(&mem::take(&mut gen.src.ts)); + uwriteln!(gen.gen.src.ts, "}}"); + + // After the default interface has its function definitions + // inlined the rest of the types are generated here as well. + gen.types(); + gen.post_types(); + gen.gen.src.ts(&mem::take(&mut gen.src.ts)); + } + None => { + uwriteln!(self.src.ts, "}}"); + } + } + + // Given all that the final piece of the puzzle for the generated + // bindings is the actual `instantiate` method itself, created by this + // structure. + let mut instantiator = Instantiator { + src: Source::default(), + gen: self, + modules, + instances: Default::default(), + interfaces, + component, + }; + instantiator.instantiate(); + instantiator.gen.src.js(&instantiator.src.js); + assert!(instantiator.src.ts.is_empty()); + + files.push(&format!("{name}.js"), self.src.js.as_bytes()); + if !self.opts.no_typescript { + files.push(&format!("{name}.d.ts"), self.src.ts.as_bytes()); + } + } + + fn js_interface<'a>(&'a mut self, iface: &'a Interface) -> JsInterface<'a> { + JsInterface { + src: Source::default(), + gen: self, + iface, + needs_ty_option: false, + needs_ty_result: false, + } + } + + /// Emits the intrinsic `i` to this file and then returns the name of the + /// intrinsic. + fn intrinsic(&mut self, i: Intrinsic) -> String { + let name = i.name().to_string(); + if !self.all_intrinsics.insert(i) { + return name; + } + + if (i == Intrinsic::I32ToF32 && !self.all_intrinsics.contains(&Intrinsic::F32ToI32)) + || (i == Intrinsic::F32ToI32 && !self.all_intrinsics.contains(&Intrinsic::I32ToF32)) + { + self.src.js(" + const I32_TO_F32_I = new Int32Array(1); + const I32_TO_F32_F = new Float32Array(I32_TO_F32_I.buffer); + "); + } + if (i == Intrinsic::I64ToF64 && !self.all_intrinsics.contains(&Intrinsic::F64ToI64)) + || (i == Intrinsic::F64ToI64 && !self.all_intrinsics.contains(&Intrinsic::I64ToF64)) + { + self.src.js(" + const I64_TO_F64_I = new BigInt64Array(1); + const I64_TO_F64_F = new Float64Array(I64_TO_F64_I.buffer); + "); + } + + match i { + Intrinsic::ClampGuest => self.src.js(" + function clamp_guest(i, min, max) { + if (i < min || i > max) \ + throw new RangeError(`must be between ${min} and ${max}`); + return i; + } + "), + + Intrinsic::DataView => self.src.js(" + let DATA_VIEW = new DataView(new ArrayBuffer()); + + function data_view(mem) { + if (DATA_VIEW.buffer !== mem.buffer) \ + DATA_VIEW = new DataView(mem.buffer); + return DATA_VIEW; + } + "), + + Intrinsic::ValidateGuestChar => self.src.js(" + function validate_guest_char(i) { + if ((i > 0x10ffff) || (i >= 0xd800 && i <= 0xdfff)) \ + throw new RangeError(`not a valid char`); + return String.fromCodePoint(i); + } + "), + + // TODO: this is incorrect. It at least allows strings of length > 0 + // but it probably doesn't do the right thing for unicode or invalid + // utf16 strings either. + Intrinsic::ValidateHostChar => self.src.js(" + function validate_host_char(s) { + if (typeof s !== 'string') \ + throw new TypeError(`must be a string`); + return s.codePointAt(0); + } + "), + + + Intrinsic::ToInt32 => self.src.js(" + function to_int32(val) { + return val >> 0; + } + "), + Intrinsic::ToUint32 => self.src.js(" + function to_uint32(val) { + return val >>> 0; + } + "), + + Intrinsic::ToInt16 => self.src.js(" + function to_int16(val) { + val >>>= 0; + val %= 2 ** 16; + if (val >= 2 ** 15) { + val -= 2 ** 16; + } + return val; + } + "), + Intrinsic::ToUint16 => self.src.js(" + function to_uint16(val) { + val >>>= 0; + val %= 2 ** 16; + return val; + } + "), + Intrinsic::ToInt8 => self.src.js(" + function to_int8(val) { + val >>>= 0; + val %= 2 ** 8; + if (val >= 2 ** 7) { + val -= 2 ** 8; + } + return val; + } + "), + Intrinsic::ToUint8 => self.src.js(" + function to_uint8(val) { + val >>>= 0; + val %= 2 ** 8; + return val; + } + "), + + Intrinsic::ToBigInt64 => self.src.js(" + function to_int64(val) { + return BigInt.asIntN(64, val); + } + "), + Intrinsic::ToBigUint64 => self.src.js(" + function to_uint64(val) { + return BigInt.asUintN(64, val); + } + "), + + Intrinsic::ToString => self.src.js(" + function to_string(val) { + if (typeof val === 'symbol') { + throw new TypeError('symbols cannot be converted to strings'); + } else { + // Calling `String` almost directly calls `ToString`, except that it also allows symbols, + // which is why we have the symbol-rejecting branch above. + // + // Definition of `String`: https://tc39.es/ecma262/#sec-string-constructor-string-value + return String(val); + } + } + "), + + Intrinsic::I32ToF32 => self.src.js(" + function i32ToF32(i) { + I32_TO_F32_I[0] = i; + return I32_TO_F32_F[0]; + } + "), + Intrinsic::F32ToI32 => self.src.js(" + function f32ToI32(f) { + I32_TO_F32_F[0] = f; + return I32_TO_F32_I[0]; + } + "), + Intrinsic::I64ToF64 => self.src.js(" + function i64ToF64(i) { + I64_TO_F64_I[0] = i; + return I64_TO_F64_F[0]; + } + "), + Intrinsic::F64ToI64 => self.src.js(" + function f64ToI64(f) { + I64_TO_F64_F[0] = f; + return I64_TO_F64_I[0]; + } + "), + + Intrinsic::Utf8Decoder => self + .src + .js("const UTF8_DECODER = new TextDecoder('utf-8');\n"), + + Intrinsic::Utf8EncodedLen => self.src.js("let UTF8_ENCODED_LEN = 0;\n"), + + Intrinsic::Utf8Encode => self.src.js(" + const UTF8_ENCODER = new TextEncoder('utf-8'); + + function utf8_encode(s, realloc, memory) { + if (typeof s !== 'string') \ + throw new TypeError('expected a string'); + + if (s.length === 0) { + UTF8_ENCODED_LEN = 0; + return 1; + } + + let alloc_len = 0; + let ptr = 0; + let writtenTotal = 0; + while (s.length > 0) { + ptr = realloc(ptr, alloc_len, 1, alloc_len + s.length); + alloc_len += s.length; + const { read, written } = UTF8_ENCODER.encodeInto( + s, + new Uint8Array(memory.buffer, ptr + writtenTotal, alloc_len - writtenTotal), + ); + writtenTotal += written; + s = s.slice(read); + } + if (alloc_len > writtenTotal) + ptr = realloc(ptr, alloc_len, 1, writtenTotal); + UTF8_ENCODED_LEN = writtenTotal; + return ptr; + } + "), + + Intrinsic::ThrowInvalidBool => self.src.js(" + function throw_invalid_bool() { + throw new RangeError(\"invalid variant discriminant for bool\"); + } + "), + } + + name + } + + fn array_ty(&self, iface: &Interface, ty: &Type) -> Option<&'static str> { + match ty { + Type::Bool => None, + Type::U8 => Some("Uint8Array"), + Type::S8 => Some("Int8Array"), + Type::U16 => Some("Uint16Array"), + Type::S16 => Some("Int16Array"), + Type::U32 => Some("Uint32Array"), + Type::S32 => Some("Int32Array"), + Type::U64 => Some("BigUint64Array"), + Type::S64 => Some("BigInt64Array"), + Type::Float32 => Some("Float32Array"), + Type::Float64 => Some("Float64Array"), + Type::Char => None, + Type::String => None, + Type::Id(id) => match &iface.types[*id].kind { + TypeDefKind::Type(t) => self.array_ty(iface, t), + _ => None, + }, + } + } + + /// Returns whether `null` is a valid value of type `ty` + fn maybe_null(&self, iface: &Interface, ty: &Type) -> bool { + self.as_nullable(iface, ty).is_some() + } + + /// Tests whether `ty` can be represented with `null`, and if it can then + /// the "other type" is returned. If `Some` is returned that means that `ty` + /// is `null | `. If `None` is returned that means that `null` can't + /// be used to represent `ty`. + fn as_nullable<'a>(&self, iface: &'a Interface, ty: &'a Type) -> Option<&'a Type> { + let id = match ty { + Type::Id(id) => *id, + _ => return None, + }; + match &iface.types[id].kind { + // If `ty` points to an `option`, then `ty` can be represented + // with `null` if `t` itself can't be represented with null. For + // example `option>` can't be represented with `null` + // since that's ambiguous if it's `none` or `some(none)`. + // + // Note, oddly enough, that `option>>` can be + // represented as `null` since: + // + // * `null` => `none` + // * `{ tag: "none" }` => `some(none)` + // * `{ tag: "some", val: null }` => `some(some(none))` + // * `{ tag: "some", val: 1 }` => `some(some(some(1)))` + // + // It's doubtful anyone would actually rely on that though due to + // how confusing it is. + TypeDefKind::Option(t) => { + if !self.maybe_null(iface, t) { + Some(t) + } else { + None + } + } + TypeDefKind::Type(t) => self.as_nullable(iface, t), + _ => None, + } + } +} + +/// Helper structure used to generate the `instantiate` method of a component. +/// +/// This is the main structure for parsing the output of Wasmtime. +struct Instantiator<'a> { + src: Source, + gen: &'a mut Js, + modules: &'a PrimaryMap>, + instances: PrimaryMap, + interfaces: &'a ComponentInterfaces<'a>, + component: &'a Component, +} + +impl Instantiator<'_> { + fn instantiate(&mut self) { + uwriteln!( + self.src.js, + "export async function instantiate(instantiateCore, imports) {{" + ); + + for init in self.component.initializers.iter() { + self.global_initializer(init); + } + + self.src.js("return "); + self.exports(&self.component.exports, 0, self.interfaces.default.as_ref()); + self.src.js(";\n"); + + uwriteln!(self.src.js, "}}"); + } + + fn global_initializer(&mut self, init: &GlobalInitializer) { + match init { + GlobalInitializer::InstantiateModule(m) => match m { + InstantiateModule::Static(idx, args) => self.instantiate_static_module(*idx, args), + + // This is only needed when instantiating an imported core wasm + // module which while easy to implement here is not possible to + // test at this time so it's left unimplemented. + InstantiateModule::Import(..) => unimplemented!(), + }, + + GlobalInitializer::LowerImport(i) => self.lower_import(i), + + GlobalInitializer::ExtractMemory(m) => { + let def = self.core_export(&m.export); + uwriteln!(self.src.js, "const memory{} = {def};", m.index.as_u32()); + } + GlobalInitializer::ExtractRealloc(r) => { + let def = self.core_def(&r.def); + uwriteln!(self.src.js, "const realloc{} = {def};", r.index.as_u32()); + } + GlobalInitializer::ExtractPostReturn(p) => { + let def = self.core_def(&p.def); + uwriteln!(self.src.js, "const postReturn{} = {def};", p.index.as_u32()); + } + + // This is only used for a "degenerate component" which internally + // has a function that always traps. While this should be trivial to + // implement (generate a JS function that always throws) there's no + // way to test this at this time so leave this unimplemented. + GlobalInitializer::AlwaysTrap(_) => unimplemented!(), + + // This is only used when the component exports core wasm modules, + // but that's not possible to test right now so leave these as + // unimplemented. + GlobalInitializer::SaveStaticModule(_) => unimplemented!(), + GlobalInitializer::SaveModuleImport(_) => unimplemented!(), + + // This is required when strings pass between components within a + // component and may change encodings. This is left unimplemented + // for now since it can't be tested and additionally JS doesn't + // support multi-memory which transcoders rely on anyway. + GlobalInitializer::Transcoder(_) => unimplemented!(), + } + } + + fn instantiate_static_module(&mut self, idx: StaticModuleIndex, args: &[CoreDef]) { + let module = &self.modules[idx].module; + + // Build a JS "import object" which represents `args`. The `args` is a + // flat representation which needs to be zip'd with the list of names to + // correspond to the JS wasm embedding API. This is one of the major + // differences between Wasmtime's and JS's embedding API. + let mut import_obj = BTreeMap::new(); + assert_eq!(module.imports().len(), args.len()); + for ((module, name, _), arg) in module.imports().zip(args) { + let def = self.core_def(arg); + let dst = import_obj.entry(module).or_insert(BTreeMap::new()); + let prev = dst.insert(name, def); + assert!(prev.is_none()); + } + let mut imports = String::new(); + if import_obj.is_empty() { + imports.push_str("{}"); + } else { + imports.push_str("{\n"); + for (module, names) in import_obj { + uwrite!(imports, "\"{module}\": {{\n"); + for (name, val) in names { + uwriteln!(imports, "\"{name}\": {val},"); + } + imports.push_str("},\n"); + } + imports.push_str("}"); + } + + // Delegate most of the work to `instantiateCore` to allow the JS caller + // to do `instantiateStreaming` or w/e is appropriate for the embedding + // at hand. We've done all the hard work of assembling the import object + // so the instantiation should be relatively straightforward. + let i = self.instances.push(idx); + let name = format!("module{}.wasm", idx.as_u32()); + uwrite!(self.src.js, "const instance{} = ", i.as_u32()); + uwriteln!(self.src.js, "await instantiateCore(\"{name}\", {imports});"); + } + + fn lower_import(&mut self, import: &LowerImport) { + // Determine the `Interface` that this import corresponds to. At this + // time `wit-component` only supports root-level imports of instances + // where instances export functions. + let (import_index, path) = &self.component.imports[import.import]; + let (import_name, _import_ty) = &self.component.import_types[*import_index]; + assert_eq!(path.len(), 1); + let iface = &self.interfaces.imports[import_name.as_str()]; + let func = iface.functions.iter().find(|f| f.name == path[0]).unwrap(); + + let index = import.index.as_u32(); + let callee = format!("lowering{index}Callee"); + uwriteln!( + self.src.js, + "const {callee} = imports.{}.{};", + import_name.to_lower_camel_case(), + func.name.to_lower_camel_case(), + ); + uwrite!(self.src.js, "function lowering{index}"); + let nparams = iface + .wasm_signature(AbiVariant::GuestImport, func) + .params + .len(); + self.bindgen( + nparams, + callee, + &import.options, + iface, + func, + AbiVariant::GuestImport, + ); + uwriteln!(self.src.js, ""); + } + + fn bindgen( + &mut self, + nparams: usize, + callee: String, + opts: &CanonicalOptions, + iface: &Interface, + func: &Function, + abi: AbiVariant, + ) { + // Technically it wouldn't be the hardest thing in the world to support + // other string encodings, but for now the code generator was originally + // written to support utf-8 so let's just leave it at that for now. In + // the future when it's easier to produce components with non-utf-8 this + // can be plumbed through to string lifting/lowering below. + assert_eq!(opts.string_encoding, StringEncoding::Utf8); + + let memory = match opts.memory { + Some(idx) => Some(format!("memory{}", idx.as_u32())), + None => None, + }; + let realloc = match opts.realloc { + Some(idx) => Some(format!("realloc{}", idx.as_u32())), + None => None, + }; + let post_return = match opts.post_return { + Some(idx) => Some(format!("postReturn{}", idx.as_u32())), + None => None, + }; + + self.src.js("("); + let mut params = Vec::new(); + for i in 0..nparams { + if i > 0 { + self.src.js(", "); + } + let param = format!("arg{i}"); + self.src.js(¶m); + params.push(param); + } + self.src.js(") {\n"); + + let mut sizes = SizeAlign::default(); + sizes.fill(iface); + let mut f = FunctionBindgen { + sizes, + gen: self.gen, + block_storage: Vec::new(), + blocks: Vec::new(), + callee, + memory, + realloc, + tmp: 0, + params, + post_return, + src: Source::default(), + }; + iface.call( + abi, + match abi { + AbiVariant::GuestImport => LiftLower::LiftArgsLowerResults, + AbiVariant::GuestExport => LiftLower::LowerArgsLiftResults, + }, + func, + &mut f, + ); + let FunctionBindgen { src, .. } = f; + + self.src.js(&src.js); + assert!(src.ts.is_empty()); + self.src.js("}"); + } + + fn core_def(&self, def: &CoreDef) -> String { + match def { + CoreDef::Export(e) => self.core_export(e), + CoreDef::Lowered(i) => format!("lowering{}", i.as_u32()), + CoreDef::AlwaysTrap(_) => unimplemented!(), + CoreDef::InstanceFlags(_) => unimplemented!(), + CoreDef::Transcoder(_) => unimplemented!(), + } + } + + fn core_export(&self, export: &CoreExport) -> String + where + T: Into + Copy, + { + let name = match &export.item { + ExportItem::Index(idx) => { + let module = &self.modules[self.instances[export.instance]].module; + let idx = (*idx).into(); + module + .exports + .iter() + .filter_map(|(name, i)| if *i == idx { Some(name) } else { None }) + .next() + .unwrap() + } + ExportItem::Name(s) => s, + }; + let i = export.instance.as_u32() as usize; + format!("instance{i}.exports[\"{name}\"]") + } + + fn exports( + &mut self, + exports: &IndexMap, + depth: usize, + iface: Option<&Interface>, + ) { + if exports.is_empty() { + self.src.js("{}"); + return; + } + + self.src.js("{\n"); + for (name, export) in exports { + let camel = name.to_lower_camel_case(); + match export { + Export::LiftedFunction { + ty: _, + func, + options, + } => { + assert!(depth < 2); + uwrite!(self.src.js, "{camel}"); + let callee = self.core_def(func); + let iface = iface.unwrap(); + let func = iface.functions.iter().find(|f| f.name == *name).unwrap(); + self.bindgen( + func.params.len(), + callee, + options, + iface, + func, + AbiVariant::GuestExport, + ); + self.src.js(",\n"); + } + Export::Instance(exports) => { + uwrite!(self.src.js, "{camel}: "); + let iface = self.interfaces.exports.get(name.as_str()); + self.exports(exports, depth + 1, iface); + self.src.js(",\n"); + } + + // ignore type exports for now + Export::Type(_) => {} + + // This can't be tested at this time so leave it unimplemented + Export::Module(_) => unimplemented!(), + } + } + self.src.js("}"); + } +} + +impl<'a> JsInterface<'a> { + fn docs_raw(&mut self, docs: &str) { + self.src.ts("/**\n"); + for line in docs.lines() { + self.src.ts(&format!(" * {}\n", line)); + } + self.src.ts(" */\n"); } - fn abi_variant(dir: Direction) -> AbiVariant { - // This generator uses a reversed mapping! In the JS host-side - // bindings, we don't use any extra adapter layer between guest wasm - // modules and the host. When the guest imports functions using the - // `GuestImport` ABI, the host directly implements the `GuestImport` - // ABI, even though the host is *exporting* functions. Similarly, when - // the guest exports functions using the `GuestExport` ABI, the host - // directly imports them with the `GuestExport` ABI, even though the - // host is *importing* functions. - match dir { - Direction::Import => AbiVariant::GuestExport, - Direction::Export => AbiVariant::GuestImport, + fn docs(&mut self, docs: &Docs) { + match &docs.contents { + Some(docs) => self.docs_raw(docs), + None => return, } } - fn array_ty(&self, iface: &Interface, ty: &Type) -> Option<&'static str> { - match ty { - Type::Bool => None, - Type::U8 => Some("Uint8Array"), - Type::S8 => Some("Int8Array"), - Type::U16 => Some("Uint16Array"), - Type::S16 => Some("Int16Array"), - Type::U32 => Some("Uint32Array"), - Type::S32 => Some("Int32Array"), - Type::U64 => Some("BigUint64Array"), - Type::S64 => Some("BigInt64Array"), - Type::Float32 => Some("Float32Array"), - Type::Float64 => Some("Float64Array"), - Type::Char => None, - Type::String => None, - Type::Id(id) => match &iface.types[*id].kind { - TypeDefKind::Type(t) => self.array_ty(iface, t), - _ => None, - }, - } + fn array_ty(&self, ty: &Type) -> Option<&'static str> { + self.gen.array_ty(self.iface, ty) } - fn print_ty(&mut self, iface: &Interface, ty: &Type) { + fn print_ty(&mut self, ty: &Type) { match ty { Type::Bool => self.src.ts("boolean"), Type::U8 @@ -170,38 +972,38 @@ impl Js { Type::Char => self.src.ts("string"), Type::String => self.src.ts("string"), Type::Id(id) => { - let ty = &iface.types[*id]; + let ty = &self.iface.types[*id]; if let Some(name) = &ty.name { return self.src.ts(&name.to_upper_camel_case()); } match &ty.kind { - TypeDefKind::Type(t) => self.print_ty(iface, t), - TypeDefKind::Tuple(t) => self.print_tuple(iface, t), + TypeDefKind::Type(t) => self.print_ty(t), + TypeDefKind::Tuple(t) => self.print_tuple(t), TypeDefKind::Record(_) => panic!("anonymous record"), TypeDefKind::Flags(_) => panic!("anonymous flags"), TypeDefKind::Enum(_) => panic!("anonymous enum"), TypeDefKind::Union(_) => panic!("anonymous union"), TypeDefKind::Option(t) => { - if self.maybe_null(iface, t) { + if self.maybe_null(t) { self.needs_ty_option = true; self.src.ts("Option<"); - self.print_ty(iface, t); + self.print_ty(t); self.src.ts(">"); } else { - self.print_ty(iface, t); + self.print_ty(t); self.src.ts(" | null"); } } TypeDefKind::Result(r) => { self.needs_ty_result = true; self.src.ts("Result<"); - self.print_optional_ty(iface, r.ok.as_ref()); + self.print_optional_ty(r.ok.as_ref()); self.src.ts(", "); - self.print_optional_ty(iface, r.err.as_ref()); + self.print_optional_ty(r.err.as_ref()); self.src.ts(">"); } TypeDefKind::Variant(_) => panic!("anonymous variant"), - TypeDefKind::List(v) => self.print_list(iface, v), + TypeDefKind::List(v) => self.print_list(v), TypeDefKind::Future(_) => todo!("anonymous future"), TypeDefKind::Stream(_) => todo!("anonymous stream"), } @@ -209,50 +1011,35 @@ impl Js { } } - fn print_optional_ty(&mut self, iface: &Interface, ty: Option<&Type>) { + fn print_optional_ty(&mut self, ty: Option<&Type>) { match ty { - Some(ty) => self.print_ty(iface, ty), + Some(ty) => self.print_ty(ty), None => self.src.ts("void"), } } - fn print_list(&mut self, iface: &Interface, ty: &Type) { - match self.array_ty(iface, ty) { + fn print_list(&mut self, ty: &Type) { + match self.array_ty(ty) { Some(ty) => self.src.ts(ty), None => { - self.print_ty(iface, ty); + self.print_ty(ty); self.src.ts("[]"); } } } - fn print_tuple(&mut self, iface: &Interface, tuple: &Tuple) { + fn print_tuple(&mut self, tuple: &Tuple) { self.src.ts("["); for (i, ty) in tuple.types.iter().enumerate() { if i > 0 { self.src.ts(", "); } - self.print_ty(iface, ty); + self.print_ty(ty); } self.src.ts("]"); } - fn docs_raw(&mut self, docs: &str) { - self.src.ts("/**\n"); - for line in docs.lines() { - self.src.ts(&format!(" * {}\n", line)); - } - self.src.ts(" */\n"); - } - - fn docs(&mut self, docs: &Docs) { - match &docs.contents { - Some(docs) => self.docs_raw(docs), - None => return, - } - } - - fn ts_func(&mut self, iface: &Interface, func: &Function) { + fn ts_func(&mut self, func: &Function) { self.docs(&func.docs); self.src.ts(&func.item_name().to_lower_camel_case()); @@ -268,19 +1055,19 @@ impl Js { } self.src.ts(to_js_ident(&name.to_lower_camel_case())); self.src.ts(": "); - self.print_ty(iface, ty); + self.print_ty(ty); } self.src.ts("): "); match func.results.len() { 0 => self.src.ts("void"), - 1 => self.print_ty(iface, func.results.iter_types().next().unwrap()), + 1 => self.print_ty(func.results.iter_types().next().unwrap()), _ => { self.src.ts("["); for (i, ty) in func.results.iter_types().enumerate() { if i != 0 { self.src.ts(", "); } - self.print_ty(iface, ty); + self.print_ty(ty); } self.src.ts("]"); } @@ -288,74 +1075,36 @@ impl Js { self.src.ts(";\n"); } - fn intrinsic(&mut self, i: Intrinsic) -> String { - if let Some(name) = self.intrinsics.get(&i) { - return name.clone(); - } - // TODO: should select a name that automatically doesn't conflict with - // anything else being generated. - self.intrinsics.insert(i, i.name().to_string()); - return i.name().to_string(); + fn maybe_null(&self, ty: &Type) -> bool { + self.gen.maybe_null(self.iface, ty) } - /// Returns whether `null` is a valid value of type `ty` - fn maybe_null(&self, iface: &Interface, ty: &Type) -> bool { - self.as_nullable(iface, ty).is_some() + fn as_nullable<'b>(&self, ty: &'b Type) -> Option<&'b Type> + where + 'a: 'b, + { + self.gen.as_nullable(self.iface, ty) } - /// Tests whether `ty` can be represented with `null`, and if it can then - /// the "other type" is returned. If `Some` is returned that means that `ty` - /// is `null | `. If `None` is returned that means that `null` can't - /// be used to represent `ty`. - fn as_nullable<'a>(&self, iface: &'a Interface, ty: &'a Type) -> Option<&'a Type> { - let id = match ty { - Type::Id(id) => *id, - _ => return None, - }; - match &iface.types[id].kind { - // If `ty` points to an `option`, then `ty` can be represented - // with `null` if `t` itself can't be represented with null. For - // example `option>` can't be represented with `null` - // since that's ambiguous if it's `none` or `some(none)`. - // - // Note, oddly enough, that `option>>` can be - // represented as `null` since: - // - // * `null` => `none` - // * `{ tag: "none" }` => `some(none)` - // * `{ tag: "some", val: null }` => `some(some(none))` - // * `{ tag: "some", val: 1 }` => `some(some(some(1)))` - // - // It's doubtful anyone would actually rely on that though due to - // how confusing it is. - TypeDefKind::Option(t) => { - if !self.maybe_null(iface, t) { - Some(t) - } else { - None - } - } - TypeDefKind::Type(t) => self.as_nullable(iface, t), - _ => None, + fn post_types(&mut self) { + if mem::take(&mut self.needs_ty_option) { + self.src + .ts("export type Option = { tag: \"none\" } | { tag: \"some\", val; T };\n"); + } + if mem::take(&mut self.needs_ty_result) { + self.src.ts( + "export type Result = { tag: \"ok\", val: T } | { tag: \"err\", val: E };\n", + ); } } } -impl Generator for Js { - fn preprocess_one(&mut self, iface: &Interface, dir: Direction) { - let variant = Self::abi_variant(dir); - self.sizes.fill(iface); - self.in_import = variant == AbiVariant::GuestImport; +impl<'a> InterfaceGenerator<'a> for JsInterface<'a> { + fn iface(&self) -> &'a Interface { + self.iface } - fn type_record( - &mut self, - iface: &Interface, - _id: TypeId, - name: &str, - record: &Record, - docs: &Docs, - ) { + fn type_record(&mut self, _id: TypeId, name: &str, record: &Record, docs: &Docs) { self.docs(docs); self.src.ts(&format!( "export interface {} {{\n", @@ -364,42 +1113,28 @@ impl Generator for Js { for field in record.fields.iter() { self.docs(&field.docs); let (option_str, ty) = self - .as_nullable(iface, &field.ty) + .as_nullable(&field.ty) .map_or(("", &field.ty), |ty| ("?", ty)); self.src.ts(&format!( "{}{}: ", field.name.to_lower_camel_case(), option_str )); - self.print_ty(iface, ty); + self.print_ty(ty); self.src.ts(",\n"); } self.src.ts("}\n"); } - fn type_tuple( - &mut self, - iface: &Interface, - _id: TypeId, - name: &str, - tuple: &Tuple, - docs: &Docs, - ) { + fn type_tuple(&mut self, _id: TypeId, name: &str, tuple: &Tuple, docs: &Docs) { self.docs(docs); self.src .ts(&format!("export type {} = ", name.to_upper_camel_case())); - self.print_tuple(iface, tuple); + self.print_tuple(tuple); self.src.ts(";\n"); } - fn type_flags( - &mut self, - _iface: &Interface, - _id: TypeId, - name: &str, - flags: &Flags, - docs: &Docs, - ) { + fn type_flags(&mut self, _id: TypeId, name: &str, flags: &Flags, docs: &Docs) { self.docs(docs); self.src.ts(&format!( "export interface {} {{\n", @@ -413,14 +1148,7 @@ impl Generator for Js { self.src.ts("}\n"); } - fn type_variant( - &mut self, - iface: &Interface, - _id: TypeId, - name: &str, - variant: &Variant, - docs: &Docs, - ) { + fn type_variant(&mut self, _id: TypeId, name: &str, variant: &Variant, docs: &Docs) { self.docs(docs); self.src .ts(&format!("export type {} = ", name.to_upper_camel_case())); @@ -443,21 +1171,14 @@ impl Generator for Js { self.src.ts("\",\n"); if let Some(ty) = case.ty { self.src.ts("val: "); - self.print_ty(iface, &ty); + self.print_ty(&ty); self.src.ts(",\n"); } self.src.ts("}\n"); } } - fn type_union( - &mut self, - iface: &Interface, - _id: TypeId, - name: &str, - union: &Union, - docs: &Docs, - ) { + fn type_union(&mut self, _id: TypeId, name: &str, union: &Union, docs: &Docs) { self.docs(docs); let name = name.to_upper_camel_case(); self.src.ts(&format!("export type {name} = ")); @@ -473,61 +1194,40 @@ impl Generator for Js { self.src.ts(&format!("export interface {name}{i} {{\n")); self.src.ts(&format!("tag: {i},\n")); self.src.ts("val: "); - self.print_ty(iface, &case.ty); + self.print_ty(&case.ty); self.src.ts(",\n"); self.src.ts("}\n"); } } - fn type_option( - &mut self, - iface: &Interface, - _id: TypeId, - name: &str, - payload: &Type, - docs: &Docs, - ) { + fn type_option(&mut self, _id: TypeId, name: &str, payload: &Type, docs: &Docs) { self.docs(docs); let name = name.to_upper_camel_case(); self.src.ts(&format!("export type {name} = ")); - if self.maybe_null(iface, payload) { + if self.maybe_null(payload) { self.needs_ty_option = true; self.src.ts("Option<"); - self.print_ty(iface, payload); + self.print_ty(payload); self.src.ts(">"); } else { - self.print_ty(iface, payload); + self.print_ty(payload); self.src.ts(" | null"); } self.src.ts(";\n"); } - fn type_result( - &mut self, - iface: &Interface, - _id: TypeId, - name: &str, - result: &Result_, - docs: &Docs, - ) { + fn type_result(&mut self, _id: TypeId, name: &str, result: &Result_, docs: &Docs) { self.docs(docs); let name = name.to_upper_camel_case(); self.needs_ty_result = true; self.src.ts(&format!("export type {name} = Result<")); - self.print_optional_ty(iface, result.ok.as_ref()); + self.print_optional_ty(result.ok.as_ref()); self.src.ts(", "); - self.print_optional_ty(iface, result.err.as_ref()); + self.print_optional_ty(result.err.as_ref()); self.src.ts(">;\n"); } - fn type_enum( - &mut self, - _iface: &Interface, - _id: TypeId, - name: &str, - enum_: &Enum, - docs: &Docs, - ) { + fn type_enum(&mut self, _id: TypeId, name: &str, enum_: &Enum, docs: &Docs) { // The complete documentation for this enum, including documentation for variants. let mut complete_docs = String::new(); @@ -555,367 +1255,49 @@ impl Generator for Js { .ts(&format!("export type {} = ", name.to_upper_camel_case())); for (i, case) in enum_.cases.iter().enumerate() { if i != 0 { - self.src.ts(" | "); - } - self.src.ts(&format!("\"{}\"", case.name)); - } - self.src.ts(";\n"); - } - - fn type_alias(&mut self, iface: &Interface, _id: TypeId, name: &str, ty: &Type, docs: &Docs) { - self.docs(docs); - self.src - .ts(&format!("export type {} = ", name.to_upper_camel_case())); - self.print_ty(iface, ty); - self.src.ts(";\n"); - } - - fn type_list(&mut self, iface: &Interface, _id: TypeId, name: &str, ty: &Type, docs: &Docs) { - self.docs(docs); - self.src - .ts(&format!("export type {} = ", name.to_upper_camel_case())); - self.print_list(iface, ty); - self.src.ts(";\n"); - } - - fn type_builtin(&mut self, iface: &Interface, _id: TypeId, name: &str, ty: &Type, docs: &Docs) { - drop((iface, _id, name, ty, docs)); - } - - // As with `abi_variant` above, we're generating host-side bindings here - // so a user "export" uses the "guest import" ABI variant on the inside of - // this `Generator` implementation. - fn export(&mut self, iface: &Interface, func: &Function) { - let prev = mem::take(&mut self.src); - - let sig = iface.wasm_signature(AbiVariant::GuestImport, func); - let params = (0..sig.params.len()) - .map(|i| format!("arg{}", i)) - .collect::>(); - self.src - .js(&format!("function({}) {{\n", params.join(", "))); - self.ts_func(iface, func); - - let mut f = FunctionBindgen::new(self, params); - iface.call( - AbiVariant::GuestImport, - LiftLower::LiftArgsLowerResults, - func, - &mut f, - ); - - let FunctionBindgen { - src, - needs_memory, - needs_realloc, - .. - } = f; - - if needs_memory { - self.needs_get_export = true; - // TODO: hardcoding "memory" - self.src.js("const memory = get_export(\"memory\");\n"); - } - - if let Some(name) = needs_realloc { - self.needs_get_export = true; - self.src - .js(&format!("const realloc = get_export(\"{}\");\n", name)); - } - - self.src.js(&src.js); - - self.src.js("}"); - - let src = mem::replace(&mut self.src, prev); - let imports = self - .guest_imports - .entry(iface.name.to_string()) - .or_insert(Imports::default()); - let dst = match &func.kind { - FunctionKind::Freestanding => &mut imports.freestanding_funcs, - }; - dst.push((func.name.to_string(), src)); - } - - // As with `abi_variant` above, we're generating host-side bindings here - // so a user "import" uses the "export" ABI variant on the inside of - // this `Generator` implementation. - fn import(&mut self, iface: &Interface, func: &Function) { - let prev = mem::take(&mut self.src); - - let params = func - .params - .iter() - .enumerate() - .map(|(i, _)| format!("arg{}", i)) - .collect::>(); - let src_object = match &func.kind { - FunctionKind::Freestanding => "this".to_string(), - }; - println!( - "{} {}", - func.item_name(), - func.item_name().to_lower_camel_case() - ); - self.src.js(&format!( - "{}({}) {{\n", - func.item_name().to_lower_camel_case(), - params.join(", ") - )); - self.ts_func(iface, func); - - let mut f = FunctionBindgen::new(self, params); - f.src_object = src_object; - iface.call( - AbiVariant::GuestExport, - LiftLower::LowerArgsLiftResults, - func, - &mut f, - ); - - let FunctionBindgen { - src, - needs_memory, - needs_realloc, - src_object, - .. - } = f; - if needs_memory { - // TODO: hardcoding "memory" - self.src - .js(&format!("const memory = {}._exports.memory;\n", src_object)); - } - - if let Some(name) = needs_realloc { - self.src.js(&format!( - "const realloc = {}._exports[\"{}\"];\n", - src_object, name - )); - } - - self.src.js(&src.js); - self.src.js("}\n"); - - let exports = self - .guest_exports - .entry(iface.name.to_string()) - .or_insert_with(Exports::default); - - let func_body = mem::replace(&mut self.src, prev); - match &func.kind { - FunctionKind::Freestanding => { - exports.freestanding_funcs.push(func_body); - } - } - } - - fn finish_one(&mut self, iface: &Interface, files: &mut Files) { - for (module, funcs) in mem::take(&mut self.guest_imports) { - // TODO: `module.exports` vs `export function` - self.src.js(&format!( - "export function add{}ToImports(imports, obj{}) {{\n", - module.to_upper_camel_case(), - if self.needs_get_export { - ", get_export" - } else { - "" - }, - )); - self.src.ts(&format!( - "export function add{}ToImports(imports: any, obj: {0}{}): void;\n", - module.to_upper_camel_case(), - if self.needs_get_export { - ", get_export: (name: string) => WebAssembly.ExportValue" - } else { - "" - }, - )); - self.src.js(&format!( - "if (!(\"{0}\" in imports)) imports[\"{0}\"] = {{}};\n", - module, - )); - - self.src.ts(&format!( - "export interface {} {{\n", - module.to_upper_camel_case() - )); - - for (name, src) in funcs.freestanding_funcs.iter() { - self.src.js(&format!( - "imports[\"{module}\"][\"{name}\"] = {};\n", - src.js.trim() - )); - self.src.ts(&src.ts); - } - - self.src.js("}"); - self.src.ts("}\n"); - } - - let imports = mem::take(&mut self.src); - - for (module, exports) in mem::take(&mut self.guest_exports) { - let module = module.to_upper_camel_case(); - self.src.ts(&format!("export class {} {{\n", module)); - self.src.js(&format!("export class {} {{\n", module)); - - self.src.ts(" - /** - * The WebAssembly instance that this class is operating with. - * This is only available after the `instantiate` method has - * been called. - */ - instance: WebAssembly.Instance; - "); - - self.src.ts(&format!( - " - /** - * Initializes this object with the provided WebAssembly - * module/instance. - * - * This is intended to be a flexible method of instantiating - * and completion of the initialization of this class. This - * method must be called before interacting with the - * WebAssembly object. - * - * The first argument to this method is where to get the - * wasm from. This can be a whole bunch of different types, - * for example: - * - * * A precompiled `WebAssembly.Module` - * * A typed array buffer containing the wasm bytecode. - * * A `Promise` of a `Response` which is used with - * `instantiateStreaming` - * * A `Response` itself used with `instantiateStreaming`. - * * An already instantiated `WebAssembly.Instance` - * - * If necessary the module is compiled, and if necessary the - * module is instantiated. Whether or not it's necessary - * depends on the type of argument provided to - * instantiation. - * - * If instantiation is performed then the `imports` object - * passed here is the list of imports used to instantiate - * the instance. This method may add its own intrinsics to - * this `imports` object too. - */ - instantiate( - module: WebAssembly.Module | BufferSource | Promise | Response | WebAssembly.Instance, - imports?: any, - ): Promise; - ", - )); - self.src.js(" - async instantiate(module, imports) { - imports = imports || {}; - "); - - // With intrinsics prep'd we can now instantiate the module. JS has - // a ... variety of methods of instantiation, so we basically just - // try to be flexible here. - self.src.js(" - if (module instanceof WebAssembly.Instance) { - this.instance = module; - } else if (module instanceof WebAssembly.Module) { - this.instance = await WebAssembly.instantiate(module, imports); - } else if (module instanceof ArrayBuffer || module instanceof Uint8Array) { - const { instance } = await WebAssembly.instantiate(module, imports); - this.instance = instance; - } else { - const { instance } = await WebAssembly.instantiateStreaming(module, imports); - this.instance = instance; - } - this._exports = this.instance.exports; - "); - self.src.js("}\n"); - - for func in exports.freestanding_funcs.iter() { - self.src.js(&func.js); - self.src.ts(&func.ts); - } - self.src.ts("}\n"); - self.src.js("}\n"); - } - - let exports = mem::take(&mut self.src); - - if mem::take(&mut self.needs_ty_option) { - self.src - .ts("export type Option = { tag: \"none\" } | { tag: \"some\", val; T };\n"); - } - if mem::take(&mut self.needs_ty_result) { - self.src.ts( - "export type Result = { tag: \"ok\", val: T } | { tag: \"err\", val: E };\n", - ); - } - - if self.intrinsics.len() > 0 { - self.src.js("import { "); - for (i, (intrinsic, name)) in mem::take(&mut self.intrinsics).into_iter().enumerate() { - if i > 0 { - self.src.js(", "); - } - self.src.js(intrinsic.name()); - if intrinsic.name() != name { - self.src.js(" as "); - self.src.js(&name); - } - self.all_intrinsics.insert(intrinsic); + self.src.ts(" | "); } - self.src.js(" } from './intrinsics.js';\n"); + self.src.ts(&format!("\"{}\"", case.name)); } + self.src.ts(";\n"); + } - self.src.js(&imports.js); - self.src.ts(&imports.ts); - self.src.js(&exports.js); - self.src.ts(&exports.ts); + fn type_alias(&mut self, _id: TypeId, name: &str, ty: &Type, docs: &Docs) { + self.docs(docs); + self.src + .ts(&format!("export type {} = ", name.to_upper_camel_case())); + self.print_ty(ty); + self.src.ts(";\n"); + } - let src = mem::take(&mut self.src); - let name = iface.name.to_kebab_case(); - files.push(&format!("{}.js", name), src.js.as_bytes()); - if !self.opts.no_typescript { - files.push(&format!("{}.d.ts", name), src.ts.as_bytes()); - } + fn type_list(&mut self, _id: TypeId, name: &str, ty: &Type, docs: &Docs) { + self.docs(docs); + self.src + .ts(&format!("export type {} = ", name.to_upper_camel_case())); + self.print_list(ty); + self.src.ts(";\n"); } - fn finish_all(&mut self, files: &mut Files) { - assert!(self.src.ts.is_empty()); - assert!(self.src.js.is_empty()); - self.print_intrinsics(); - assert!(self.src.ts.is_empty()); - files.push("intrinsics.js", self.src.js.as_bytes()); + fn type_builtin(&mut self, _id: TypeId, name: &str, ty: &Type, docs: &Docs) { + drop((_id, name, ty, docs)); } } struct FunctionBindgen<'a> { gen: &'a mut Js, + sizes: SizeAlign, tmp: usize, src: Source, block_storage: Vec, blocks: Vec<(String, Vec)>, - needs_memory: bool, - needs_realloc: Option, params: Vec, - src_object: String, + memory: Option, + realloc: Option, + post_return: Option, + callee: String, } impl FunctionBindgen<'_> { - fn new(gen: &mut Js, params: Vec) -> FunctionBindgen<'_> { - FunctionBindgen { - gen, - tmp: 0, - src: Source::default(), - block_storage: Vec::new(), - blocks: Vec::new(), - needs_memory: false, - needs_realloc: None, - params, - src_object: "this".to_string(), - } - } - fn tmp(&mut self) -> usize { let ret = self.tmp; self.tmp += 1; @@ -931,21 +1313,23 @@ impl FunctionBindgen<'_> { } fn load(&mut self, method: &str, offset: i32, operands: &[String], results: &mut Vec) { - self.needs_memory = true; + let memory = self.memory.as_ref().unwrap(); let view = self.gen.intrinsic(Intrinsic::DataView); results.push(format!( - "{}(memory).{}({} + {}, true)", - view, method, operands[0], offset, + "{view}({memory}).{method}({} + {offset}, true)", + operands[0], )); } fn store(&mut self, method: &str, offset: i32, operands: &[String]) { - self.needs_memory = true; + let memory = self.memory.as_ref().unwrap(); let view = self.gen.intrinsic(Intrinsic::DataView); - self.src.js(&format!( - "{}(memory).{}({} + {}, {}, true);\n", - view, method, operands[1], offset, operands[0] - )); + uwriteln!( + self.src.js, + "{view}({memory}).{method}({} + {offset}, {}, true);", + operands[1], + operands[0] + ); } fn bind_results(&mut self, amt: usize, results: &mut Vec) { @@ -974,7 +1358,7 @@ impl Bindgen for FunctionBindgen<'_> { type Operand = String; fn sizes(&self) -> &SizeAlign { - &self.gen.sizes + &self.sizes } fn push_block(&mut self) { @@ -1692,109 +2076,101 @@ impl Bindgen for FunctionBindgen<'_> { results.push(format!("enum{tmp}")); } - Instruction::ListCanonLower { element, realloc } => { - // Lowering only happens when we're passing lists into wasm, - // which forces us to always allocate, so this should always be - // `Some`. - let realloc = realloc.unwrap(); - self.gen.needs_get_export = true; - self.needs_memory = true; - self.needs_realloc = Some(realloc.to_string()); + Instruction::ListCanonLower { element, .. } => { let tmp = self.tmp(); - - let size = self.gen.sizes.size(element); - let align = self.gen.sizes.align(element); - self.src - .js(&format!("const val{} = {};\n", tmp, operands[0])); - self.src.js(&format!("const len{} = val{0}.length;\n", tmp)); - self.src.js(&format!( - "const ptr{} = realloc(0, 0, {}, len{0} * {});\n", - tmp, align, size, - )); + let memory = self.memory.as_ref().unwrap(); + let realloc = self.realloc.as_ref().unwrap(); + + let size = self.sizes.size(element); + let align = self.sizes.align(element); + uwriteln!(self.src.js, "const val{tmp} = {};", operands[0]); + uwriteln!(self.src.js, "const len{tmp} = val{tmp}.length;"); + uwriteln!( + self.src.js, + "const ptr{tmp} = {realloc}(0, 0, {align}, len{tmp} * {size});" + ); // TODO: this is the wrong endianness - self.src.js(&format!( - "(new Uint8Array(memory.buffer, ptr{0}, len{0} * {1})).set(new Uint8Array(val{0}.buffer, val{0}.byteOffset, len{0} * {1}));\n", - tmp, size, - )); + uwriteln!( + self.src.js, + "const src{tmp} = new Uint8Array(val{tmp}.buffer, val{tmp}.byteOffset, len{tmp} * {size});", + ); + uwriteln!( + self.src.js, + "(new Uint8Array({memory}.buffer, ptr{tmp}, len{tmp} * {size})).set(src{tmp});", + ); results.push(format!("ptr{}", tmp)); results.push(format!("len{}", tmp)); } Instruction::ListCanonLift { element, .. } => { - self.needs_memory = true; let tmp = self.tmp(); - self.src.js(&format!("const ptr{tmp} = {};\n", operands[0])); - self.src.js(&format!("const len{tmp} = {};\n", operands[1])); + let memory = self.memory.as_ref().unwrap(); + uwriteln!(self.src.js, "const ptr{tmp} = {};", operands[0]); + uwriteln!(self.src.js, "const len{tmp} = {};", operands[1]); // TODO: this is the wrong endianness let array_ty = self.gen.array_ty(iface, element).unwrap(); - self.src.js(&format!( - "const result{tmp} = new {array_ty}(memory.buffer.slice(ptr{tmp}, ptr{tmp} + len{tmp} * {}));\n", - self.gen.sizes.size(element), - )); + uwriteln!( + self.src.js, + "const result{tmp} = new {array_ty}({memory}.buffer.slice(ptr{tmp}, ptr{tmp} + len{tmp} * {}));", + self.sizes.size(element), + ); results.push(format!("result{tmp}")); } - Instruction::StringLower { realloc } => { - // Lowering only happens when we're passing strings into wasm, - // which forces us to always allocate, so this should always be - // `Some`. - let realloc = realloc.unwrap(); - self.gen.needs_get_export = true; - self.needs_memory = true; - self.needs_realloc = Some(realloc.to_string()); + Instruction::StringLower { .. } => { let tmp = self.tmp(); + let memory = self.memory.as_ref().unwrap(); + let realloc = self.realloc.as_ref().unwrap(); let encode = self.gen.intrinsic(Intrinsic::Utf8Encode); - self.src.js(&format!( - "const ptr{} = {}({}, realloc, memory);\n", - tmp, encode, operands[0], - )); + uwriteln!( + self.src.js, + "const ptr{tmp} = {encode}({}, {realloc}, {memory});", + operands[0], + ); let encoded_len = self.gen.intrinsic(Intrinsic::Utf8EncodedLen); - self.src - .js(&format!("const len{} = {};\n", tmp, encoded_len)); + uwriteln!(self.src.js, "const len{tmp} = {encoded_len};"); results.push(format!("ptr{}", tmp)); results.push(format!("len{}", tmp)); } Instruction::StringLift => { - self.needs_memory = true; let tmp = self.tmp(); - self.src.js(&format!("const ptr{tmp} = {};\n", operands[0])); - self.src.js(&format!("const len{tmp} = {};\n", operands[1])); + let memory = self.memory.as_ref().unwrap(); + uwriteln!(self.src.js, "const ptr{tmp} = {};", operands[0]); + uwriteln!(self.src.js, "const len{tmp} = {};", operands[1]); let decoder = self.gen.intrinsic(Intrinsic::Utf8Decoder); - self.src.js(&format!( - "const result{tmp} = {decoder}.decode(new Uint8Array(memory.buffer, ptr{tmp}, len{tmp}));\n", - )); + uwriteln!( + self.src.js, + "const result{tmp} = {decoder}.decode(new Uint8Array({memory}.buffer, ptr{tmp}, len{tmp}));", + ); results.push(format!("result{tmp}")); } - Instruction::ListLower { element, realloc } => { - let realloc = realloc.unwrap(); + Instruction::ListLower { element, .. } => { let (body, body_results) = self.blocks.pop().unwrap(); assert!(body_results.is_empty()); let tmp = self.tmp(); let vec = format!("vec{}", tmp); let result = format!("result{}", tmp); let len = format!("len{}", tmp); - self.needs_realloc = Some(realloc.to_string()); - let size = self.gen.sizes.size(element); - let align = self.gen.sizes.align(element); + let size = self.sizes.size(element); + let align = self.sizes.align(element); // first store our vec-to-lower in a temporary since we'll // reference it multiple times. - self.src.js(&format!("const {} = {};\n", vec, operands[0])); - self.src.js(&format!("const {} = {}.length;\n", len, vec)); + uwriteln!(self.src.js, "const {vec} = {};", operands[0]); + uwriteln!(self.src.js, "const {len} = {vec}.length;"); // ... then realloc space for the result in the guest module - self.src.js(&format!( - "const {} = realloc(0, 0, {}, {} * {});\n", - result, align, len, size, - )); + let realloc = self.realloc.as_ref().unwrap(); + uwriteln!( + self.src.js, + "const {result} = {realloc}(0, 0, {align}, {len} * {size});" + ); // ... then consume the vector and use the block to lower the // result. - self.src - .js(&format!("for (let i = 0; i < {}.length; i++) {{\n", vec)); - self.src.js(&format!("const e = {}[i];\n", vec)); - self.src - .js(&format!("const base = {} + i * {};\n", result, size)); + uwriteln!(self.src.js, "for (let i = 0; i < {vec}.length; i++) {{"); + uwriteln!(self.src.js, "const e = {vec}[i];"); + uwriteln!(self.src.js, "const base = {result} + i * {size};"); self.src.js(&body); self.src.js("}\n"); @@ -1805,23 +2181,20 @@ impl Bindgen for FunctionBindgen<'_> { Instruction::ListLift { element, .. } => { let (body, body_results) = self.blocks.pop().unwrap(); let tmp = self.tmp(); - let size = self.gen.sizes.size(element); - let len = format!("len{}", tmp); - self.src.js(&format!("const {} = {};\n", len, operands[1])); - let base = format!("base{}", tmp); - self.src.js(&format!("const {} = {};\n", base, operands[0])); - let result = format!("result{}", tmp); - self.src.js(&format!("const {} = [];\n", result)); + let size = self.sizes.size(element); + let len = format!("len{tmp}"); + uwriteln!(self.src.js, "const {len} = {};", operands[1]); + let base = format!("base{tmp}"); + uwriteln!(self.src.js, "const {base} = {};", operands[0]); + let result = format!("result{tmp}"); + uwriteln!(self.src.js, "const {result} = [];"); results.push(result.clone()); - self.src - .js(&format!("for (let i = 0; i < {}; i++) {{\n", len)); - self.src - .js(&format!("const base = {} + i * {};\n", base, size)); + uwriteln!(self.src.js, "for (let i = 0; i < {len}; i++) {{"); + uwriteln!(self.src.js, "const base = {base} + i * {size};"); self.src.js(&body); assert_eq!(body_results.len(), 1); - self.src - .js(&format!("{}.push({});\n", result, body_results[0])); + uwriteln!(self.src.js, "{result}.push({});", body_results[0]); self.src.js("}\n"); } @@ -1829,71 +2202,25 @@ impl Bindgen for FunctionBindgen<'_> { Instruction::IterBasePointer => results.push("base".to_string()), - Instruction::CallWasm { - iface: _, - name, - sig, - } => { + Instruction::CallWasm { sig, .. } => { self.bind_results(sig.results.len(), results); - self.src.js(&self.src_object); - self.src.js("._exports['"); - self.src.js(&name); - self.src.js("']("); - self.src.js(&operands.join(", ")); - self.src.js(");\n"); + uwriteln!(self.src.js, "{}({});", self.callee, operands.join(", ")); } Instruction::CallInterface { module: _, func } => { - let call = |me: &mut FunctionBindgen<'_>| match &func.kind { - FunctionKind::Freestanding => { - me.src.js(&format!( - "obj.{}({})", - func.name.to_lower_camel_case(), - operands.join(", "), - )); - } - }; - let mut bind_results = |me: &mut FunctionBindgen<'_>| { - let amt = func.results.len(); - if amt == 0 { - return; - } - me.src.js("const "); - if amt > 1 { - me.src.js("["); - } - for i in 0..amt { - if i > 0 { - me.src.js(", "); - } - let name = format!("ret{i}"); - me.src.js(&name); - results.push(name); - } - if amt > 1 { - me.src.js("]"); - } - me.src.js(" = "); - }; - - bind_results(self); - call(self); - self.src.js(";\n"); + self.bind_results(func.results.len(), results); + uwriteln!(self.src.js, "{}({});", self.callee, operands.join(", ")); } - Instruction::Return { amt, func } => { - if !self.gen.in_import && iface.guest_export_needs_post_return(func) { - let name = &func.name; - self.src.js(&format!( - "{}._exports[\"cabi_post_{name}\"](ret);\n", - self.src_object - )); + Instruction::Return { amt, .. } => { + if let Some(f) = &self.post_return { + uwriteln!(self.src.js, "{f}(ret);"); } match amt { 0 => {} - 1 => self.src.js(&format!("return {};\n", operands[0])), - _ => self.src.js(&format!("return [{}];\n", operands.join(", "))), + 1 => uwriteln!(self.src.js, "return {};", operands[0]), + _ => uwriteln!(self.src.js, "return [{}];", operands.join(", ")), } } @@ -1914,18 +2241,14 @@ impl Bindgen for FunctionBindgen<'_> { Instruction::I32Store8 { offset } => self.store("setInt8", *offset, operands), Instruction::I32Store16 { offset } => self.store("setInt16", *offset, operands), - Instruction::Malloc { - realloc, - size, - align, - } => { - self.needs_realloc = Some(realloc.to_string()); + Instruction::Malloc { size, align, .. } => { let tmp = self.tmp(); - let ptr = format!("ptr{}", tmp); - self.src.js(&format!( - "const {} = realloc(0, 0, {}, {});\n", - ptr, align, size - )); + let realloc = self.realloc.as_ref().unwrap(); + let ptr = format!("ptr{tmp}"); + uwriteln!( + self.src.js, + "const {ptr} = {realloc}(0, 0, {align}, {size});", + ); results.push(ptr); } @@ -1934,261 +2257,7 @@ impl Bindgen for FunctionBindgen<'_> { } } -impl Js { - fn print_intrinsics(&mut self) { - if self.all_intrinsics.contains(&Intrinsic::I32ToF32) - || self.all_intrinsics.contains(&Intrinsic::F32ToI32) - { - self.src.js(" - const I32_TO_F32_I = new Int32Array(1); - const I32_TO_F32_F = new Float32Array(I32_TO_F32_I.buffer); - "); - } - if self.all_intrinsics.contains(&Intrinsic::I64ToF64) - || self.all_intrinsics.contains(&Intrinsic::F64ToI64) - { - self.src.js(" - const I64_TO_F64_I = new BigInt64Array(1); - const I64_TO_F64_F = new Float64Array(I64_TO_F64_I.buffer); - "); - } - - if self.all_intrinsics.contains(&Intrinsic::Promises) { - self.all_intrinsics.insert(Intrinsic::Slab); - } - - for i in mem::take(&mut self.all_intrinsics) { - self.print_intrinsic(i); - } - } - - fn print_intrinsic(&mut self, i: Intrinsic) { - match i { - Intrinsic::ClampGuest => self.src.js(" - export function clamp_guest(i, min, max) { - if (i < min || i > max) \ - throw new RangeError(`must be between ${min} and ${max}`); - return i; - } - "), - - Intrinsic::DataView => self.src.js(" - let DATA_VIEW = new DataView(new ArrayBuffer()); - - export function data_view(mem) { - if (DATA_VIEW.buffer !== mem.buffer) \ - DATA_VIEW = new DataView(mem.buffer); - return DATA_VIEW; - } - "), - - Intrinsic::ValidateGuestChar => self.src.js(" - export function validate_guest_char(i) { - if ((i > 0x10ffff) || (i >= 0xd800 && i <= 0xdfff)) \ - throw new RangeError(`not a valid char`); - return String.fromCodePoint(i); - } - "), - - // TODO: this is incorrect. It at least allows strings of length > 0 - // but it probably doesn't do the right thing for unicode or invalid - // utf16 strings either. - Intrinsic::ValidateHostChar => self.src.js(" - export function validate_host_char(s) { - if (typeof s !== 'string') \ - throw new TypeError(`must be a string`); - return s.codePointAt(0); - } - "), - - - Intrinsic::ToInt32 => self.src.js(" - export function to_int32(val) { - return val >> 0; - } - "), - Intrinsic::ToUint32 => self.src.js(" - export function to_uint32(val) { - return val >>> 0; - } - "), - - Intrinsic::ToInt16 => self.src.js(" - export function to_int16(val) { - val >>>= 0; - val %= 2 ** 16; - if (val >= 2 ** 15) { - val -= 2 ** 16; - } - return val; - } - "), - Intrinsic::ToUint16 => self.src.js(" - export function to_uint16(val) { - val >>>= 0; - val %= 2 ** 16; - return val; - } - "), - Intrinsic::ToInt8 => self.src.js(" - export function to_int8(val) { - val >>>= 0; - val %= 2 ** 8; - if (val >= 2 ** 7) { - val -= 2 ** 8; - } - return val; - } - "), - Intrinsic::ToUint8 => self.src.js(" - export function to_uint8(val) { - val >>>= 0; - val %= 2 ** 8; - return val; - } - "), - - Intrinsic::ToBigInt64 => self.src.js(" - export function to_int64(val) { - return BigInt.asIntN(64, val); - } - "), - Intrinsic::ToBigUint64 => self.src.js(" - export function to_uint64(val) { - return BigInt.asUintN(64, val); - } - "), - - Intrinsic::ToString => self.src.js(" - export function to_string(val) { - if (typeof val === 'symbol') { - throw new TypeError('symbols cannot be converted to strings'); - } else { - // Calling `String` almost directly calls `ToString`, except that it also allows symbols, - // which is why we have the symbol-rejecting branch above. - // - // Definition of `String`: https://tc39.es/ecma262/#sec-string-constructor-string-value - return String(val); - } - } - "), - - Intrinsic::I32ToF32 => self.src.js(" - export function i32ToF32(i) { - I32_TO_F32_I[0] = i; - return I32_TO_F32_F[0]; - } - "), - Intrinsic::F32ToI32 => self.src.js(" - export function f32ToI32(f) { - I32_TO_F32_F[0] = f; - return I32_TO_F32_I[0]; - } - "), - Intrinsic::I64ToF64 => self.src.js(" - export function i64ToF64(i) { - I64_TO_F64_I[0] = i; - return I64_TO_F64_F[0]; - } - "), - Intrinsic::F64ToI64 => self.src.js(" - export function f64ToI64(f) { - I64_TO_F64_F[0] = f; - return I64_TO_F64_I[0]; - } - "), - - Intrinsic::Utf8Decoder => self - .src - .js("export const UTF8_DECODER = new TextDecoder('utf-8');\n"), - - Intrinsic::Utf8EncodedLen => self.src.js("export let UTF8_ENCODED_LEN = 0;\n"), - - Intrinsic::Utf8Encode => self.src.js(" - const UTF8_ENCODER = new TextEncoder('utf-8'); - - export function utf8_encode(s, realloc, memory) { - if (typeof s !== 'string') \ - throw new TypeError('expected a string'); - - if (s.length === 0) { - UTF8_ENCODED_LEN = 0; - return 1; - } - - let alloc_len = 0; - let ptr = 0; - let writtenTotal = 0; - while (s.length > 0) { - ptr = realloc(ptr, alloc_len, 1, alloc_len + s.length); - alloc_len += s.length; - const { read, written } = UTF8_ENCODER.encodeInto( - s, - new Uint8Array(memory.buffer, ptr + writtenTotal, alloc_len - writtenTotal), - ); - writtenTotal += written; - s = s.slice(read); - } - if (alloc_len > writtenTotal) - ptr = realloc(ptr, alloc_len, 1, writtenTotal); - UTF8_ENCODED_LEN = writtenTotal; - return ptr; - } - "), - - Intrinsic::Slab => self.src.js(" - export class Slab { - constructor() { - this.list = []; - this.head = 0; - } - - insert(val) { - if (this.head >= this.list.length) { - this.list.push({ - next: this.list.length + 1, - val: undefined, - }); - } - const ret = this.head; - const slot = this.list[ret]; - this.head = slot.next; - slot.next = -1; - slot.val = val; - return ret; - } - - get(idx) { - if (idx >= this.list.length) - throw new RangeError('handle index not valid'); - const slot = this.list[idx]; - if (slot.next === -1) - return slot.val; - throw new RangeError('handle index not valid'); - } - - remove(idx) { - const ret = this.get(idx); // validate the slot - const slot = this.list[idx]; - slot.val = undefined; - slot.next = this.head; - this.head = idx; - return ret; - } - } - "), - - Intrinsic::Promises => self.src.js("export const PROMISES = new Slab();\n"), - Intrinsic::ThrowInvalidBool => self.src.js(" - export function throw_invalid_bool() { - throw new RangeError(\"invalid variant discriminant for bool\"); - } - "), - } - } -} - -pub fn to_js_ident(name: &str) -> &str { +fn to_js_ident(name: &str) -> &str { match name { "in" => "in_", "import" => "import_", diff --git a/crates/gen-host-js/tests/codegen.rs b/crates/gen-host-js/tests/codegen.rs index 3a5500859..0e5ebab67 100644 --- a/crates/gen-host-js/tests/codegen.rs +++ b/crates/gen-host-js/tests/codegen.rs @@ -5,16 +5,16 @@ macro_rules! gen_test { ($name:ident $test:tt $dir:ident) => { #[test] fn $name() { - test_helpers::run_codegen_test( + drop(include_str!($test)); + test_helpers::run_component_codegen_test( "js", - std::path::Path::new($test) - .file_stem() - .unwrap() - .to_str() - .unwrap(), - include_str!($test), + $test.as_ref(), test_helpers::Direction::$dir, - wit_bindgen_gen_host_js::Opts::default().build(), + |name, component, files| { + wit_bindgen_gen_host_js::Opts::default() + .generate(name, component, files) + .unwrap() + }, super::verify, ) } diff --git a/crates/gen-host-js/tests/helpers.d.ts b/crates/gen-host-js/tests/helpers.d.ts deleted file mode 100644 index 724a20977..000000000 --- a/crates/gen-host-js/tests/helpers.d.ts +++ /dev/null @@ -1,7 +0,0 @@ -export function getWasm(): Uint8Array; - -export interface Wasi { - start(instance: WebAssembly.Instance): void; -} - -export function addWasiToImports(importObj: any): Wasi; diff --git a/crates/gen-host-js/tests/helpers.js b/crates/gen-host-js/tests/helpers.js deleted file mode 100644 index 13c0a7d1e..000000000 --- a/crates/gen-host-js/tests/helpers.js +++ /dev/null @@ -1,26 +0,0 @@ -import { readFileSync } from 'fs'; -import { WASI } from 'wasi'; - -export function getWasm() { - return readFileSync(process.argv[2]); -} - -class MyWasi { - constructor(wasi) { - this.wasi = wasi; - } - - start(instance) { - if ('_start' in instance.exports) { - this.wasi.start(instance); - } else { - this.wasi.initialize(instance); - } - } -} - -export function addWasiToImports(importObj) { - const wasi = new WASI(); - importObj.wasi_snapshot_preview1 = wasi.wasiImport; - return new MyWasi(wasi); -} diff --git a/crates/gen-host-js/tests/helpers.ts b/crates/gen-host-js/tests/helpers.ts new file mode 100644 index 000000000..5de607ae8 --- /dev/null +++ b/crates/gen-host-js/tests/helpers.ts @@ -0,0 +1,23 @@ +// @ts-ignore +import { readFile } from 'node:fs/promises'; +// @ts-ignore +import { argv, stdout } from 'node:process'; + +// This is a helper function used from `host.ts` test in the `tests/runtime/*` +// directory to pass as the `instantiateCore` argument to the `instantiate` +// function generated by `wit-bindgen`. +// +// This function loads the module named by `path` and instantiates it with the +// `imports` object provided. The `path` is a relative path to a wasm file +// within the generated directory which for tests is passed as argv 2. +export async function loadWasm(path: string, imports: any) { + const root = argv[2]; + const m = await WebAssembly.compile(await readFile(root + '/' + path)) + return await WebAssembly.instantiate(m, imports); +} + +export const testwasi = { + log(bytes: Uint8Array) { + stdout.write(bytes); + }, +}; diff --git a/crates/gen-host-js/tests/runtime.rs b/crates/gen-host-js/tests/runtime.rs index f6c0f1693..9b2f0a57c 100644 --- a/crates/gen-host-js/tests/runtime.rs +++ b/crates/gen-host-js/tests/runtime.rs @@ -2,26 +2,23 @@ use std::env; use std::fs; use std::path::Path; use std::process::Command; -use wit_bindgen_core::Generator; -test_helpers::runtime_tests!("ts"); +test_helpers::runtime_component_tests!("ts"); -fn execute(name: &str, wasm: &Path, ts: &Path, imports: &Path, exports: &Path) { - let dir = test_helpers::test_directory("runtime", "wasmtime-py", name); +fn execute(name: &str, lang: &str, wasm: &Path, ts: &Path) { + let dir = test_helpers::test_directory("runtime", "js", &format!("{name}-{lang}")); + let wasm = std::fs::read(wasm).unwrap(); println!("OUT_DIR = {:?}", dir); println!("Generating bindings..."); - // We call `generate_all` with exports from the imports.wit file, and - // imports from the exports.wit wit file. It's reversed because we're - // implementing the host side of these APIs. - let imports = wit_bindgen_core::wit_parser::Interface::parse_file(imports).unwrap(); - let exports = wit_bindgen_core::wit_parser::Interface::parse_file(exports).unwrap(); let mut files = Default::default(); wit_bindgen_gen_host_js::Opts::default() - .build() - .generate_all(&[exports], &[imports], &mut files); + .generate(name, &wasm, &mut files) + .unwrap(); for (file, contents) in files.iter() { - fs::write(dir.join(file), contents).unwrap(); + let dst = dir.join(file); + std::fs::create_dir_all(dst.parent().unwrap()).unwrap(); + std::fs::write(&dst, contents).unwrap(); } let (cmd, args) = if cfg!(windows) { @@ -31,8 +28,7 @@ fn execute(name: &str, wasm: &Path, ts: &Path, imports: &Path, exports: &Path) { }; fs::copy(ts, dir.join("host.ts")).unwrap(); - fs::copy("tests/helpers.d.ts", dir.join("helpers.d.ts")).unwrap(); - fs::copy("tests/helpers.js", dir.join("helpers.js")).unwrap(); + fs::copy("tests/helpers.ts", dir.join("helpers.ts")).unwrap(); let config = dir.join("tsconfig.json"); fs::write( &config, @@ -55,43 +51,23 @@ fn execute(name: &str, wasm: &Path, ts: &Path, imports: &Path, exports: &Path) { ) .unwrap(); - run(Command::new(cmd) - .args(args) - .arg("tsc") - .arg("--project") - .arg(&config)); - - // Currently there's mysterious uvwasi errors creating a `WASI` on Windows. - // Unsure what's happening so let's ignore these tests for now since there's - // not much Windows-specific here anyway. - if cfg!(windows) { - return; - } + test_helpers::run_command( + Command::new(cmd) + .args(args) + .arg("tsc") + .arg("--project") + .arg(&config), + ); fs::write(dir.join("package.json"), "{\"type\":\"module\"}").unwrap(); let mut path = Vec::new(); path.push(env::current_dir().unwrap()); path.push(dir.clone()); - println!("{:?}", std::env::join_paths(&path)); - run(Command::new("node") - .arg("--experimental-wasi-unstable-preview1") - .arg("--stack-trace-limit=1000") - .arg(dir.join("host.js")) - .env("NODE_PATH", std::env::join_paths(&path).unwrap()) - .arg(wasm)); -} - -fn run(cmd: &mut Command) { - println!("running {:?}", cmd); - let output = cmd.output().expect("failed to executed"); - println!("status: {}", output.status); - println!( - "stdout:\n {}", - String::from_utf8_lossy(&output.stdout).replace("\n", "\n ") - ); - println!( - "stderr:\n {}", - String::from_utf8_lossy(&output.stderr).replace("\n", "\n ") + test_helpers::run_command( + Command::new("node") + .arg("--stack-trace-limit=1000") + .arg(dir.join("host.js")) + .env("NODE_PATH", std::env::join_paths(&path).unwrap()) + .arg(dir), ); - assert!(output.status.success()); } diff --git a/crates/test-helpers/Cargo.toml b/crates/test-helpers/Cargo.toml index b5c9fbaeb..81c7d651c 100644 --- a/crates/test-helpers/Cargo.toml +++ b/crates/test-helpers/Cargo.toml @@ -9,12 +9,15 @@ doctest = false test = false [dependencies] -test-helpers-macros = { path = 'macros' } +test-helpers-macros = { path = 'macros', optional = true } wit-bindgen-core = { workspace = true } wit-parser = { workspace = true } +wit-component = { workspace = true } +wat = { workspace = true } [features] -default = ['guest-rust', 'guest-c', 'guest-teavm-java'] -guest-rust = ['test-helpers-macros/guest-rust'] -guest-c = ['test-helpers-macros/guest-c'] -guest-teavm-java = ['test-helpers-macros/guest-teavm-java'] +default = ['guest-rust', 'guest-c', 'guest-teavm-java', 'macros'] +macros = ['dep:test-helpers-macros'] +guest-rust = ['test-helpers-macros?/guest-rust'] +guest-c = ['test-helpers-macros?/guest-c'] +guest-teavm-java = ['test-helpers-macros?/guest-teavm-java'] diff --git a/crates/test-helpers/macros/src/lib.rs b/crates/test-helpers/macros/src/lib.rs index 5a719a6f7..08a337361 100644 --- a/crates/test-helpers/macros/src/lib.rs +++ b/crates/test-helpers/macros/src/lib.rs @@ -53,6 +53,17 @@ pub fn codegen_tests(input: TokenStream) -> TokenStream { /// all host tests that use the "js" extension. #[proc_macro] pub fn runtime_tests(input: TokenStream) -> TokenStream { + generate_runtime_tests(input, false) +} + +/// Same as `runtime_tests!` but iterates over component wasms instead of core +/// wasms. +#[proc_macro] +pub fn runtime_component_tests(input: TokenStream) -> TokenStream { + generate_runtime_tests(input, true) +} + +fn generate_runtime_tests(input: TokenStream, use_components: bool) -> TokenStream { let host_extension = input.to_string(); let host_extension = host_extension.trim_matches('"'); let host_file = format!("host.{}", host_extension); @@ -64,27 +75,41 @@ pub fn runtime_tests(input: TokenStream) -> TokenStream { continue; } let name_str = entry.file_name().unwrap().to_str().unwrap(); - for (lang, name, wasm, _component) in WASMS { + for (lang, name, wasm, component) in WASMS { if *name != name_str { continue; } - let name_str = format!("{}_{}", name_str, lang); - let name = quote::format_ident!("{}", name_str); + let name = quote::format_ident!("{}_{}", name_str, lang); let host_file = entry.join(&host_file).to_str().unwrap().to_string(); let import_wit = entry.join("imports.wit").to_str().unwrap().to_string(); let export_wit = entry.join("exports.wit").to_str().unwrap().to_string(); - tests.push(quote::quote! { - #[test] - fn #name() { - crate::execute( - #name_str, - #wasm.as_ref(), - #host_file.as_ref(), - #import_wit.as_ref(), - #export_wit.as_ref(), - ) - } - }); + if use_components { + tests.push(quote::quote! { + #[test] + fn #name() { + crate::execute( + #name_str, + #lang, + #component.as_ref(), + #host_file.as_ref(), + ) + } + }); + } else { + let name_str = format!("{}_{}", name_str, lang); + tests.push(quote::quote! { + #[test] + fn #name() { + crate::execute( + #name_str, + #wasm.as_ref(), + #host_file.as_ref(), + #import_wit.as_ref(), + #export_wit.as_ref(), + ) + } + }); + } } } diff --git a/crates/test-helpers/src/lib.rs b/crates/test-helpers/src/lib.rs index 7605dda56..da9ba5c84 100644 --- a/crates/test-helpers/src/lib.rs +++ b/crates/test-helpers/src/lib.rs @@ -1,9 +1,12 @@ +#[cfg(feature = "macros")] pub use test_helpers_macros::*; use std::fs; use std::path::{Path, PathBuf}; use std::process::Command; -use wit_bindgen_core::Generator; +use wit_bindgen_core::{Files, Generator}; +use wit_parser::abi::{AbiVariant, WasmType}; +use wit_parser::{Function, Interface}; pub enum Direction { Import, @@ -25,7 +28,7 @@ pub fn run_codegen_test( verify: fn(&Path, &str), ) { let mut files = Default::default(); - let iface = wit_parser::Interface::parse(wit_name, wit_contents).unwrap(); + let iface = Interface::parse(wit_name, wit_contents).unwrap(); let (imports, exports) = match dir { Direction::Import => (vec![iface], vec![]), Direction::Export => (vec![], vec![iface]), @@ -89,3 +92,129 @@ stderr --- stderr = String::from_utf8_lossy(&output.stderr).replace("\n", "\n\t"), ); } + +pub fn run_component_codegen_test( + gen_name: &str, + wit_path: &Path, + dir: Direction, + generate: fn(&str, &[u8], &mut Files), + verify: fn(&Path, &str), +) { + let mut encoder = wit_component::ComponentEncoder::default(); + let iface = Interface::parse_file(wit_path).unwrap(); + + let wasm = match dir { + Direction::Import => { + encoder = encoder.imports([iface.clone()]).unwrap(); + dummy_module(&[iface], &[], None) + } + Direction::Export => { + encoder = encoder.interface(iface.clone()).unwrap(); + dummy_module(&[], &[], Some(&iface)) + } + }; + + let component = encoder + .module(&wasm) + .unwrap() + .validate(true) + .encode() + .unwrap(); + + let name = wit_path.file_stem().and_then(|s| s.to_str()).unwrap(); + + let gen_name = format!( + "{gen_name}-{}", + match dir { + Direction::Import => "import", + Direction::Export => "export", + } + ); + let dir = test_directory("codegen", &gen_name, name); + std::fs::write(dir.join("component.wasm"), &component).unwrap(); + + let mut files = Default::default(); + generate(name, &component, &mut files); + for (file, contents) in files.iter() { + let dst = dir.join(file); + std::fs::create_dir_all(dst.parent().unwrap()).unwrap(); + std::fs::write(&dst, contents).unwrap(); + } + + verify(&dir, name); +} + +pub fn dummy_module( + imports: &[Interface], + exports: &[Interface], + default: Option<&Interface>, +) -> Vec { + let mut wat = String::new(); + wat.push_str("(module\n"); + for import in imports { + for func in import.functions.iter() { + let sig = import.wasm_signature(AbiVariant::GuestImport, func); + + wat.push_str(&format!( + "(import \"{}\" \"{}\" (func", + import.name, func.name + )); + push_tys(&mut wat, "param", &sig.params); + push_tys(&mut wat, "result", &sig.results); + wat.push_str("))\n"); + } + } + + for export in exports { + for func in export.functions.iter() { + let name = format!("{}#{}", export.name, func.name); + push_func(&mut wat, &name, export, func); + } + } + + if let Some(default) = default { + for func in default.functions.iter() { + push_func(&mut wat, &func.name, default, func); + } + } + + wat.push_str("(memory (export \"memory\") 0)\n"); + wat.push_str( + "(func (export \"cabi_realloc\") (param i32 i32 i32 i32) (result i32) unreachable)\n", + ); + wat.push_str(")\n"); + + return wat::parse_str(&wat).unwrap(); + + fn push_func(wat: &mut String, name: &str, iface: &Interface, func: &Function) { + let sig = iface.wasm_signature(AbiVariant::GuestExport, func); + wat.push_str(&format!("(func (export \"{name}\")")); + push_tys(wat, "param", &sig.params); + push_tys(wat, "result", &sig.results); + wat.push_str(" unreachable)\n"); + + if iface.guest_export_needs_post_return(func) { + wat.push_str(&format!("(func (export \"cabi_post_{name}\")")); + push_tys(wat, "param", &sig.results); + wat.push_str(")\n"); + } + } + + fn push_tys(dst: &mut String, desc: &str, params: &[WasmType]) { + if params.is_empty() { + return; + } + dst.push_str(" ("); + dst.push_str(desc); + for ty in params { + dst.push_str(" "); + match ty { + WasmType::I32 => dst.push_str("i32"), + WasmType::I64 => dst.push_str("i64"), + WasmType::F32 => dst.push_str("f32"), + WasmType::F64 => dst.push_str("f64"), + } + } + dst.push_str(")"); + } +} diff --git a/crates/wasi_snapshot_preview1/src/lib.rs b/crates/wasi_snapshot_preview1/src/lib.rs index 7f415534c..cf1176580 100644 --- a/crates/wasi_snapshot_preview1/src/lib.rs +++ b/crates/wasi_snapshot_preview1/src/lib.rs @@ -81,6 +81,9 @@ pub extern "C" fn fd_write( mut iovs_len: usize, nwritten: *mut Size, ) -> Errno { + if fd != 1 { + unreachable(); + } unsafe { // Advance to the first non-empty buffer. while iovs_len != 0 && (*iovs_ptr).buf_len == 0 { @@ -116,3 +119,19 @@ pub extern "C" fn fd_close(fd: Fd) -> Errno { pub extern "C" fn proc_exit(rval: Exitcode) -> ! { unreachable() } + +#[no_mangle] +pub extern "C" fn fd_fdstat_get(fd: Fd, fdstat: *mut Fdstat) -> Errno { + if fd != 1 { + unreachable(); + } + + unsafe { + (*fdstat).fs_filetype = FILETYPE_UNKNOWN; + (*fdstat).fs_flags = FDFLAGS_APPEND; + (*fdstat).fs_rights_base = RIGHTS_FD_WRITE; + (*fdstat).fs_rights_inheriting = RIGHTS_FD_WRITE; + } + + ERRNO_SUCCESS +} diff --git a/crates/wit-bindgen-demo/Cargo.toml b/crates/wit-bindgen-demo/Cargo.toml index b872578d0..48d496bfa 100644 --- a/crates/wit-bindgen-demo/Cargo.toml +++ b/crates/wit-bindgen-demo/Cargo.toml @@ -11,6 +11,7 @@ test = false doctest = false [dependencies] +anyhow = { workspace = true } wit-bindgen-core = { workspace = true } wit-bindgen-gen-guest-rust = { workspace = true } wit-bindgen-gen-host-wasmtime-rust = { workspace = true } @@ -21,3 +22,5 @@ wit-bindgen-gen-guest-teavm-java = { workspace = true } wit-bindgen-gen-markdown = { workspace = true } wit-bindgen-guest-rust = { workspace = true } wasmprinter = { workspace = true } +wit-component = { workspace = true } +test-helpers = { path = '../test-helpers', default-features = false } diff --git a/crates/wit-bindgen-demo/build.sh b/crates/wit-bindgen-demo/build.sh index ff9e54d24..d2b594927 100755 --- a/crates/wit-bindgen-demo/build.sh +++ b/crates/wit-bindgen-demo/build.sh @@ -5,14 +5,17 @@ set -ex rm -rf static mkdir static +# Build the core wasm binary that will become a component cargo build -p wit-bindgen-demo --target wasm32-unknown-unknown --release -cp target/wasm32-unknown-unknown/release/wit_bindgen_demo.wasm static/demo.wasm -cargo run host js \ - --export crates/wit-bindgen-demo/browser.wit \ - --import crates/wit-bindgen-demo/demo.wit \ - --out-dir static +# Translate the core wasm binary to a component +cargo run --release -p wit-component --bin wit-component -- \ + target/wasm32-unknown-unknown/release/wit_bindgen_demo.wasm -o target/demo.wasm +# Generate JS host bindings +cargo run host js target/demo.wasm --out-dir static + +# Build JS from TypeScript and then copy in the ace editor as well. cp crates/wit-bindgen-demo/{index.html,main.ts} static/ (cd crates/wit-bindgen-demo && npx tsc ../../static/main.ts --target es6) diff --git a/crates/wit-bindgen-demo/browser.wit b/crates/wit-bindgen-demo/console.wit similarity index 100% rename from crates/wit-bindgen-demo/browser.wit rename to crates/wit-bindgen-demo/console.wit diff --git a/crates/wit-bindgen-demo/main.ts b/crates/wit-bindgen-demo/main.ts index d1f514453..6fa455f93 100644 --- a/crates/wit-bindgen-demo/main.ts +++ b/crates/wit-bindgen-demo/main.ts @@ -1,5 +1,4 @@ -import { Demo, Options } from './demo.js'; -import * as browser from './browser.js'; +import { Demo, Options, instantiate } from './demo.js'; class Editor { input: HTMLTextAreaElement; @@ -9,7 +8,7 @@ class Editor { rustUnchecked: HTMLInputElement; wasmtimeTracing: HTMLInputElement; generatedFiles: Record; - demo: Demo; + demo?: Demo; options: Options; rerender: number | null; inputEditor: AceAjax.Editor; @@ -34,7 +33,6 @@ class Editor { this.outputEditor.setOption("useWorker", false); this.generatedFiles = {}; - this.demo = new Demo(); this.options = { rustUnchecked: false, wasmtimeTracing: false, @@ -44,13 +42,13 @@ class Editor { } async instantiate() { - const imports = {}; - const obj = { - log: console.log, - error: console.error, - }; - browser.addBrowserToImports(imports, obj, name => this.demo.instance.exports[name]); - await this.demo.instantiate(fetch('./demo.wasm'), imports); + this.demo = await instantiate( + async (name, imports) => { + const obj = await WebAssembly.instantiateStreaming(fetch(name), imports) + return obj.instance; + }, + { console }, + ); this.installListeners(); this.render(); } diff --git a/crates/wit-bindgen-demo/src/lib.rs b/crates/wit-bindgen-demo/src/lib.rs index 6d57eb2a0..9687ea054 100644 --- a/crates/wit-bindgen-demo/src/lib.rs +++ b/crates/wit-bindgen-demo/src/lib.rs @@ -1,9 +1,10 @@ +use anyhow::Result; use std::sync::Once; use wit_bindgen_core::wit_parser::Interface; -use wit_bindgen_core::Generator; +use wit_bindgen_core::{Files, Generator}; wit_bindgen_guest_rust::export!("demo.wit"); -wit_bindgen_guest_rust::import!("browser.wit"); +wit_bindgen_guest_rust::import!("console.wit"); struct Demo; @@ -13,42 +14,11 @@ impl demo::Demo for Demo { wit: String, options: demo::Options, ) -> Result, String> { - static INIT: Once = Once::new(); - INIT.call_once(|| { - let prev_hook = std::panic::take_hook(); - std::panic::set_hook(Box::new(move |info| { - browser::error(&info.to_string()); - prev_hook(info); - })); - }); + init(); + + let mut files = Files::default(); + render(lang, &wit, &mut files, &options).map_err(|e| format!("{:?}", e))?; - let mut gen: Box = match lang { - demo::Lang::Rust => Box::new({ - let mut opts = wit_bindgen_gen_guest_rust::Opts::default(); - opts.unchecked = options.rust_unchecked; - opts.build() - }), - demo::Lang::Java => Box::new(wit_bindgen_gen_guest_teavm_java::Opts::default().build()), - demo::Lang::Wasmtime => Box::new({ - let mut opts = wit_bindgen_gen_host_wasmtime_rust::Opts::default(); - opts.tracing = options.wasmtime_tracing; - opts.build() - }), - demo::Lang::WasmtimePy => { - Box::new(wit_bindgen_gen_host_wasmtime_py::Opts::default().build()) - } - demo::Lang::Js => Box::new(wit_bindgen_gen_host_js::Opts::default().build()), - demo::Lang::C => Box::new(wit_bindgen_gen_guest_c::Opts::default().build()), - demo::Lang::Markdown => Box::new(wit_bindgen_gen_markdown::Opts::default().build()), - }; - let iface = Interface::parse("input", &wit).map_err(|e| format!("{:?}", e))?; - let mut files = Default::default(); - let (imports, exports) = if options.import { - (vec![iface], vec![]) - } else { - (vec![], vec![iface]) - }; - gen.generate_all(&imports, &exports, &mut files); Ok(files .iter() .map(|(name, contents)| { @@ -62,3 +32,78 @@ impl demo::Demo for Demo { .collect()) } } + +fn init() { + static INIT: Once = Once::new(); + INIT.call_once(|| { + console::log("installing panic hook"); + let prev_hook = std::panic::take_hook(); + std::panic::set_hook(Box::new(move |info| { + console::error(&info.to_string()); + prev_hook(info); + })); + }); +} + +fn render(lang: demo::Lang, wit: &str, files: &mut Files, options: &demo::Options) -> Result<()> { + let iface = Interface::parse("input", &wit)?; + + let mut gen_world = |mut gen: Box| { + let (imports, exports) = if options.import { + (vec![iface.clone()], vec![]) + } else { + (vec![], vec![iface.clone()]) + }; + gen.generate_all(&imports, &exports, files); + }; + + match lang { + demo::Lang::Rust => { + let mut opts = wit_bindgen_gen_guest_rust::Opts::default(); + opts.unchecked = options.rust_unchecked; + gen_world(Box::new(opts.build())) + } + demo::Lang::Java => gen_world(Box::new( + wit_bindgen_gen_guest_teavm_java::Opts::default().build(), + )), + demo::Lang::Wasmtime => { + let mut opts = wit_bindgen_gen_host_wasmtime_rust::Opts::default(); + opts.tracing = options.wasmtime_tracing; + gen_world(Box::new(opts.build())) + } + demo::Lang::WasmtimePy => gen_world(Box::new( + wit_bindgen_gen_host_wasmtime_py::Opts::default().build(), + )), + demo::Lang::C => gen_world(Box::new(wit_bindgen_gen_guest_c::Opts::default().build())), + demo::Lang::Markdown => { + gen_world(Box::new(wit_bindgen_gen_markdown::Opts::default().build())) + } + + // JS is different from other languages at this time where it takes a + // component as input as opposed to an `Interface`. To work with this + // demo a dummy component is synthesized to generate bindings for. The + // dummy core wasm module is created from the `test_helpers` support + // this workspace already offsets, and then `wit-component` is used to + // synthesize a component from our input interface and dummy module. + // Finally this component is fed into the host generator which gives us + // the files we want. + demo::Lang::Js => { + let (imports, interface) = if options.import { + (vec![iface], None) + } else { + (Vec::new(), Some(iface)) + }; + let dummy = test_helpers::dummy_module(&imports, &[], interface.as_ref()); + let mut encoder = wit_component::ComponentEncoder::default() + .module(&dummy)? + .imports(imports)?; + if let Some(iface) = interface { + encoder = encoder.interface(iface)?; + } + let wasm = encoder.encode()?; + wit_bindgen_gen_host_js::Opts::default().generate("input", &wasm, files)?; + } + } + + Ok(()) +} diff --git a/crates/wit-component/Cargo.toml b/crates/wit-component/Cargo.toml index f47b64f7a..b74eaba0d 100644 --- a/crates/wit-component/Cargo.toml +++ b/crates/wit-component/Cargo.toml @@ -31,6 +31,7 @@ wasmprinter = { workspace = true } glob = "0.3.0" pretty_assertions = "1.3.0" env_logger = { workspace = true } +test-helpers = { path = '../test-helpers', default-features = false } [features] default = ["cli"] diff --git a/crates/wit-component/tests/interfaces.rs b/crates/wit-component/tests/interfaces.rs index 31fa62e1e..88d13fde7 100644 --- a/crates/wit-component/tests/interfaces.rs +++ b/crates/wit-component/tests/interfaces.rs @@ -3,8 +3,7 @@ use pretty_assertions::assert_eq; use std::fs; use std::path::{Path, PathBuf}; use wit_component::ComponentEncoder; -use wit_parser::abi::{AbiVariant, WasmType}; -use wit_parser::{Function, Interface}; +use wit_parser::Interface; /// Tests the encoding of the "types only" mode of `wit-component`. /// @@ -115,7 +114,8 @@ fn run_test(path: &Path) -> Result<()> { // recover the original `*.wit` interfaces from the component output. println!("test dummy module"); - let module = dummy_module(&import_interfaces, &export_interfaces, default.as_ref()); + let module = + test_helpers::dummy_module(&import_interfaces, &export_interfaces, default.as_ref()); let mut encoder = ComponentEncoder::default() .module(&module)? .validate(true) @@ -173,78 +173,3 @@ fn read_interfaces(dir: &Path, pattern: &str) -> Result>() } - -fn dummy_module( - imports: &[Interface], - exports: &[Interface], - default: Option<&Interface>, -) -> Vec { - let mut wat = String::new(); - wat.push_str("(module\n"); - for import in imports { - for func in import.functions.iter() { - let sig = import.wasm_signature(AbiVariant::GuestImport, func); - - wat.push_str(&format!( - "(import \"{}\" \"{}\" (func", - import.name, func.name - )); - push_tys(&mut wat, "param", &sig.params); - push_tys(&mut wat, "result", &sig.results); - wat.push_str("))\n"); - } - } - - for export in exports { - for func in export.functions.iter() { - let name = format!("{}#{}", export.name, func.name); - push_func(&mut wat, &name, export, func); - } - } - - if let Some(default) = default { - for func in default.functions.iter() { - push_func(&mut wat, &func.name, default, func); - } - } - - wat.push_str("(memory (export \"memory\") 0)\n"); - wat.push_str( - "(func (export \"cabi_realloc\") (param i32 i32 i32 i32) (result i32) unreachable)\n", - ); - wat.push_str(")\n"); - - return wat::parse_str(&wat).unwrap(); - - fn push_func(wat: &mut String, name: &str, iface: &Interface, func: &Function) { - let sig = iface.wasm_signature(AbiVariant::GuestExport, func); - wat.push_str(&format!("(func (export \"{name}\")")); - push_tys(wat, "param", &sig.params); - push_tys(wat, "result", &sig.results); - wat.push_str(" unreachable)\n"); - - if iface.guest_export_needs_post_return(func) { - wat.push_str(&format!("(func (export \"cabi_post_{name}\")")); - push_tys(wat, "param", &sig.results); - wat.push_str(")\n"); - } - } - - fn push_tys(dst: &mut String, desc: &str, params: &[WasmType]) { - if params.is_empty() { - return; - } - dst.push_str(" ("); - dst.push_str(desc); - for ty in params { - dst.push_str(" "); - match ty { - WasmType::I32 => dst.push_str("i32"), - WasmType::I64 => dst.push_str("i64"), - WasmType::F32 => dst.push_str("f32"), - WasmType::F64 => dst.push_str("f64"), - } - } - dst.push_str(")"); - } -} diff --git a/src/bin/wit-bindgen.rs b/src/bin/wit-bindgen.rs index fe9a4d5fa..14bd73e63 100644 --- a/src/bin/wit-bindgen.rs +++ b/src/bin/wit-bindgen.rs @@ -1,4 +1,4 @@ -use anyhow::{Context, Result}; +use anyhow::{anyhow, Context, Result}; use clap::Parser; use std::path::PathBuf; use wit_bindgen_core::{wit_parser, Files, Generator}; @@ -31,6 +31,8 @@ enum Category { opts: wit_bindgen_gen_markdown::Opts, #[clap(flatten)] common: Common, + #[clap(flatten)] + world: World, }, } @@ -42,6 +44,8 @@ enum HostGenerator { opts: wit_bindgen_gen_host_wasmtime_rust::Opts, #[clap(flatten)] common: Common, + #[clap(flatten)] + world: World, }, /// Generates bindings for Python hosts using the Wasmtime engine. WasmtimePy { @@ -49,13 +53,20 @@ enum HostGenerator { opts: wit_bindgen_gen_host_wasmtime_py::Opts, #[clap(flatten)] common: Common, + #[clap(flatten)] + world: World, }, /// Generates bindings for JavaScript hosts. Js { #[clap(flatten)] opts: wit_bindgen_gen_host_js::Opts, + + component: PathBuf, #[clap(flatten)] common: Common, + + #[clap(long)] + name: Option, }, } @@ -67,6 +78,8 @@ enum GuestGenerator { opts: wit_bindgen_gen_guest_rust::Opts, #[clap(flatten)] common: Common, + #[clap(flatten)] + world: World, }, /// Generates bindings for C/CPP guest modules. C { @@ -74,6 +87,8 @@ enum GuestGenerator { opts: wit_bindgen_gen_guest_c::Opts, #[clap(flatten)] common: Common, + #[clap(flatten)] + world: World, }, /// Generates bindings for TeaVM-based Java guest modules. TeavmJava { @@ -81,15 +96,13 @@ enum GuestGenerator { opts: wit_bindgen_gen_guest_teavm_java::Opts, #[clap(flatten)] common: Common, + #[clap(flatten)] + world: World, }, } #[derive(Debug, Parser)] -struct Common { - /// Where to place output files - #[clap(long = "out-dir")] - out_dir: Option, - +struct World { /// Generate import bindings for the given `*.wit` interface. Can be /// specified multiple times. #[clap(long = "import", short)] @@ -101,37 +114,68 @@ struct Common { exports: Vec, } +#[derive(Debug, Parser, Clone)] +struct Common { + /// Where to place output files + #[clap(long = "out-dir")] + out_dir: Option, +} + +impl Opt { + fn common(&self) -> &Common { + match &self.category { + Category::Guest(GuestGenerator::Rust { common, .. }) + | Category::Guest(GuestGenerator::C { common, .. }) + | Category::Guest(GuestGenerator::TeavmJava { common, .. }) + | Category::Host(HostGenerator::WasmtimeRust { common, .. }) + | Category::Host(HostGenerator::WasmtimePy { common, .. }) + | Category::Host(HostGenerator::Js { common, .. }) + | Category::Markdown { common, .. } => common, + } + } +} + fn main() -> Result<()> { let opt: Opt = Opt::parse(); - let (mut generator, common): (Box, _) = match opt.category { - Category::Guest(GuestGenerator::Rust { opts, common }) => (Box::new(opts.build()), common), - Category::Host(HostGenerator::WasmtimeRust { opts, common }) => { - (Box::new(opts.build()), common) + let common = opt.common().clone(); + + let mut files = Files::default(); + match opt.category { + Category::Guest(GuestGenerator::Rust { opts, world, .. }) => { + gen_world(Box::new(opts.build()), world, &mut files)?; } - Category::Host(HostGenerator::WasmtimePy { opts, common }) => { - (Box::new(opts.build()), common) + Category::Host(HostGenerator::WasmtimeRust { opts, world, .. }) => { + gen_world(Box::new(opts.build()), world, &mut files)?; } - Category::Host(HostGenerator::Js { opts, common }) => (Box::new(opts.build()), common), - Category::Guest(GuestGenerator::C { opts, common }) => (Box::new(opts.build()), common), - Category::Guest(GuestGenerator::TeavmJava { opts, common }) => { - (Box::new(opts.build()), common) + Category::Host(HostGenerator::WasmtimePy { opts, world, .. }) => { + gen_world(Box::new(opts.build()), world, &mut files)?; } - Category::Markdown { opts, common } => (Box::new(opts.build()), common), - }; - - let imports = common - .imports - .iter() - .map(|wit| Interface::parse_file(wit)) - .collect::>>()?; - let exports = common - .exports - .iter() - .map(|wit| Interface::parse_file(wit)) - .collect::>>()?; - - let mut files = Files::default(); - generator.generate_all(&imports, &exports, &mut files); + Category::Host(HostGenerator::Js { + opts, + component, + name, + .. + }) => { + let wasm = wat::parse_file(&component)?; + let name = match &name { + Some(name) => name.as_str(), + None => component + .file_stem() + .and_then(|s| s.to_str()) + .ok_or_else(|| anyhow!("filename not valid utf-8"))?, + }; + opts.generate(name, &wasm, &mut files)?; + } + Category::Guest(GuestGenerator::C { opts, world, .. }) => { + gen_world(Box::new(opts.build()), world, &mut files)?; + } + Category::Guest(GuestGenerator::TeavmJava { opts, world, .. }) => { + gen_world(Box::new(opts.build()), world, &mut files)?; + } + Category::Markdown { opts, world, .. } => { + gen_world(Box::new(opts.build()), world, &mut files)?; + } + } for (name, contents) in files.iter() { let dst = match &common.out_dir { @@ -148,3 +192,19 @@ fn main() -> Result<()> { Ok(()) } + +fn gen_world(mut generator: Box, world: World, files: &mut Files) -> Result<()> { + let imports = world + .imports + .iter() + .map(|wit| Interface::parse_file(wit)) + .collect::>>()?; + let exports = world + .exports + .iter() + .map(|wit| Interface::parse_file(wit)) + .collect::>>()?; + + generator.generate_all(&imports, &exports, files); + Ok(()) +} diff --git a/tests/runtime/flavorful/host.ts b/tests/runtime/flavorful/host.ts index 277b288cb..a26753097 100644 --- a/tests/runtime/flavorful/host.ts +++ b/tests/runtime/flavorful/host.ts @@ -1,60 +1,53 @@ -import { addImportsToImports, Imports, MyErrno } from "./imports.js"; -import { Exports } from "./exports.js"; -import * as exports from "./exports.js"; -import { getWasm, addWasiToImports } from "./helpers.js"; +import { loadWasm, testwasi } from "./helpers.js"; +import { instantiate } from "./flavorful.js"; + // @ts-ignore import * as assert from 'assert'; async function run() { - const importObj = {}; - const imports: Imports = { - fListInRecord1(x) {}, - fListInRecord2() { return { a: 'list_in_record2' }; }, - fListInRecord3(x) { - assert.strictEqual(x.a, 'list_in_record3 input'); - return { a: 'list_in_record3 output' }; - }, - fListInRecord4(x) { - assert.strictEqual(x.a, 'input4'); - return { a: 'result4' }; - }, - fListInVariant1(a, b, c) { - assert.strictEqual(a, 'foo'); - assert.deepStrictEqual(b, { tag: 'err', val: 'bar' }); - assert.deepStrictEqual(c, { tag: 0, val: 'baz' }); - }, - fListInVariant2() { return 'list_in_variant2'; }, - fListInVariant3(x) { - assert.strictEqual(x, 'input3'); - return 'output3'; - }, + const wasm = await instantiate(loadWasm, { + testwasi, + imports: { + fListInRecord1(x) {}, + fListInRecord2() { return { a: 'list_in_record2' }; }, + fListInRecord3(x) { + assert.strictEqual(x.a, 'list_in_record3 input'); + return { a: 'list_in_record3 output' }; + }, + fListInRecord4(x) { + assert.strictEqual(x.a, 'input4'); + return { a: 'result4' }; + }, + fListInVariant1(a, b, c) { + assert.strictEqual(a, 'foo'); + assert.deepStrictEqual(b, { tag: 'err', val: 'bar' }); + assert.deepStrictEqual(c, { tag: 0, val: 'baz' }); + }, + fListInVariant2() { return 'list_in_variant2'; }, + fListInVariant3(x) { + assert.strictEqual(x, 'input3'); + return 'output3'; + }, - errnoResult() { return { tag: 'err', val: "b" }; }, - listTypedefs(x, y) { - assert.strictEqual(x, 'typedef1'); - assert.deepStrictEqual(y, ['typedef2']); - return [(new TextEncoder).encode('typedef3'), ['typedef4']]; - }, + errnoResult() { return { tag: 'err', val: "b" }; }, + listTypedefs(x, y) { + assert.strictEqual(x, 'typedef1'); + assert.deepStrictEqual(y, ['typedef2']); + return [(new TextEncoder).encode('typedef3'), ['typedef4']]; + }, - listOfVariants(bools, results, enums) { - assert.deepStrictEqual(bools, [true, false]); - assert.deepStrictEqual(results, [{ tag: 'ok', val: undefined }, { tag: 'err', val: undefined }]); - assert.deepStrictEqual(enums, ["success", "a"]); - return [ - [false, true], - [{ tag: 'err', val: undefined }, { tag: 'ok', val: undefined }], - ["a", "b"], - ]; + listOfVariants(bools, results, enums) { + assert.deepStrictEqual(bools, [true, false]); + assert.deepStrictEqual(results, [{ tag: 'ok', val: undefined }, { tag: 'err', val: undefined }]); + assert.deepStrictEqual(enums, ["success", "a"]); + return [ + [false, true], + [{ tag: 'err', val: undefined }, { tag: 'ok', val: undefined }], + ["a", "b"], + ]; + }, }, - }; - let instance: WebAssembly.Instance; - addImportsToImports(importObj, imports, name => instance.exports[name]); - const wasi = addWasiToImports(importObj); - - const wasm = new Exports(); - await wasm.instantiate(getWasm(), importObj); - wasi.start(wasm.instance); - instance = wasm.instance; + }); wasm.testImports(); wasm.fListInRecord1({ a: "list_in_record1" }); diff --git a/tests/runtime/invalid/host.ts b/tests/runtime/invalid/host.ts index 241020abf..adbc22736 100644 --- a/tests/runtime/invalid/host.ts +++ b/tests/runtime/invalid/host.ts @@ -1,59 +1,51 @@ -import { addImportsToImports, Imports } from "./imports.js"; -import { Exports } from "./exports.js"; -import { getWasm, addWasiToImports } from "./helpers.js"; +import { instantiate } from "./invalid.js"; +import { loadWasm, testwasi } from "./helpers.js"; // @ts-ignore import * as assert from 'assert'; async function run() { - const importObj = {}; - const imports: Imports = { - roundtripU8(x) { throw new Error('unreachable'); }, - roundtripS8(x) { throw new Error('unreachable'); }, - roundtripU16(x) { throw new Error('unreachable'); }, - roundtripS16(x) { throw new Error('unreachable'); }, - roundtripBool(x) { throw new Error('unreachable'); }, - roundtripChar(x) { throw new Error('unreachable'); }, - roundtripEnum(x) { throw new Error('unreachable'); }, - /* - unalignedRoundtrip1(u16, u32, u64, flag32, flag64) { - assert.deepStrictEqual(Array.from(u16), [1]); - assert.deepStrictEqual(Array.from(u32), [2]); - assert.deepStrictEqual(Array.from(u64), [3n]); - assert.deepStrictEqual(flag32, [{ - b0: false, b1: false, b2: false, b3: false, b4: false, b5: false, b6: false, b7: false, - b8: true, b9: false, b10: false, b11: false, b12: false, b13: false, b14: false, b15: false, - b16: false, b17: false, b18: false, b19: false, b20: false, b21: false, b22: false, b23: false, - b24: false, b25: false, b26: false, b27: false, b28: false, b29: false, b30: false, b31: false, - }]); - assert.deepStrictEqual(flag64, [{ - b0: false, b1: false, b2: false, b3: false, b4: false, b5: false, b6: false, b7: false, - b8: false, b9: true, b10: false, b11: false, b12: false, b13: false, b14: false, b15: false, - b16: false, b17: false, b18: false, b19: false, b20: false, b21: false, b22: false, b23: false, - b24: false, b25: false, b26: false, b27: false, b28: false, b29: false, b30: false, b31: false, - b32: false, b33: false, b34: false, b35: false, b36: false, b37: false, b38: false, b39: false, - b40: false, b41: false, b42: false, b43: false, b44: false, b45: false, b46: false, b47: false, - b48: false, b49: false, b50: false, b51: false, b52: false, b53: false, b54: false, b55: false, - b56: false, b57: false, b58: false, b59: false, b60: false, b61: false, b62: false, b63: false, - }]); + const wasm = await instantiate(loadWasm, { + testwasi, + imports: { + roundtripU8(x) { throw new Error('unreachable'); }, + roundtripS8(x) { throw new Error('unreachable'); }, + roundtripU16(x) { throw new Error('unreachable'); }, + roundtripS16(x) { throw new Error('unreachable'); }, + roundtripBool(x) { throw new Error('unreachable'); }, + roundtripChar(x) { throw new Error('unreachable'); }, + roundtripEnum(x) { throw new Error('unreachable'); }, + /* + unalignedRoundtrip1(u16, u32, u64, flag32, flag64) { + assert.deepStrictEqual(Array.from(u16), [1]); + assert.deepStrictEqual(Array.from(u32), [2]); + assert.deepStrictEqual(Array.from(u64), [3n]); + assert.deepStrictEqual(flag32, [{ + b0: false, b1: false, b2: false, b3: false, b4: false, b5: false, b6: false, b7: false, + b8: true, b9: false, b10: false, b11: false, b12: false, b13: false, b14: false, b15: false, + b16: false, b17: false, b18: false, b19: false, b20: false, b21: false, b22: false, b23: false, + b24: false, b25: false, b26: false, b27: false, b28: false, b29: false, b30: false, b31: false, + }]); + assert.deepStrictEqual(flag64, [{ + b0: false, b1: false, b2: false, b3: false, b4: false, b5: false, b6: false, b7: false, + b8: false, b9: true, b10: false, b11: false, b12: false, b13: false, b14: false, b15: false, + b16: false, b17: false, b18: false, b19: false, b20: false, b21: false, b22: false, b23: false, + b24: false, b25: false, b26: false, b27: false, b28: false, b29: false, b30: false, b31: false, + b32: false, b33: false, b34: false, b35: false, b36: false, b37: false, b38: false, b39: false, + b40: false, b41: false, b42: false, b43: false, b44: false, b45: false, b46: false, b47: false, + b48: false, b49: false, b50: false, b51: false, b52: false, b53: false, b54: false, b55: false, + b56: false, b57: false, b58: false, b59: false, b60: false, b61: false, b62: false, b63: false, + }]); + }, + unalignedRoundtrip2(record, f32, f64, string, list) { + assert.deepStrictEqual(Array.from(record), [{ a: 10, b: 11n }]); + assert.deepStrictEqual(Array.from(f32), [100]); + assert.deepStrictEqual(Array.from(f64), [101]); + assert.deepStrictEqual(string, ['foo']); + assert.deepStrictEqual(list, [new Uint8Array([102])]); + }, + */ }, - unalignedRoundtrip2(record, f32, f64, string, list) { - assert.deepStrictEqual(Array.from(record), [{ a: 10, b: 11n }]); - assert.deepStrictEqual(Array.from(f32), [100]); - assert.deepStrictEqual(Array.from(f64), [101]); - assert.deepStrictEqual(string, ['foo']); - assert.deepStrictEqual(list, [new Uint8Array([102])]); - }, - */ - - }; - let instance: WebAssembly.Instance; - addImportsToImports(importObj, imports); - const wasi = addWasiToImports(importObj); - - const wasm = new Exports(); - await wasm.instantiate(getWasm(), importObj); - wasi.start(wasm.instance); - instance = wasm.instance; + }); assert.throws(() => wasm.invalidBool(), /invalid variant discriminant for bool/); assert.throws(() => wasm.invalidU8(), /must be between/); diff --git a/tests/runtime/js_instantiate/exports.wit b/tests/runtime/js_instantiate/exports.wit deleted file mode 100644 index acf082171..000000000 --- a/tests/runtime/js_instantiate/exports.wit +++ /dev/null @@ -1 +0,0 @@ -nop: func() diff --git a/tests/runtime/js_instantiate/host.ts b/tests/runtime/js_instantiate/host.ts deleted file mode 100644 index e3cca6341..000000000 --- a/tests/runtime/js_instantiate/host.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { Exports } from "./exports.js"; -import { getWasm } from "./helpers.js"; - -async function run() { - const importObj = {}; - const wasm = new Exports(); - await wasm.instantiate(getWasm(), importObj); - - // test other methods of creating a wasm wrapper - (new Exports()).instantiate(getWasm().buffer, importObj); - (new Exports()).instantiate(new Uint8Array(getWasm()), importObj); - (new Exports()).instantiate(new WebAssembly.Module(getWasm()), importObj); - { - const obj = new Exports(); - obj.instantiate(new WebAssembly.Instance(new WebAssembly.Module(getWasm()), importObj)); - } -} - -await run() diff --git a/tests/runtime/js_instantiate/imports.wit b/tests/runtime/js_instantiate/imports.wit deleted file mode 100644 index e69de29bb..000000000 diff --git a/tests/runtime/js_instantiate/wasm.c b/tests/runtime/js_instantiate/wasm.c deleted file mode 100644 index 47641461e..000000000 --- a/tests/runtime/js_instantiate/wasm.c +++ /dev/null @@ -1,3 +0,0 @@ -#include - -void exports_nop() {} diff --git a/tests/runtime/lists/host.ts b/tests/runtime/lists/host.ts index 06b8aa6ac..aea83f391 100644 --- a/tests/runtime/lists/host.ts +++ b/tests/runtime/lists/host.ts @@ -1,111 +1,104 @@ -import { addImportsToImports, Imports } from "./imports.js"; -import { Exports } from "./exports.js"; -import * as exports from "./exports.js"; -import { getWasm, addWasiToImports } from "./helpers.js"; +import { loadWasm, testwasi } from "./helpers.js"; +import { instantiate } from "./lists.js"; + // @ts-ignore import * as assert from 'assert'; async function run() { - const importObj = {}; - const imports: Imports = { - emptyListParam(a) { - assert.deepStrictEqual(Array.from(a), []); - }, - emptyStringParam(a) { - assert.strictEqual(a, ''); - }, - emptyListResult() { - return new Uint8Array([]); - }, - emptyStringResult() { return ''; }, - listParam(a) { - assert.deepStrictEqual(Array.from(a), [1, 2, 3, 4]); - }, - listParam2(a) { - assert.strictEqual(a, 'foo'); - }, - listParam3(a) { - assert.deepStrictEqual(a, ['foo', 'bar', 'baz']); - }, - listParam4(a) { - assert.deepStrictEqual(a, [['foo', 'bar'], ['baz']]); - }, - listResult() { - return new Uint8Array([1, 2, 3, 4, 5]); - }, - listResult2() { return 'hello!'; }, - listResult3() { return ['hello,', 'world!']; }, - listRoundtrip(x) { return x; }, - stringRoundtrip(x) { return x; }, - - listMinmax8(u, s) { - assert.deepEqual(u.length, 2); - assert.deepEqual(u[0], 0); - assert.deepEqual(u[1], (1 << 8) - 1); - assert.deepEqual(s.length, 2); - assert.deepEqual(s[0], -(1 << 7)); - assert.deepEqual(s[1], (1 << 7) - 1); - - return [u, s]; - }, - - listMinmax16(u, s) { - assert.deepEqual(u.length, 2); - assert.deepEqual(u[0], 0); - assert.deepEqual(u[1], (1 << 16) - 1); - assert.deepEqual(s.length, 2); - assert.deepEqual(s[0], -(1 << 15)); - assert.deepEqual(s[1], (1 << 15) - 1); - - return [u, s]; - }, - - listMinmax32(u, s) { - assert.deepEqual(u.length, 2); - assert.deepEqual(u[0], 0); - assert.deepEqual(u[1], ~0 >>> 0); - assert.deepEqual(s.length, 2); - assert.deepEqual(s[0], 1 << 31); - assert.deepEqual(s[1], ((1 << 31) - 1) >>> 0); - - return [u, s]; - }, - - listMinmax64(u, s) { - assert.deepEqual(u.length, 2); - assert.deepEqual(u[0], 0n); - assert.deepEqual(u[1], (2n ** 64n) - 1n); - assert.deepEqual(s.length, 2); - assert.deepEqual(s[0], -(2n ** 63n)); - assert.deepEqual(s[1], (2n ** 63n) - 1n); - - return [u, s]; - }, - - listMinmaxFloat(f, d) { - assert.deepEqual(f.length, 4); - assert.deepEqual(f[0], -3.4028234663852886e+38); - assert.deepEqual(f[1], 3.4028234663852886e+38); - assert.deepEqual(f[2], Number.NEGATIVE_INFINITY); - assert.deepEqual(f[3], Number.POSITIVE_INFINITY); - - assert.deepEqual(d.length, 4); - assert.deepEqual(d[0], -Number.MAX_VALUE); - assert.deepEqual(d[1], Number.MAX_VALUE); - assert.deepEqual(d[2], Number.NEGATIVE_INFINITY); - assert.deepEqual(d[3], Number.POSITIVE_INFINITY); - - return [f, d]; + const wasm = await instantiate(loadWasm, { + testwasi, + imports: { + emptyListParam(a) { + assert.deepStrictEqual(Array.from(a), []); + }, + emptyStringParam(a) { + assert.strictEqual(a, ''); + }, + emptyListResult() { + return new Uint8Array([]); + }, + emptyStringResult() { return ''; }, + listParam(a) { + assert.deepStrictEqual(Array.from(a), [1, 2, 3, 4]); + }, + listParam2(a) { + assert.strictEqual(a, 'foo'); + }, + listParam3(a) { + assert.deepStrictEqual(a, ['foo', 'bar', 'baz']); + }, + listParam4(a) { + assert.deepStrictEqual(a, [['foo', 'bar'], ['baz']]); + }, + listResult() { + return new Uint8Array([1, 2, 3, 4, 5]); + }, + listResult2() { return 'hello!'; }, + listResult3() { return ['hello,', 'world!']; }, + listRoundtrip(x) { return x; }, + stringRoundtrip(x) { return x; }, + + listMinmax8(u, s) { + assert.deepEqual(u.length, 2); + assert.deepEqual(u[0], 0); + assert.deepEqual(u[1], (1 << 8) - 1); + assert.deepEqual(s.length, 2); + assert.deepEqual(s[0], -(1 << 7)); + assert.deepEqual(s[1], (1 << 7) - 1); + + return [u, s]; + }, + + listMinmax16(u, s) { + assert.deepEqual(u.length, 2); + assert.deepEqual(u[0], 0); + assert.deepEqual(u[1], (1 << 16) - 1); + assert.deepEqual(s.length, 2); + assert.deepEqual(s[0], -(1 << 15)); + assert.deepEqual(s[1], (1 << 15) - 1); + + return [u, s]; + }, + + listMinmax32(u, s) { + assert.deepEqual(u.length, 2); + assert.deepEqual(u[0], 0); + assert.deepEqual(u[1], ~0 >>> 0); + assert.deepEqual(s.length, 2); + assert.deepEqual(s[0], 1 << 31); + assert.deepEqual(s[1], ((1 << 31) - 1) >>> 0); + + return [u, s]; + }, + + listMinmax64(u, s) { + assert.deepEqual(u.length, 2); + assert.deepEqual(u[0], 0n); + assert.deepEqual(u[1], (2n ** 64n) - 1n); + assert.deepEqual(s.length, 2); + assert.deepEqual(s[0], -(2n ** 63n)); + assert.deepEqual(s[1], (2n ** 63n) - 1n); + + return [u, s]; + }, + + listMinmaxFloat(f, d) { + assert.deepEqual(f.length, 4); + assert.deepEqual(f[0], -3.4028234663852886e+38); + assert.deepEqual(f[1], 3.4028234663852886e+38); + assert.deepEqual(f[2], Number.NEGATIVE_INFINITY); + assert.deepEqual(f[3], Number.POSITIVE_INFINITY); + + assert.deepEqual(d.length, 4); + assert.deepEqual(d[0], -Number.MAX_VALUE); + assert.deepEqual(d[1], Number.MAX_VALUE); + assert.deepEqual(d[2], Number.NEGATIVE_INFINITY); + assert.deepEqual(d[3], Number.POSITIVE_INFINITY); + + return [f, d]; + }, }, - }; - let instance: WebAssembly.Instance; - addImportsToImports(importObj, imports, name => instance.exports[name]); - const wasi = addWasiToImports(importObj); - - const wasm = new Exports(); - await wasm.instantiate(getWasm(), importObj); - wasi.start(wasm.instance); - instance = wasm.instance; + }); const bytes = wasm.allocatedBytes(); wasm.testImports(); diff --git a/tests/runtime/lists/wasm.c b/tests/runtime/lists/wasm.c index 58ae2166b..77cd6e5f9 100644 --- a/tests/runtime/lists/wasm.c +++ b/tests/runtime/lists/wasm.c @@ -106,6 +106,30 @@ void exports_test_imports() { imports_list_string_free(&a); } + { + imports_list_u8_t a, b; + a.len = 0; + a.ptr = (unsigned char*) ""; + imports_list_roundtrip(&a, &b); + assert(b.len == a.len); + assert(memcmp(b.ptr, a.ptr, a.len) == 0); + imports_list_u8_free(&b); + + a.len = 1; + a.ptr = (unsigned char*) "x"; + imports_list_roundtrip(&a, &b); + assert(b.len == a.len); + assert(memcmp(b.ptr, a.ptr, a.len) == 0); + imports_list_u8_free(&b); + + a.len = 5; + a.ptr = (unsigned char*) "hello"; + imports_list_roundtrip(&a, &b); + assert(b.len == a.len); + assert(memcmp(b.ptr, a.ptr, a.len) == 0); + imports_list_u8_free(&b); + } + { imports_string_t a, b; imports_string_set(&a, "x"); diff --git a/tests/runtime/lists/wasm.rs b/tests/runtime/lists/wasm.rs index 0cd10e2fe..91d2c4947 100644 --- a/tests/runtime/lists/wasm.rs +++ b/tests/runtime/lists/wasm.rs @@ -26,6 +26,10 @@ impl exports::Exports for Exports { assert_eq!(list_result2(), "hello!"); assert_eq!(list_result3(), ["hello,", "world!"]); + assert_eq!(list_roundtrip(&[]), []); + assert_eq!(list_roundtrip(b"x"), b"x"); + assert_eq!(list_roundtrip(b"hello"), b"hello"); + assert_eq!(string_roundtrip("x"), "x"); assert_eq!(string_roundtrip(""), ""); assert_eq!(string_roundtrip("hello"), "hello"); diff --git a/tests/runtime/many_arguments/host.ts b/tests/runtime/many_arguments/host.ts index c895a9e9f..aa321fbac 100644 --- a/tests/runtime/many_arguments/host.ts +++ b/tests/runtime/many_arguments/host.ts @@ -1,6 +1,5 @@ -import { addImportsToImports, Imports } from "./imports.js"; -import { Exports } from "./exports.js"; -import { getWasm, addWasiToImports } from "./helpers.js"; +import { instantiate } from "./many_arguments.js"; +import { loadWasm, testwasi } from "./helpers.js"; function assertEq(x: any, y: any) { if (x !== y) @@ -13,52 +12,46 @@ function assert(x: boolean) { } async function run() { - const importObj = {}; - const imports: Imports = { - manyArguments( - a1, - a2, - a3, - a4, - a5, - a6, - a7, - a8, - a9, - a10, - a11, - a12, - a13, - a14, - a15, - a16, - ) { - assertEq(a1, 1n); - assertEq(a2, 2n); - assertEq(a3, 3n); - assertEq(a4, 4n); - assertEq(a5, 5n); - assertEq(a6, 6n); - assertEq(a7, 7n); - assertEq(a8, 8n); - assertEq(a9, 9n); - assertEq(a10, 10n); - assertEq(a11, 11n); - assertEq(a12, 12n); - assertEq(a13, 13n); - assertEq(a14, 14n); - assertEq(a15, 15n); - assertEq(a16, 16n); + const wasm = await instantiate(loadWasm, { + testwasi, + imports: { + manyArguments( + a1, + a2, + a3, + a4, + a5, + a6, + a7, + a8, + a9, + a10, + a11, + a12, + a13, + a14, + a15, + a16, + ) { + assertEq(a1, 1n); + assertEq(a2, 2n); + assertEq(a3, 3n); + assertEq(a4, 4n); + assertEq(a5, 5n); + assertEq(a6, 6n); + assertEq(a7, 7n); + assertEq(a8, 8n); + assertEq(a9, 9n); + assertEq(a10, 10n); + assertEq(a11, 11n); + assertEq(a12, 12n); + assertEq(a13, 13n); + assertEq(a14, 14n); + assertEq(a15, 15n); + assertEq(a16, 16n); + }, }, - }; - let instance: WebAssembly.Instance; - addImportsToImports(importObj, imports); - const wasi = addWasiToImports(importObj); - - const wasm = new Exports(); - await wasm.instantiate(getWasm(), importObj); - wasi.start(wasm.instance); - instance = wasm.instance; + }); wasm.manyArguments( 1n, diff --git a/tests/runtime/numbers/host.ts b/tests/runtime/numbers/host.ts index 2913de2b6..7267057e4 100644 --- a/tests/runtime/numbers/host.ts +++ b/tests/runtime/numbers/host.ts @@ -1,6 +1,5 @@ -import { addImportsToImports, Imports } from "./imports.js"; -import { Exports } from "./exports.js"; -import { getWasm, addWasiToImports } from "./helpers.js"; +import { loadWasm, testwasi } from "./helpers.js"; +import { instantiate } from "./numbers.js"; function assertEq(x: any, y: any) { if (x !== y) @@ -13,28 +12,25 @@ function assert(x: boolean) { } async function run() { - const importObj = {}; let scalar = 0; - addImportsToImports(importObj, { - roundtripU8(x) { return x; }, - roundtripS8(x) { return x; }, - roundtripU16(x) { return x; }, - roundtripS16(x) { return x; }, - roundtripU32(x) { return x; }, - roundtripS32(x) { return x; }, - roundtripU64(x) { return x; }, - roundtripS64(x) { return x; }, - roundtripFloat32(x) { return x; }, - roundtripFloat64(x) { return x; }, - roundtripChar(x) { return x; }, - setScalar(x) { scalar = x; }, - getScalar() { return scalar; }, + const wasm = await instantiate(loadWasm, { + testwasi, + imports: { + roundtripU8(x) { return x; }, + roundtripS8(x) { return x; }, + roundtripU16(x) { return x; }, + roundtripS16(x) { return x; }, + roundtripU32(x) { return x; }, + roundtripS32(x) { return x; }, + roundtripU64(x) { return x; }, + roundtripS64(x) { return x; }, + roundtripFloat32(x) { return x; }, + roundtripFloat64(x) { return x; }, + roundtripChar(x) { return x; }, + setScalar(x) { scalar = x; }, + getScalar() { return scalar; }, + }, }); - const wasi = addWasiToImports(importObj); - - const wasm = new Exports(); - await wasm.instantiate(getWasm(), importObj); - wasi.start(wasm.instance); wasm.testImports(); diff --git a/tests/runtime/records/host.ts b/tests/runtime/records/host.ts index 3f0670a0d..49db46dfe 100644 --- a/tests/runtime/records/host.ts +++ b/tests/runtime/records/host.ts @@ -1,30 +1,22 @@ -import { addImportsToImports, Imports } from "./imports.js"; -import { Exports } from "./exports.js"; -import * as exports from "./exports.js"; -import { getWasm, addWasiToImports } from "./helpers.js"; +import { loadWasm, testwasi } from "./helpers.js"; +import { instantiate, ImportObject } from "./records.js"; // @ts-ignore -import * as assert from 'assert'; +import * as assert from 'node:assert'; async function run() { - const importObj = {}; - const imports: Imports = { - multipleResults() { return [4, 5]; }, - swapTuple([a, b]) { return [b, a]; }, - roundtripFlags1(x) { return x; }, - roundtripFlags2(x) { return x; }, - roundtripFlags3(r0, r1, r2, r3) { return [r0, r1, r2, r3]; }, - roundtripRecord1(x) { return x; }, - tuple0([]) { return []; }, - tuple1([x]) { return [x]; }, - }; - let instance: WebAssembly.Instance; - addImportsToImports(importObj, imports, name => instance.exports[name]); - const wasi = addWasiToImports(importObj); - - const wasm = new Exports(); - await wasm.instantiate(getWasm(), importObj); - wasi.start(wasm.instance); - instance = wasm.instance; + const wasm = await instantiate(loadWasm, { + testwasi, + imports: { + multipleResults() { return [4, 5]; }, + swapTuple([a, b]) { return [b, a]; }, + roundtripFlags1(x) { return x; }, + roundtripFlags2(x) { return x; }, + roundtripFlags3(r0, r1, r2, r3) { return [r0, r1, r2, r3]; }, + roundtripRecord1(x) { return x; }, + tuple0([]) { return []; }, + tuple1([x]) { return [x]; }, + }, + }); wasm.testImports(); assert.deepEqual(wasm.multipleResults(), [100, 200]); diff --git a/tests/runtime/smoke/host.ts b/tests/runtime/smoke/host.ts index 0cdb09a93..ae4d322fc 100644 --- a/tests/runtime/smoke/host.ts +++ b/tests/runtime/smoke/host.ts @@ -1,6 +1,5 @@ -import { addImportsToImports, Imports } from "./imports.js"; -import { Exports } from "./exports.js"; -import { getWasm, addWasiToImports } from "./helpers.js"; +import { loadWasm, testwasi } from "./helpers.js"; +import { instantiate } from "./smoke.js"; function assert(x: boolean, msg: string) { if (!x) @@ -8,18 +7,16 @@ function assert(x: boolean, msg: string) { } async function run() { - const importObj = {}; let hit = false; - addImportsToImports(importObj, { - thunk() { - hit = true; - } - }); - const wasi = addWasiToImports(importObj); - const wasm = new Exports(); - await wasm.instantiate(getWasm(), importObj); - wasi.start(wasm.instance); + const wasm = await instantiate(loadWasm, { + testwasi, + imports: { + thunk() { + hit = true; + }, + }, + }); wasm.thunk(); assert(hit, "import not called"); diff --git a/tests/runtime/smoke/wasm.c b/tests/runtime/smoke/wasm.c index fd6927b66..0ca7b9e3a 100644 --- a/tests/runtime/smoke/wasm.c +++ b/tests/runtime/smoke/wasm.c @@ -1,6 +1,9 @@ #include #include +#include void exports_thunk() { imports_thunk(); + + printf("howdy\n"); } diff --git a/tests/runtime/variants/host.ts b/tests/runtime/variants/host.ts index 1948d008d..5d22879f6 100644 --- a/tests/runtime/variants/host.ts +++ b/tests/runtime/variants/host.ts @@ -1,45 +1,37 @@ -import { addImportsToImports, Imports, MyErrno } from "./imports.js"; -import { Exports } from "./exports.js"; -import * as exports from "./exports.js"; -import { getWasm, addWasiToImports } from "./helpers.js"; +import { loadWasm, testwasi } from "./helpers.js"; +import { instantiate } from "./variants.js"; // @ts-ignore import * as assert from 'assert'; async function run() { - const importObj = {}; - const imports: Imports = { - roundtripOption(x) { return x; }, - roundtripResult(x) { - if (x.tag == 'ok') { - return { tag: 'ok', val: x.val }; - } else { - return { tag: 'err', val: Math.round(x.val) }; - } + const wasm = await instantiate(loadWasm, { + testwasi, + imports: { + roundtripOption(x) { return x; }, + roundtripResult(x) { + if (x.tag == 'ok') { + return { tag: 'ok', val: x.val }; + } else { + return { tag: 'err', val: Math.round(x.val) }; + } + }, + roundtripEnum(x) { return x; }, + invertBool(x) { return !x; }, + variantCasts(x) { return x; }, + variantZeros(x) { return x; }, + variantTypedefs(x, y, z) {}, + variantEnums(a, b, c) { + assert.deepStrictEqual(a, true); + assert.deepStrictEqual(b, { tag: 'ok', val: undefined }); + assert.deepStrictEqual(c, "success"); + return [ + false, + { tag: 'err', val: undefined }, + "a", + ]; + }, }, - roundtripEnum(x) { return x; }, - invertBool(x) { return !x; }, - variantCasts(x) { return x; }, - variantZeros(x) { return x; }, - variantTypedefs(x, y, z) {}, - variantEnums(a, b, c) { - assert.deepStrictEqual(a, true); - assert.deepStrictEqual(b, { tag: 'ok', val: undefined }); - assert.deepStrictEqual(c, "success"); - return [ - false, - { tag: 'err', val: undefined }, - "a", - ]; - }, - }; - let instance: WebAssembly.Instance; - addImportsToImports(importObj, imports, name => instance.exports[name]); - const wasi = addWasiToImports(importObj); - - const wasm = new Exports(); - await wasm.instantiate(getWasm(), importObj); - wasi.start(wasm.instance); - instance = wasm.instance; + }); wasm.testImports(); assert.deepStrictEqual(wasm.roundtripOption(1), 1);