diff --git a/site/source/docs/tools_reference/settings_reference.rst b/site/source/docs/tools_reference/settings_reference.rst index de87804636bcf..f8567c530658a 100644 --- a/site/source/docs/tools_reference/settings_reference.rst +++ b/site/source/docs/tools_reference/settings_reference.rst @@ -2509,6 +2509,26 @@ If 1, target compiling a shared Wasm Memory. Default value: false +.. _shared_wasmgc: + +SHARED_WASMGC +============= + +If true, enables support for experimental shared Wasm GC. Expects the +module to contain a mutable shared anyref global to be imported as "env" +"_shared_heap_root" and exported as "_shared_heap_root". The import will be +provided a null value on the main thread, where the user code is expected to +initialize it with some shared object during the start function. This shared +object will then be provided as the import when instantiating the module on +additional Workers. This shared anyref global can be used to bootstrap +arbitrary shared Wasm GC state. Since LLVM cannot emit Wasm GC instructions +or shared anyref globals, users are expected to use wasm-merge to add the +_shared_heap_root global and additional Wasm GC code post-link. + +.. note:: This is an experimental setting + +Default value: false + .. _wasm_workers: WASM_WORKERS diff --git a/src/lib/libpthread.js b/src/lib/libpthread.js index 020df4b83b1c5..e092fe4f20f39 100644 --- a/src/lib/libpthread.js +++ b/src/lib/libpthread.js @@ -435,6 +435,9 @@ var LibraryPThread = { #if LOAD_SOURCE_MAP wasmSourceMap, #endif +#if SHARED_WASMGC + sharedHeapRootVal: wasmExports['_shared_heap_root'].value, +#endif #if MAIN_MODULE dynamicLibraries, // Share all modules that have been loaded so far. New workers @@ -1354,7 +1357,20 @@ var LibraryPThread = { } worker.postMessage({cmd: {{{ CMD_CHECK_MAILBOX }}}}); } - } + }, + +#if SHARED_WASMGC + _shared_heap_root__deps: ['$makeSharedHeapRootGlobal'], + _shared_heap_root: "makeSharedHeapRootGlobal()", + $makeSharedHeapRootGlobal: () => { + // Wasm module for acquiring a shared anyref WebAssembly.Global: + // (module (global (export "g") (mut (ref null (shared any))) (ref.null (shared any)))) + var bytes = new Uint8Array([0, 97, 115, 109, 1, 0, 0, 0, 6, 9, 1, 99, 101, 110, 1, 208, 101, 113, 11, 7, 5, 1, 1, 103, 3, 0]); + var module = new WebAssembly.Module(bytes); + var instance = new WebAssembly.Instance(module, {}); + return instance.exports.g; + }, +#endif // SHARED_WASMGC }; autoAddDeps(LibraryPThread, '$PThread'); diff --git a/src/runtime_pthread.js b/src/runtime_pthread.js index 27996fc16499e..26d48b6b86103 100644 --- a/src/runtime_pthread.js +++ b/src/runtime_pthread.js @@ -112,6 +112,10 @@ if (ENVIRONMENT_IS_PTHREAD) { wasmSourceMap = resetPrototype(WasmSourceMap, msgData.wasmSourceMap); #endif +#if SHARED_WASMGC + __shared_heap_root.value = msgData.sharedHeapRootVal; +#endif + #if !WASM_ESM_INTEGRATION #if MINIMAL_RUNTIME // Pass the shared Wasm module in the Module object for MINIMAL_RUNTIME. diff --git a/src/settings.js b/src/settings.js index 35c77f89e0f25..af2fecf89efed 100644 --- a/src/settings.js +++ b/src/settings.js @@ -1661,6 +1661,20 @@ var USE_SQLITE3 = false; // [compile+link] var SHARED_MEMORY = false; +// If true, enables support for experimental shared Wasm GC. Expects the +// module to contain a mutable shared anyref global to be imported as "env" +// "_shared_heap_root" and exported as "_shared_heap_root". The import will be +// provided a null value on the main thread, where the user code is expected to +// initialize it with some shared object during the start function. This shared +// object will then be provided as the import when instantiating the module on +// additional Workers. This shared anyref global can be used to bootstrap +// arbitrary shared Wasm GC state. Since LLVM cannot emit Wasm GC instructions +// or shared anyref globals, users are expected to use wasm-merge to add the +// _shared_heap_root global and additional Wasm GC code post-link. +// [link] +// [experimental] +var SHARED_WASMGC = false; + // Enables support for Wasm Workers. Wasm Workers enable applications // to create threads using a lightweight web-specific API that builds on top // of Wasm SharedArrayBuffer + Atomics API. diff --git a/test/common.py b/test/common.py index 76942882bcaec..dd6eff67a62fe 100644 --- a/test/common.py +++ b/test/common.py @@ -77,6 +77,7 @@ EMCONFIG = exe_path_from_root('em-config') EMRUN = exe_path_from_root('emrun') WASM_DIS = os.path.join(building.get_binaryen_bin(), 'wasm-dis') +WASM_MERGE = os.path.join(building.get_binaryen_bin(), 'wasm-merge') LLVM_OBJDUMP = shared.llvm_tool_path('llvm-objdump') PYTHON = sys.executable diff --git a/test/test_other.py b/test/test_other.py index b25e7bca0c2b0..42f4ab821a0a0 100644 --- a/test/test_other.py +++ b/test/test_other.py @@ -43,6 +43,7 @@ NON_ZERO, PYTHON, TEST_ROOT, + WASM_MERGE, WEBIDL_BINDER, RunnerCore, check_node_version, @@ -13275,6 +13276,90 @@ def test_pthread_js_exception(self): self.set_setting('EXIT_RUNTIME') self.do_runf('other/test_pthread_js_exception.c', 'missing is not defined', assert_returncode=NON_ZERO, cflags=['-pthread']) + @requires_pthreads + @requires_node_25 + def test_shared_wasmgc(self): + create_file('test_shared_wasmgc.c', r''' + #include + #include + #include + + __attribute__((import_module("wat"))) void shared_gc_main(void); + + void print_int(int val) { + emscripten_console_logf("%d", val); + } + + void* thread_main(void* arg) { + shared_gc_main(); + return NULL; + } + + int main() { + shared_gc_main(); + + pthread_t t1, t2, t3; + pthread_create(&t1, NULL, thread_main, NULL); + pthread_create(&t2, NULL, thread_main, NULL); + pthread_create(&t3, NULL, thread_main, NULL); + + pthread_join(t1, NULL); + pthread_join(t2, NULL); + pthread_join(t3, NULL); + + return 0; + } + ''') + + create_file('shared_gc.wat', r''' + (module + (type $counter (shared (struct (field (mut i32))))) + + (import "app" "print_int" (func $print_int (param i32))) + + (global $root + (export "_shared_heap_root") + (import "env" "_shared_heap_root") + (mut (ref null (shared any))) + ) + + (func $init + (if (ref.is_null (global.get $root)) + (then + (global.set $root (struct.new $counter (i32.const 0))) + ) + ) + ) + (start $init) + + (func (export "shared_gc_main") + (call $print_int (ref.is_null (global.get $root))) + ) + ) + ''') + + out_js = self.in_dir('test_shared_wasmgc.js') + out_wasm = self.in_dir('test_shared_wasmgc.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.c', '-o', + out_js, + ]) + + self.run_process([ + WASM_MERGE, '--enable-threads', '--enable-reference-types', + '--enable-gc', '--enable-shared-everything', out_wasm, 'app', + 'shared_gc.wat', 'wat', '-o', out_wasm, + ]) + + self.node_args.append('--experimental-wasm-shared') + + # TODO: Once multithreaded casting is fixed, increment and print the counter value. + output = self.run_js(out_js) + self.assertEqual(output.splitlines(), ['0', '0', '0', '0']) + @crossplatform def test_config_closure_compiler(self): self.run_process([EMCC, test_file('hello_world.c'), '--closure=1']) diff --git a/tools/emscripten.py b/tools/emscripten.py index 82d8563214dbe..38dfd6cb5c43d 100644 --- a/tools/emscripten.py +++ b/tools/emscripten.py @@ -821,6 +821,11 @@ def add_standard_wasm_imports(send_items_map): if settings.IMPORTED_MEMORY: send_items_map['memory'] = 'wasmMemory' + # This import should come from user code merged into the module with + # wasm-merge post-link. + if settings.SHARED_WASMGC: + send_items_map['_shared_heap_root'] = '__shared_heap_root' + if settings.AUTODEBUG: extra_sent_items += [ 'log_execution', diff --git a/tools/link.py b/tools/link.py index 6319a61f5b1ed..9b79f60f0e630 100644 --- a/tools/link.py +++ b/tools/link.py @@ -517,6 +517,11 @@ def setup_pthreads(): '$invokeEntryPoint', ] + # This import should come from user code merged into the module with + # wasm-merge post-link. + if settings.SHARED_WASMGC: + settings.DEFAULT_LIBRARY_FUNCS_TO_INCLUDE += ['_shared_heap_root'] + if settings.MINIMAL_RUNTIME: building.user_requested_exports.add('exit') diff --git a/tools/settings.py b/tools/settings.py index c8d02cd70518a..71deea3bf442f 100644 --- a/tools/settings.py +++ b/tools/settings.py @@ -157,6 +157,7 @@ ('NODERAWSOCKETS', 'WASMFS', 'the node:net backend is not wired into WASMFS sockets'), ('NODERAWSOCKETS', 'PROXY_POSIX_SOCKETS', 'they are alternative socket backends'), ('NODERAWSOCKETS', 'SOCKET_WEBRTC', 'they are alternative socket backends'), + ('SHARED_WASMGC', 'NO_PTHREADS', 'SHARED_WASMGC requires threads to be enabled'), ] EXPERIMENTAL_SETTINGS = { @@ -166,6 +167,7 @@ 'CROSS_ORIGIN_STORAGE': '-sCROSS_ORIGIN_STORAGE is experimental; the underlying browser API is not yet shipped in any browser', 'SUPPORT_BIG_ENDIAN': '-sSUPPORT_BIG_ENDIAN is experimental, not all features are fully supported.', 'WASM_ESM_INTEGRATION': '-sWASM_ESM_INTEGRATION is still experimental and not yet supported in browsers', + 'SHARED_WASMGC': '-sSHARED_WASMGC is experimental and subject to change', } # For renamed settings the format is: