From 9aad0957dfe5c41acc09acab30fab877d1bcf7a3 Mon Sep 17 00:00:00 2001 From: Tomasz Andrzejak Date: Tue, 15 Sep 2026 21:27:17 +0200 Subject: [PATCH] deps: bump js-component-bindgen crate This updates component bindgen to v2. The newer call generation path relies on host-side task setup. Our generated bindings run inside the SpiderMonkey wasm guest, where the native embedding already manages execution and call lifetimes - that is why this patch introduces embedding adapter for handling calls, returns and borrows locally. --- .github/workflows/main.yml | 13 +- Cargo.lock | 238 +++++++++++++----- Cargo.toml | 9 +- .../src/bindgen.rs | 15 +- embedding/embedding.cpp | 47 +++- embedding/embedding.h | 1 + test/cases/resource-borrow-import/source.js | 7 + test/cases/resource-borrow-import/test.js | 5 +- test/cases/resource-borrow-import/world.wit | 1 + test/cases/resources/imports.js | 3 +- test/cases/resources/source.js | 4 +- test/export-buffers.js | 114 +++++++++ test/import-return-buffers.js | 61 +++++ 13 files changed, 441 insertions(+), 77 deletions(-) create mode 100644 test/export-buffers.js create mode 100644 test/import-return-buffers.js diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index bd7b5c03..94b0812a 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -108,9 +108,10 @@ jobs: with: submodules: recursive - - name: Get StarlingMonkey Commit + - name: Get native engine source hash id: starlingmonkey-commit - run: echo "STARLINGMONKEY_HASH=$(git submodule status | head -c9 | tail -c8)" >> "$GITHUB_OUTPUT" + # Include the embedding; Git objects avoid checkout line-ending differences. + run: echo "STARLINGMONKEY_HASH=$(git ls-tree -z HEAD -- StarlingMonkey embedding | git hash-object --stdin)" >> "$GITHUB_OUTPUT" - uses: actions/cache@v4 id: starlingmonkey-build @@ -178,9 +179,9 @@ jobs: steps: - uses: actions/checkout@v4 - - name: Get StarlingMonkey Commit + - name: Get native engine source hash id: starlingmonkey-commit - run: echo "STARLINGMONKEY_HASH=$(git submodule status | head -c9 | tail -c8)" >> "$GITHUB_OUTPUT" + run: echo "STARLINGMONKEY_HASH=$(git ls-tree -z HEAD -- StarlingMonkey embedding | git hash-object --stdin)" >> "$GITHUB_OUTPUT" - name: Restore StarlingMonkey build from cache uses: actions/cache/restore@v4 @@ -223,9 +224,9 @@ jobs: steps: - uses: actions/checkout@v4 - - name: Get StarlingMonkey Commit + - name: Get native engine source hash id: starlingmonkey-commit - run: echo "STARLINGMONKEY_HASH=$(git submodule status | head -c9 | tail -c8)" >> "$GITHUB_OUTPUT" + run: echo "STARLINGMONKEY_HASH=$(git ls-tree -z HEAD -- StarlingMonkey embedding | git hash-object --stdin)" >> "$GITHUB_OUTPUT" - name: Restore StarlingMonkey build from cache uses: actions/cache/restore@v4 diff --git a/Cargo.lock b/Cargo.lock index b32af0ed..26610fe8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -54,9 +54,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.102" +version = "1.0.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" [[package]] name = "base64" @@ -106,9 +106,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.17.0" +version = "3.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" [[package]] name = "cfg-if" @@ -171,6 +171,15 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" +[[package]] +name = "cpp_demangle" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0667304c32ea56cb4cd6d2d7c0cfe9a2f8041229db8c033af7f8d69492429def" +dependencies = [ + "cfg-if", +] + [[package]] name = "cpufeatures" version = "0.2.17" @@ -182,9 +191,9 @@ dependencies = [ [[package]] name = "cranelift-bforest" -version = "0.130.2" +version = "0.135.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b5996f01a686b2349cdb379083ec5ad3e8cb8767fb2d495d3a4f2ee4163a18d" +checksum = "547433a3f4204f0ed0f5a5e3ffaf086a0c005fd0efdf040098083f38572e8a49" dependencies = [ "cranelift-entity", "wasmtime-internal-core", @@ -192,9 +201,9 @@ dependencies = [ [[package]] name = "cranelift-bitset" -version = "0.130.2" +version = "0.135.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "523fea83273f6a985520f57788809a4de2165794d9ab00fb1254fceb4f5aa00c" +checksum = "61ca4f30f5f2eb3837b6e73664d9c0c9a42c4c0dce05f0027f2f2af54c1b20d7" dependencies = [ "serde", "serde_derive", @@ -203,9 +212,9 @@ dependencies = [ [[package]] name = "cranelift-entity" -version = "0.130.2" +version = "0.135.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6038b3147c7982f4951150d5f96c7c06c1e7214b99d4b4a98607aadf8ded89d1" +checksum = "bc2c3088c6be94765abea42eccff7ac2b6519488e66bb5159febabcf539afd6e" dependencies = [ "cranelift-bitset", "serde", @@ -393,9 +402,7 @@ checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" [[package]] name = "js-component-bindgen" -version = "1.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7349a16becdbbf9f4ba40666e267a283c1e5347fc5d24bc47f07cdbceb7cbac" +version = "2.10.1" dependencies = [ "anyhow", "base64", @@ -403,12 +410,12 @@ dependencies = [ "heck", "log", "semver", - "wasm-encoder 0.245.1", - "wasmparser 0.245.1", + "wasm-encoder 0.258.0", + "wasmparser 0.258.0", "wasmtime-environ", - "wit-bindgen-core", - "wit-component", - "wit-parser", + "wit-bindgen-core 0.61.1", + "wit-component 0.258.0", + "wit-parser 0.258.0", ] [[package]] @@ -443,12 +450,12 @@ checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" [[package]] name = "object" -version = "0.38.1" +version = "0.39.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "271638cd5fa9cca89c4c304675ca658efc4e64a66c716b7cfe1afb4b9611dbbc" +checksum = "2e5a6c098c7a3b6547378093f5cc30bc54fd361ce711e05293a5cc589562739b" dependencies = [ "crc32fast", - "hashbrown 0.16.1", + "hashbrown 0.17.1", "indexmap", "memchr", ] @@ -520,6 +527,12 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +[[package]] +name = "rustc-demangle" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b74b56ffa8bb2830709a538c2cbcae9aa062db0d2a42563bfb09bdaae44020eb" + [[package]] name = "rustversion" version = "1.0.22" @@ -618,9 +631,9 @@ dependencies = [ "wasmparser 0.245.1", "wirm", "wit-bindgen", - "wit-bindgen-core", - "wit-component", - "wit-parser", + "wit-bindgen-core 0.61.1", + "wit-component 0.258.0", + "wit-parser 0.258.0", ] [[package]] @@ -649,9 +662,9 @@ dependencies = [ [[package]] name = "target-lexicon" -version = "0.13.2" +version = "0.13.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e502f78cdbb8ba4718f566c418c52bc729126ffd16baee5baa718cf25dd5a69a" +checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca" [[package]] name = "termcolor" @@ -690,9 +703,9 @@ checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" [[package]] name = "unicode-ident" -version = "1.0.18" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] name = "unicode-width" @@ -740,12 +753,32 @@ dependencies = [ [[package]] name = "wasm-encoder" -version = "0.248.0" +version = "0.254.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09480d646178e5fdd12bb06e812d0af9a3a191dbc9cd697fdc86687beade7393" +dependencies = [ + "leb128fmt", + "wasmparser 0.254.0", +] + +[[package]] +name = "wasm-encoder" +version = "0.258.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e974fe6821a8cf64575d51ea2194e2c8f77e7b66e9afe7419ce8a97f9ee0d251" +dependencies = [ + "leb128fmt", + "wasmparser 0.258.0", +] + +[[package]] +name = "wasm-encoder" +version = "0.259.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac92cf547bc18d27ecc521015c08c353b4f18b84ab388bb6d1b6b682c620d9b6" +checksum = "b1d0246511d901aacf25d2dc9111f0054947de5e093fe662099e709ff7530dc3" dependencies = [ "leb128fmt", - "wasmparser 0.248.0", + "wasmparser 0.259.0", ] [[package]] @@ -760,6 +793,18 @@ dependencies = [ "wasmparser 0.245.1", ] +[[package]] +name = "wasm-metadata" +version = "0.258.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18a11585adb92fe9b55ad1d760e8d8fb5d87e0d2e303cb8eed57f078d54293a2" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder 0.258.0", + "wasmparser 0.258.0", +] + [[package]] name = "wasmparser" version = "0.245.1" @@ -788,9 +833,34 @@ dependencies = [ [[package]] name = "wasmparser" -version = "0.248.0" +version = "0.254.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5769a29f799fbab136aaf65b4fe5384cd7d93fe6fc9ba0dcb6c8382a1f16e27" +dependencies = [ + "bitflags", + "hashbrown 0.17.1", + "indexmap", + "semver", + "serde", +] + +[[package]] +name = "wasmparser" +version = "0.258.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa4439c5eee9df71ee0c6efb37f63b1fcb1fec38f85f5142c54e7ed05d33091a" +checksum = "d9a61719f93a87b16d325921e251800c4833f8fab50fa21c7de73aed50086313" +dependencies = [ + "bitflags", + "hashbrown 0.17.1", + "indexmap", + "semver", +] + +[[package]] +name = "wasmparser" +version = "0.259.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f7c12eac7bb587801590f6a67ff0bc84d0748513864b71108e4b311cc3df694" dependencies = [ "bitflags", "indexmap", @@ -798,39 +868,41 @@ dependencies = [ [[package]] name = "wasmprinter" -version = "0.245.1" +version = "0.254.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f41517a3716fbb8ccf46daa9c1325f760fcbff5168e75c7392288e410b91ac8" +checksum = "64e3ba11e024f504698f87b629b2b66c22a1231758de7607754963f7d198be39" dependencies = [ "anyhow", "termcolor", - "wasmparser 0.245.1", + "wasmparser 0.254.0", ] [[package]] name = "wasmtime-environ" -version = "43.0.2" +version = "48.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4172382dcc785c31d0e862c6780a18f5dd437914d22c4691351f965ef751c821" +checksum = "13f10ef166b4850535bc43c347797364cda5189870be163a617ebe5c0d72f139" dependencies = [ "anyhow", + "cpp_demangle", "cranelift-bforest", "cranelift-bitset", "cranelift-entity", "gimli", - "hashbrown 0.16.1", + "hashbrown 0.17.1", "indexmap", "log", "object", "postcard", + "rustc-demangle", "semver", "serde", "serde_derive", "sha2", "smallvec", "target-lexicon", - "wasm-encoder 0.245.1", - "wasmparser 0.245.1", + "wasm-encoder 0.254.0", + "wasmparser 0.254.0", "wasmprinter", "wasmtime-internal-component-util", "wasmtime-internal-core", @@ -838,39 +910,40 @@ dependencies = [ [[package]] name = "wasmtime-internal-component-util" -version = "43.0.2" +version = "48.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "935d9ab293ba27d1ec9aa7bc1b3a43993dbe961af2a8f23f90a11e1331b4c13f" +checksum = "540b7151342d75c3d596447c136d3862a280600bc6fd10b5dfc7c9e67c2f719f" [[package]] name = "wasmtime-internal-core" -version = "43.0.2" +version = "48.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a3820b174f477d2a7083209d1ad5353fcdb11eaea434b2137b8681029460dd3" +checksum = "8679b612a408274d91d1e0edf6baa36b63cc48d937f84dd1e7fb2fbe16a907ae" dependencies = [ - "hashbrown 0.16.1", + "anyhow", + "hashbrown 0.17.1", "libm", "serde", ] [[package]] name = "wast" -version = "248.0.0" +version = "259.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acc54622ed5a5cddafcdf152043f9d4aed54d4a653d686b7dfe874809fca99d7" +checksum = "c69beba8d9da07af9a0971b559149a2ac4729895b55144de4fddbf5a02660648" dependencies = [ "bumpalo", "leb128fmt", "memchr", "unicode-width", - "wasm-encoder 0.248.0", + "wasm-encoder 0.259.0", ] [[package]] name = "wat" -version = "1.248.0" +version = "1.259.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d75cd9e510603909748e6ebab89f27cd04472c1d9d85a3c88a7a6fc51a1a7934" +checksum = "c6eec44b0c80391b20fb7ad9ea440c72c5d382385bdf74444bb18321c425017e" dependencies = [ "wast", ] @@ -987,7 +1060,18 @@ checksum = "99cdef5ccf0b0e9bf30868d6f9c5ed116c84ae95f84ba29d2216d3e922de3963" dependencies = [ "anyhow", "heck", - "wit-parser", + "wit-parser 0.245.1", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.61.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87445680dfe6d6b5369e884bd63c03a3335268e855365b2fc3f7bcdce96a630e" +dependencies = [ + "anyhow", + "heck", + "wit-parser 0.258.0", ] [[package]] @@ -1001,9 +1085,9 @@ dependencies = [ "indexmap", "prettyplease", "syn", - "wasm-metadata", - "wit-bindgen-core", - "wit-component", + "wasm-metadata 0.245.1", + "wit-bindgen-core 0.54.0", + "wit-component 0.245.1", ] [[package]] @@ -1017,7 +1101,7 @@ dependencies = [ "proc-macro2", "quote", "syn", - "wit-bindgen-core", + "wit-bindgen-core 0.54.0", "wit-bindgen-rust", ] @@ -1035,10 +1119,29 @@ dependencies = [ "serde_derive", "serde_json", "wasm-encoder 0.245.1", - "wasm-metadata", + "wasm-metadata 0.245.1", "wasmparser 0.245.1", + "wit-parser 0.245.1", +] + +[[package]] +name = "wit-component" +version = "0.258.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "481b5c47b2ecce0389b5e08a05557d6a190c9cd761773b8880a8017ee04dc7ef" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder 0.258.0", + "wasm-metadata 0.258.0", + "wasmparser 0.258.0", "wat", - "wit-parser", + "wit-parser 0.258.0", ] [[package]] @@ -1059,3 +1162,22 @@ dependencies = [ "unicode-xid", "wasmparser 0.245.1", ] + +[[package]] +name = "wit-parser" +version = "0.258.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff4daaa3cd97ae49ecd0a99dc009d453f93e0f083dd3be38c0f24a83a93e37ac" +dependencies = [ + "anyhow", + "hashbrown 0.17.1", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-ident", + "wasmparser 0.258.0", +] diff --git a/Cargo.toml b/Cargo.toml index 5ea08336..26f516a9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,11 +27,10 @@ wasmparser = { version = "0.245.1", features = [ "std", "validate", ] } -wit-component = { version = "0.245.1", features = ["dummy-module"] } -wit-parser = { version = "0.245.1", features = ["decoding", "serde"] } +wit-component = { version = "0.258.0", features = ["dummy-module"] } +wit-parser = { version = "0.258.0", features = ["decoding", "serde"] } wit-bindgen = { version = "0.54.0", features = ["macros", "async", "realloc"] } -wit-bindgen-core = { version = "0.54.0", default-features = false } - -js-component-bindgen = { version = "1.19.0" } +wit-bindgen-core = { version = "0.61.1", default-features = false } +js-component-bindgen = { version = "2.11.0" } diff --git a/crates/spidermonkey-embedding-splicer/src/bindgen.rs b/crates/spidermonkey-embedding-splicer/src/bindgen.rs index e909ee33..36be7fdc 100644 --- a/crates/spidermonkey-embedding-splicer/src/bindgen.rs +++ b/crates/spidermonkey-embedding-splicer/src/bindgen.rs @@ -104,6 +104,7 @@ struct JsBindgen<'a> { sizes: SizeAlign, memory: String, realloc: String, + dealloc: String, // export "name" exports: Vec<(String, BindingItem)>, @@ -158,6 +159,7 @@ pub fn componentize_bindgen( sizes: SizeAlign::default(), memory: "$memory".to_string(), realloc: "$realloc".to_string(), + dealloc: "$dealloc".to_string(), exports: Vec::new(), imports: Vec::new(), resource_directions: HashMap::new(), @@ -370,7 +372,7 @@ pub fn componentize_bindgen( let repCnt = 1; let repTable = new Map(); - let [$memory, $realloc{}] = $bindings; + let [$memory, $realloc, $dealloc{}] = $bindings; delete globalThis.$bindings; {finalization_registries} @@ -908,9 +910,17 @@ impl JsBindgen<'_> { ErrHandling::None }; + // Imports use the guest-export ABI here because the native glue returns + // its allocated result area to JS. Release it only after lifting finishes. + let needs_post_return = + abi == AbiVariant::GuestExport && self.resolve.wasm_signature(abi, func).retptr; let tracing_prefix = String::new(); + // Lifting copies owned export parameters as well as import results. let mut f = FunctionBindgen::builder() + .skip_fn_call_task_management(true) .is_async(false) + .canonical_abi_async(false) + .wrap_async_future_result(false) .tracing_prefix(&tracing_prefix) .intrinsics(&mut self.all_intrinsics) .valid_lifting_optimization(true) @@ -921,6 +931,8 @@ impl JsBindgen<'_> { .callee(callee) .memory(&self.memory) .realloc(&self.realloc) + .dealloc(&self.dealloc) + .maybe_post_return(needs_post_return.then_some(&self.dealloc)) .tmp(0) .params(params) .encoding(match string_encoding { @@ -936,6 +948,7 @@ impl JsBindgen<'_> { .asmjs(false) .requires_async_porcelain(requires_async_porcelain) .tracing_enabled(false) + .no_component_error_wrapping(false) .maybe_iface_name(iface_name.as_deref()) .build(); diff --git a/embedding/embedding.cpp b/embedding/embedding.cpp index d49a2b26..503c273d 100644 --- a/embedding/embedding.cpp +++ b/embedding/embedding.cpp @@ -1,6 +1,7 @@ #include "embedding.h" #include "debugger.h" #include "builtins/web/performance.h" +#include "js/Conversions.h" namespace builtins::web::console { @@ -140,6 +141,16 @@ cabi_realloc_adapter(void *ptr, size_t orig_size, size_t org_align, // see: https://github.com/bytecodealliance/wasmtime/blob/aec935f2e746d71934c8a131be15bbbb4392138c/crates/wasmtime/src/runtime/component/func/host.rs#L741 __attribute__((export_name("cabi_realloc"))) void * cabi_realloc(void *ptr, size_t orig_size, size_t org_align, size_t new_size) { + // Empty canonical buffers have no backing allocation. + if (new_size == 0) { + if (orig_size != 0) { + JS_free(Runtime.cx, ptr); + } + return nullptr; + } + if (orig_size == 0) { + ptr = nullptr; + } void *ret = JS_realloc(Runtime.cx, ptr, orig_size, new_size); if (!ret) { Runtime.engine->abort("(cabi_realloc) Unable to realloc"); @@ -162,6 +173,9 @@ __attribute__((export_name("call"))) uint32_t call(uint32_t fn_idx, Runtime.engine->abort("(call) unexpected call state, post_call was not called after last call"); } Runtime.cur_fn_idx = fn_idx; + if (argptr) { + Runtime.free_list.push_back(argptr); + } ComponentizeRuntime::CoreFn *fn = &Runtime.fns[fn_idx]; if (Runtime.debug) { fprintf(stderr, "(call) Function [%d] - ", fn_idx); @@ -464,11 +478,31 @@ static bool ReallocFn(JSContext *cx, unsigned argc, JS::Value *vp) { size_t old_len = args[1].toInt32(); size_t align = args[2].toInt32(); size_t new_len = args[3].toInt32(); + // This wrapper is only used by generated lowering code. Native JS values + // can retain allocations made through the general cabi_realloc export. + if (old_ptr) { + Runtime.free_list_remove(old_ptr); + } void *ptr = cabi_realloc(old_ptr, old_len, align, new_len); + if (ptr) { + Runtime.free_list.push_back(ptr); + } args.rval().setInt32((uint32_t)ptr); return true; } +static bool DeallocFn(JSContext *cx, unsigned argc, JS::Value *vp) { + JS::CallArgs args = JS::CallArgsFromVp(argc, vp); + uint32_t ptr; + if (!JS::ToUint32(cx, args[0], &ptr)) { + return false; + } + Runtime.free_list_remove((void *)ptr); + cabi_free((void *)ptr); + args.rval().setUndefined(); + return true; +} + void cabi_free(void *ptr) { LOG("(cabi_free) %d", (uint32_t)ptr); JS_free(Runtime.cx, ptr); @@ -537,7 +571,7 @@ bool install(api::Engine *engine) { uint32_t import_cnt = atoi(getenv("IMPORT_CNT")); JS::RootedObject import_bindings( - Runtime.cx, JS::NewArrayObject(Runtime.cx, 2 + import_cnt)); + Runtime.cx, JS::NewArrayObject(Runtime.cx, 3 + import_cnt)); LOG("(wizer) create the memory buffer JS object"); JS::RootedObject mem(Runtime.cx, JS_NewPlainObject(Runtime.cx)); @@ -557,6 +591,15 @@ bool install(api::Engine *engine) { JS::RootedObject function_obj(Runtime.cx, JS_GetFunctionObject(realloc_fn)); JS_SetElement(Runtime.cx, import_bindings, 1, function_obj); + JSFunction *dealloc_fn = JS_NewFunction(Runtime.cx, DeallocFn, 0, 0, "dealloc"); + if (!dealloc_fn) { + return false; + } + function_obj = JS_GetFunctionObject(dealloc_fn); + if (!JS_SetElement(Runtime.cx, import_bindings, 2, function_obj)) { + return false; + } + LOG("(wizer) create the %d import JS functions", import_cnt); for (size_t i = 0; i < import_cnt; i++) { sprintf(&env_name[0], "IMPORT%zu_NAME", i); @@ -569,7 +612,7 @@ bool install(api::Engine *engine) { return false; } JS::RootedObject function_obj(Runtime.cx, JS_GetFunctionObject(import_fn)); - JS_SetElement(Runtime.cx, import_bindings, 2 + i, function_obj); + JS_SetElement(Runtime.cx, import_bindings, 3 + i, function_obj); } LOG("(wizer) setting the binding global"); diff --git a/embedding/embedding.h b/embedding/embedding.h index 11e95d5b..dfd06037 100644 --- a/embedding/embedding.h +++ b/embedding/embedding.h @@ -82,6 +82,7 @@ namespace componentize::embedding // the current export function call int cur_fn_idx = -1; + // Owned call areas and buffers produced by generated JS lowering. std::vector free_list; void free_list_remove(void *ptr) diff --git a/test/cases/resource-borrow-import/source.js b/test/cases/resource-borrow-import/source.js index a32daf89..818ee5b1 100644 --- a/test/cases/resource-borrow-import/source.js +++ b/test/cases/resource-borrow-import/source.js @@ -15,3 +15,10 @@ export function testBorrowEarlyDrop(value) { value[Symbol.dispose](); return result; } + +export async function testBorrowAsync(value, fail) { + await Promise.resolve(); + const result = foo(value) + 10; + if (fail) throw result; + return result; +} diff --git a/test/cases/resource-borrow-import/test.js b/test/cases/resource-borrow-import/test.js index 5e5566ce..50e94f13 100644 --- a/test/cases/resource-borrow-import/test.js +++ b/test/cases/resource-borrow-import/test.js @@ -1,4 +1,4 @@ -import { strictEqual } from 'node:assert' +import { strictEqual, throws } from 'node:assert' import { Thing } from "./resource-borrow-import.js" export function test(instance) { @@ -7,4 +7,7 @@ export function test(instance) { let thing = new Thing(42) strictEqual(instance.testBorrow(thing), 42 + 2 + 3 + 6) strictEqual(instance.testBorrowEarlyDrop(thing), 42 + 2 + 3 + 8) + strictEqual(instance.testBorrowAsync(thing, false), 42 + 2 + 3 + 10) + throws(() => instance.testBorrowAsync(thing, true), error => error.payload === 42 + 2 + 3 + 10) + strictEqual(instance.testBorrowAsync(thing, false), 42 + 2 + 3 + 10) } diff --git a/test/cases/resource-borrow-import/world.wit b/test/cases/resource-borrow-import/world.wit index 2e084ce9..81b97b26 100644 --- a/test/cases/resource-borrow-import/world.wit +++ b/test/cases/resource-borrow-import/world.wit @@ -16,4 +16,5 @@ world test { export test: func(v: u32) -> u32; export test-borrow: func(v: borrow) -> u32; export test-borrow-early-drop: func(v: borrow) -> u32; + export test-borrow-async: func(v: borrow, fail: bool) -> result; } diff --git a/test/cases/resources/imports.js b/test/cases/resources/imports.js index 44e24da9..e387730c 100644 --- a/test/cases/resources/imports.js +++ b/test/cases/resources/imports.js @@ -1,6 +1,6 @@ export class Y { constructor (value) { - this.value = value + this.a = value } getA () { return this.a; @@ -13,4 +13,3 @@ export class Y { return x; } } - diff --git a/test/cases/resources/source.js b/test/cases/resources/source.js index bf82506a..47bf773b 100644 --- a/test/cases/resources/source.js +++ b/test/cases/resources/source.js @@ -41,13 +41,13 @@ export const exports = { let y = new Y(3); const sum1 = Y.add(y, 4).getA(); if (sum1 != 7) { - return { tag: 'err', value: `expected 7; got ${sum1}` }; + return { tag: 'err', val: `expected 7; got ${sum1}` }; } y.setA(5); const sum2 = Y.add(y, 4).getA(); if (sum2 != 9) { - return { tag: 'err', value: `expected 9; got ${sum2}` }; + return { tag: 'err', val: `expected 9; got ${sum2}` }; } return { tag: 'ok' }; diff --git a/test/export-buffers.js b/test/export-buffers.js new file mode 100644 index 00000000..2c54ca21 --- /dev/null +++ b/test/export-buffers.js @@ -0,0 +1,114 @@ +import { componentize } from '@bytecodealliance/componentize-js'; +import { transpile } from '@bytecodealliance/jco'; +import { assert, beforeAll, suite, test } from 'vitest'; + +import { DEBUG_TEST_ENABLED, WEVAL_TEST_ENABLED, maybeLogging } from './util.js'; + +const source = ` +let saved; +export function echo(value) { return value; } +export function remember(value) { + saved = [value, new TextEncoder().encode('x'.repeat(value.length))]; +} +export function snapshot() { return saved; } +`; +const wit = ` +package test:export-buffers; +world test { + export echo: func(value: list) -> list; + export remember: func(value: list); + export snapshot: func() -> tuple, list>; +} +`; + +let instantiate; +let coreModules; + +beforeAll(async () => { + const { component } = await componentize(source, { + sourceName: 'export-buffers.js', + witWorld: wit, + disableFeatures: maybeLogging([ + 'stdio', 'random', 'clocks', 'http', 'fetch-event', + ]), + debugBuild: DEBUG_TEST_ENABLED, + enableAot: WEVAL_TEST_ENABLED, + }); + const { files } = await transpile(component, { + name: 'export-buffers', + instantiation: 'sync', + }); + const encodedBindings = Buffer.from(files['export-buffers.js']).toString('base64'); + const modulePromise = import(`data:text/javascript;base64,${encodedBindings}`); + ({ instantiate } = await modulePromise); + coreModules = new Map( + Object.entries(files) + .filter(([name]) => name.endsWith('.wasm')) + .map(([name, bytes]) => [name, new WebAssembly.Module(bytes)]), + ); +}); + +function createInstance() { + const memories = new Set(); + let realloc; + const exports = instantiate( + name => coreModules.get(name), + {}, + (module, imports) => { + const instance = new WebAssembly.Instance(module, imports); + for (const value of Object.values(instance.exports)) { + if (value instanceof WebAssembly.Memory) memories.add(value); + } + if (instance.exports.cabi_realloc) realloc = instance.exports.cabi_realloc; + return instance; + }, + ); + assert.equal(memories.size, 1); + assert.isFunction(realloc); + return { exports, memory: [...memories][0], realloc }; +} + +suite('Export buffer ownership', () => { + test('repeated exports do not accumulate canonical buffers', () => { + const { exports, memory } = createInstance(); + const input = new Uint8Array(64 * 1024).fill(0xab); + for (let i = 0; i < 3000; i++) exports.echo(input); + const before = memory.buffer.byteLength; + for (let i = 0; i < 2000; i++) { + const result = exports.echo(input); + assert.lengthOf(result, input.length); + assert.equal(result[0], 0xab); + assert.equal(result.at(-1), 0xab); + } + // One leaked payload per call would add 125 MiB; allow for GC high-water growth. + const growth = memory.buffer.byteLength - before; + assert.isBelow(growth, 64 * 1024 * 1024, `Wasm memory grew by ${growth} bytes`); + }); + + test('retains JS-owned buffers and handles empty allocations', () => { + const { exports, realloc } = createInstance(); + const input = Uint8Array.from({ length: 4096 }, (_, i) => i & 0xff); + const saved = [ + input.slice(), + new TextEncoder().encode('x'.repeat(input.length)), + ]; + exports.remember(input); + for (let i = 0; i < 100; i++) { + input.fill(i); + assert.deepEqual(exports.echo(input), input); + } + assert.deepEqual(exports.snapshot(), saved); + + for (const align of [1, 2, 4, 8]) { + assert.equal(realloc(0, 0, align, 0), 0); + const ptr = realloc(align, 0, align, 16); + assert.notEqual(ptr, 0); + assert.equal(ptr % align, 0); + assert.equal(realloc(ptr, 16, align, 0), 0); + } + const empty = new Uint8Array(); + exports.remember(empty); + for (let i = 0; i < 100; i++) assert.deepEqual(exports.echo(empty), empty); + assert.deepEqual(exports.snapshot(), [empty, empty]); + }); +}); diff --git a/test/import-return-buffers.js b/test/import-return-buffers.js new file mode 100644 index 00000000..1a4f17c4 --- /dev/null +++ b/test/import-return-buffers.js @@ -0,0 +1,61 @@ +import { readFile } from 'node:fs/promises'; +import { Script } from 'node:vm'; +import { assert, test } from 'vitest'; + +import { splicer } from '../lib/spidermonkey-embedding-splicer.js'; + +import { DEBUG_TEST_ENABLED, WEVAL_TEST_ENABLED } from './util.js'; + +test('frees an imported string and its return area after copying', async () => { + const wit = ` +package test:import-return-buffers; +interface host { + read: func() -> string; +} +world test { + import host; +} +`; + const engineName = WEVAL_TEST_ENABLED + ? 'starlingmonkey_embedding_weval.wasm' + : `starlingmonkey_embedding${DEBUG_TEST_ENABLED ? '.debug' : ''}.wasm`; + const engine = await readFile(new URL(`../lib/${engineName}`, import.meta.url)); + const { jsBindings } = splicer.spliceBindings( + engine, [], wit, undefined, undefined, false, + ); + const memory = new WebAssembly.Memory({ initial: 1 }); + const bytes = new Uint8Array(memory.buffer); + const view = new DataView(memory.buffer); + const text = 'café 🦀'; + const encoded = new TextEncoder().encode(text); + const returnPtr = 8; + const dataPtr = 16; + + // The indirect return area is an 8-byte (data pointer, byte length) pair. + view.setUint32(returnPtr, dataPtr, true); + view.setUint32(returnPtr + 4, encoded.length, true); + bytes.set(encoded, dataPtr); + + let host; + const freed = []; + new Script(jsBindings).runInNewContext({ + WebAssembly: undefined, + contentGlobal: { TextEncoder, TextDecoder }, + defineBuiltinModule(name, exports) { + assert.equal(name, 'test:import-return-buffers/host'); + host = exports; + }, + $bindings: [ + memory, + () => assert.fail('unexpected allocation for an import parameter'), + (ptr, size = 8) => { + freed.push(ptr); + bytes.fill(0xdd, ptr, ptr + size); + }, + () => returnPtr, + ], + }); + + assert.equal(host.read(), text); + assert.deepEqual(freed, [dataPtr, returnPtr]); +});