diff --git a/site/source/docs/tools_reference/settings_reference.rst b/site/source/docs/tools_reference/settings_reference.rst index 901f385511eea..c4fead5216d64 100644 --- a/site/source/docs/tools_reference/settings_reference.rst +++ b/site/source/docs/tools_reference/settings_reference.rst @@ -2536,6 +2536,9 @@ are not present. Since LLVM cannot emit Wasm GC instructions or shared anyref globals, users are expected to use wasm-merge to add these globals and additional Wasm GC code post-link. +Additionally, WebAssembly JS string builtins and imported string constants +(with module "'") are enabled when compiling the WebAssembly module. + .. note:: This is an experimental setting Default value: false diff --git a/src/lib/libdylink.js b/src/lib/libdylink.js index cb420a32191fd..9c4d3eb736d79 100644 --- a/src/lib/libdylink.js +++ b/src/lib/libdylink.js @@ -935,22 +935,34 @@ var LibraryDylink = { return moduleExports; } +#if SHARED_WASMGC + var compileOptions = { + builtins: ['js-string'], + importedStringConstants: "'", + }; +#endif + if (flags.loadAsync) { return (async () => { var instance; if (binary instanceof WebAssembly.Module) { instance = new WebAssembly.Instance(binary, info); } else { - // Destructuring assignment without declaration has to be wrapped - // with parens or parser will treat the l-value as an object - // literal instead. - ({ module: binary, instance } = await WebAssembly.instantiate(binary, info)); + ({ module: binary, instance } = await WebAssembly.instantiate(binary, info +#if SHARED_WASMGC + , compileOptions +#endif + )); } return postInstantiation(binary, instance); })(); } - var module = binary instanceof WebAssembly.Module ? binary : new WebAssembly.Module(binary); + var module = binary instanceof WebAssembly.Module ? binary : new WebAssembly.Module(binary +#if SHARED_WASMGC + , compileOptions +#endif + ); var instance = new WebAssembly.Instance(module, info); return postInstantiation(module, instance); } diff --git a/src/postamble_minimal.js b/src/postamble_minimal.js index 132faf1d0b06f..2c37f5efa8719 100644 --- a/src/postamble_minimal.js +++ b/src/postamble_minimal.js @@ -202,6 +202,12 @@ const moduleUrl = `ENVIRONMENT_IS_AUDIO_WORKLET ? '${TARGET_BASENAME}.wasm' : ne // precompiled WebAssembly Module. assert(WebAssembly.instantiateStreaming || Module['wasm'], 'Must load WebAssembly Module in to variable Module.wasm before adding compiled output .js script to the DOM'); #endif +#if SHARED_WASMGC +var compileOptions = { + builtins: ['js-string'], + importedStringConstants: "'", +}; +#endif #if MODULARIZE || AUDIO_WORKLET instantiatePromise = #endif @@ -210,13 +216,25 @@ instantiatePromise = // Node's fetch API cannot be used for local files, so we cannot use instantiateStreaming && !ENVIRONMENT_IS_NODE #endif - ? WebAssembly.instantiateStreaming(fetch({{{ moduleUrl }}}), imports) - : WebAssembly.instantiate(Module['wasm'], imports)).then((output) => { + ? WebAssembly.instantiateStreaming(fetch({{{ moduleUrl }}}), imports +#if SHARED_WASMGC + , compileOptions +#endif + ) + : WebAssembly.instantiate(Module['wasm'], imports +#if SHARED_WASMGC + , compileOptions +#endif + )).then((output) => { #else #if MODULARIZE || AUDIO_WORKLET instantiatePromise = #endif -WebAssembly.instantiateStreaming(fetch({{{ moduleUrl }}}), imports).then((output) => { +WebAssembly.instantiateStreaming(fetch({{{ moduleUrl }}}), imports +#if SHARED_WASMGC + , compileOptions +#endif +).then((output) => { #endif #else // Non-streaming instantiation @@ -235,7 +253,11 @@ assert(Module['wasm'], 'Must load WebAssembly Module in to variable Module.wasm #if MODULARIZE || AUDIO_WORKLET instantiatePromise = #endif -WebAssembly.instantiate(Module['wasm'], imports).then(/** @suppress {missingProperties} */ (output) => { +WebAssembly.instantiate(Module['wasm'], imports +#if SHARED_WASMGC + , compileOptions +#endif +).then(/** @suppress {missingProperties} */ (output) => { #endif #if !LibraryManager.has('libexports.js') && ASYNCIFY != 1 diff --git a/src/preamble.js b/src/preamble.js index 7585eaef04c2a..1c8837a502960 100644 --- a/src/preamble.js +++ b/src/preamble.js @@ -543,6 +543,13 @@ var splitModuleProxyHandler = { }; #endif +#if SHARED_WASMGC +var compileOptions = { + builtins: ['js-string'], + importedStringConstants: "'", +}; +#endif + #if SPLIT_MODULE || !WASM_ASYNC_COMPILATION function instantiateSync(file, info) { var module; @@ -569,7 +576,11 @@ function instantiateSync(file, info) { } } } - module ||= new WebAssembly.Module(binary); + module ||= new WebAssembly.Module(binary +#if SHARED_WASMGC + , compileOptions +#endif + ); if (ENVIRONMENT_IS_NODE && !hasCached) { #if RUNTIME_DEBUG dbg('NODE_CODE_CACHING: saving module'); @@ -577,7 +588,11 @@ function instantiateSync(file, info) { fs.writeFileSync(cachedCodeFile, v8.serialize(module)); } #else // NODE_CODE_CACHING - module = new WebAssembly.Module(binary); + module = new WebAssembly.Module(binary +#if SHARED_WASMGC + , compileOptions +#endif + ); #endif // NODE_CODE_CACHING var instance = new WebAssembly.Instance(module, info); return [instance, module]; @@ -588,7 +603,11 @@ function instantiateSync(file, info) { async function instantiateArrayBuffer(binaryFile, imports) { try { var binary = await getWasmBinary(binaryFile); - var instance = await WebAssembly.instantiate(binary, imports); + var instance = await WebAssembly.instantiate(binary, imports +#if SHARED_WASMGC + , compileOptions +#endif + ); return instance; } catch (reason) { err(`failed to asynchronously prepare wasm: ${reason}`); @@ -637,7 +656,11 @@ async function instantiateAsync(binary, binaryFile, imports) { #if expectToReceiveOnModule('onCOSCacheHit') Module['onCOSCacheHit']?.(cosHash.value); #endif - return WebAssembly.instantiate(cosBytes, imports); + return WebAssembly.instantiate(cosBytes, imports +#if SHARED_WASMGC + , compileOptions +#endif + ); } catch { // Any error (not found, not allowed, …) — fetch from the network and // attempt to store in COS for future page loads. @@ -670,7 +693,11 @@ async function instantiateAsync(binary, binaryFile, imports) { err(`COS store failed: ${storeErr}`); } })(); - return WebAssembly.instantiate(wasmBytes, imports); + return WebAssembly.instantiate(wasmBytes, imports +#if SHARED_WASMGC + , compileOptions +#endif + ); } catch (fetchErr) { // Network fetch failed; fall through to the standard path below. err(`COS fallback fetch failed: ${fetchErr}`); @@ -704,7 +731,11 @@ async function instantiateAsync(binary, binaryFile, imports) { ) { try { var response = fetch(binaryFile, {{{ makeModuleReceiveExpr('fetchSettings', "{ credentials: 'same-origin' }") }}}); - var instantiationResult = await WebAssembly.instantiateStreaming(response, imports); + var instantiationResult = await WebAssembly.instantiateStreaming(response, imports +#if SHARED_WASMGC + , compileOptions +#endif + ); return instantiationResult; } catch (reason) { // We expect the most common failure cause to be a bad MIME type for the binary, diff --git a/src/settings.js b/src/settings.js index 1d582ee26dcba..fb977f35424d8 100644 --- a/src/settings.js +++ b/src/settings.js @@ -1682,6 +1682,9 @@ var SHARED_MEMORY = false; // are not present. Since LLVM cannot emit Wasm GC instructions or shared anyref // globals, users are expected to use wasm-merge to add these globals and // additional Wasm GC code post-link. +// +// Additionally, WebAssembly JS string builtins and imported string constants +// (with module "'") are enabled when compiling the WebAssembly module. // [link] // [experimental] var SHARED_WASMGC = false; diff --git a/test/test_other.py b/test/test_other.py index 92913a04958fc..f27b2c2db357d 100644 --- a/test/test_other.py +++ b/test/test_other.py @@ -13452,6 +13452,60 @@ def test_shared_wasmgc_thread_state(self): output = self.run_js(out_js) self.assertEqual(sorted(output.splitlines()), ['100', '300', '42']) + @requires_pthreads + @requires_node_25 + def test_shared_wasmgc_strings(self): + create_file('test_shared_wasmgc_strings.c', r''' + #include + #include + + __attribute__((import_module("wat"))) void run_test(void); + + void print_int(int val) { + emscripten_console_logf("%d", val); + } + + int main() { + run_test(); + return 0; + } + ''') + + create_file('strings.wat', r''' + (module + (import "app" "print_int" (func $print_int (param i32))) + (import "wasm:js-string" "length" (func $length (param (ref null (shared extern))) (result i32))) + (import "wasm:js-string" "fromCharCode" (func $fromCharCode (param i32) (result (ref (shared extern))))) + (import "'" "hello world" (global $msg (ref (shared extern)))) + + (func (export "run_test") + (call $print_int (call $length (global.get $msg))) + (call $print_int (call $length (call $fromCharCode (i32.const 65)))) + ) + ) + ''') + + out_js = self.in_dir('test_shared_wasmgc_strings.js') + out_wasm = self.in_dir('test_shared_wasmgc_strings.wasm') + + self.run_process([ + EMCC, '-pthread', '-sSHARED_WASMGC', '-sERROR_ON_UNDEFINED_SYMBOLS=0', + '-sEXIT_RUNTIME', '-sPROXY_TO_PTHREAD', + '-sEXPORTED_FUNCTIONS=_main,_print_int', + 'test_shared_wasmgc_strings.c', '-o', out_js, + ]) + + self.run_process([ + WASM_MERGE, '--enable-threads', '--enable-reference-types', + '--enable-gc', '--enable-shared-everything', '--enable-strings', + out_wasm, 'app', 'strings.wat', 'wat', '-o', out_wasm, + ]) + + self.node_args.append('--experimental-wasm-shared') + + output = self.run_js(out_js) + self.assertEqual(output.splitlines(), ['11', '1']) + @crossplatform def test_config_closure_compiler(self): self.run_process([EMCC, test_file('hello_world.c'), '--closure=1'])