diff --git a/site/source/docs/tools_reference/settings_reference.rst b/site/source/docs/tools_reference/settings_reference.rst index f8567c530658a..901f385511eea 100644 --- a/site/source/docs/tools_reference/settings_reference.rst +++ b/site/source/docs/tools_reference/settings_reference.rst @@ -2514,16 +2514,27 @@ Default value: false 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. +If true, enables experimental support for bootstrapping shared Wasm GC +programs. + +If the module contains a mutable shared anyref global imported as "env" +"_shared_heap_root" and exported as "_shared_heap_root", the import will be +provided a null value on the main thread. The module's start function is +expected to check if the imported global is null, and initialize it to some +shared object if so. This shared object will then be provided as the imported +value when instantiating the module on additional Workers. This shared anyref +global can be used to bootstrap arbitrary shared Wasm GC state. + +Additionally, if the module exports mutable shared anyref globals named +"_gc_thread_state" and "_gc_spawn_arg", the runtime will get the value of +"_gc_spawn_arg" on the thread that calls pthread_create and assign it to +"_gc_thread_state" on the spawned thread. This allows simpler bootstrapping +of thread-local state. + +Both mechanisms are optional and will have no effect if the exported globals +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. .. note:: This is an experimental setting diff --git a/src/lib/libpthread.js b/src/lib/libpthread.js index 2f65bfa8ae739..b98f448578ea5 100644 --- a/src/lib/libpthread.js +++ b/src/lib/libpthread.js @@ -436,7 +436,7 @@ var LibraryPThread = { wasmSourceMap, #endif #if SHARED_WASMGC - sharedHeapRootVal: wasmExports['_shared_heap_root'].value, + sharedHeapRootVal: wasmExports['_shared_heap_root']?.value ?? null, #endif #if MAIN_MODULE dynamicLibraries, @@ -725,6 +725,9 @@ var LibraryPThread = { start_routine: threadParams.startRoutine, arg: threadParams.arg, pthread_ptr: threadParams.pthread_ptr, +#if SHARED_WASMGC + gcSpawnArg: threadParams.gcSpawnArg, +#endif }; #if OFFSCREENCANVAS_SUPPORT // Note that we do not need to quote these names because they are only used @@ -901,10 +904,18 @@ var LibraryPThread = { } #endif // OFFSCREENCANVAS_SUPPORT +#if SHARED_WASMGC + var gcSpawnArg = wasmExports['_gc_spawn_arg']?.value ?? null; +#endif + // Synchronously proxy the thread creation to main thread if possible. If we // need to transfer ownership of objects, then proxy asynchronously via // postMessage. - if (ENVIRONMENT_IS_PTHREAD && (!transferList.length || error)) { + if (ENVIRONMENT_IS_PTHREAD && (!transferList.length || error) +#if SHARED_WASMGC + && gcSpawnArg === null +#endif + ) { return pthreadCreateProxied(pthread_ptr, attr, startRoutine, arg); } @@ -925,6 +936,9 @@ var LibraryPThread = { startRoutine, pthread_ptr, arg, +#if SHARED_WASMGC + gcSpawnArg, +#endif #if OFFSCREENCANVAS_SUPPORT moduleCanvasId, offscreenCanvases, diff --git a/src/runtime_pthread.js b/src/runtime_pthread.js index 26d48b6b86103..cb1ad13f7ba17 100644 --- a/src/runtime_pthread.js +++ b/src/runtime_pthread.js @@ -138,6 +138,11 @@ if (ENVIRONMENT_IS_PTHREAD) { #if ASSERTIONS assert(msgData.pthread_ptr); assert(wasmMemory, "CMD_RUN received before CMD_LOAD"); +#endif +#if SHARED_WASMGC + if (wasmExports['_gc_thread_state']) { + wasmExports['_gc_thread_state'].value = msgData.gcSpawnArg ?? null; + } #endif // Call inside JS module to set up the stack frame for this pthread in JS module scope. // This needs to be the first thing that we do, as we cannot call to any C/C++ functions diff --git a/src/settings.js b/src/settings.js index af2fecf89efed..1d582ee26dcba 100644 --- a/src/settings.js +++ b/src/settings.js @@ -1661,16 +1661,27 @@ 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. +// If true, enables experimental support for bootstrapping shared Wasm GC +// programs. +// +// If the module contains a mutable shared anyref global imported as "env" +// "_shared_heap_root" and exported as "_shared_heap_root", the import will be +// provided a null value on the main thread. The module's start function is +// expected to check if the imported global is null, and initialize it to some +// shared object if so. This shared object will then be provided as the imported +// value when instantiating the module on additional Workers. This shared anyref +// global can be used to bootstrap arbitrary shared Wasm GC state. +// +// Additionally, if the module exports mutable shared anyref globals named +// "_gc_thread_state" and "_gc_spawn_arg", the runtime will get the value of +// "_gc_spawn_arg" on the thread that calls pthread_create and assign it to +// "_gc_thread_state" on the spawned thread. This allows simpler bootstrapping +// of thread-local state. +// +// Both mechanisms are optional and will have no effect if the exported globals +// 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. // [link] // [experimental] var SHARED_WASMGC = false; diff --git a/test/test_other.py b/test/test_other.py index af0d39f4fb216..92913a04958fc 100644 --- a/test/test_other.py +++ b/test/test_other.py @@ -13349,6 +13349,109 @@ def test_shared_wasmgc(self): output = self.run_js(out_js) self.assertEqual(output.splitlines(), ['0', '0', '0', '0']) + @requires_pthreads + @requires_node_25 + def test_shared_wasmgc_thread_state(self): + create_file('test_shared_wasmgc_thread_state.c', r''' + #include + #include + #include + + __attribute__((import_module("wat"))) void run_test(void); + __attribute__((import_module("wat"))) void thread_main(void); + + void print_int(int val) { + emscripten_console_logf("%d", val); + } + + static void* thread_entry(void* arg) { + thread_main(); + return NULL; + } + + pthread_t spawn_pthread(void) { + pthread_t thread; + pthread_create(&thread, NULL, thread_entry, NULL); + return thread; + } + + void join_pthread(pthread_t thread) { + pthread_join(thread, NULL); + } + + int main() { + run_test(); + return 0; + } + ''') + + create_file('thread_state.wat', r''' + (module + (import "app" "print_int" (func $print_int (param i32))) + (import "app" "spawn_pthread" (func $spawn_pthread (result i32))) + (import "app" "join_pthread" (func $join_pthread (param i32))) + + (global $state (export "_gc_thread_state") (mut (ref null (shared any))) (ref.null (shared none))) + (global $spawn_arg (export "_gc_spawn_arg") (mut (ref null (shared any))) (ref.null (shared none))) + + (func $spawn_thread (param $arg (ref null (shared any))) (result i32) + (local $tid i32) + (global.set $spawn_arg (local.get $arg)) + (local.set $tid (call $spawn_pthread)) + (global.set $spawn_arg (ref.null (shared none))) + (local.get $tid) + ) + + ;; TODO: Once multithreaded casting is fixed, have $spawn_thread take a + ;; function reference and an anyref argument. + (func (export "thread_main") + (call $print_int + (i31.get_u + (ref.cast (ref (shared i31)) + (global.get $state) + ) + ) + ) + ) + + (func (export "run_test") + (local $t1 i32) + (local $t2 i32) + (local $t3 i32) + + (local.set $t1 (call $spawn_thread (ref.i31_shared (i32.const 42)))) + (call $join_pthread (local.get $t1)) + + (local.set $t2 (call $spawn_thread (ref.i31_shared (i32.const 100)))) + (call $join_pthread (local.get $t2)) + + (local.set $t3 (call $spawn_thread (ref.i31_shared (i32.const 300)))) + (call $join_pthread (local.get $t3)) + ) + ) + ''') + + out_js = self.in_dir('test_shared_wasmgc_thread_state.js') + out_wasm = self.in_dir('test_shared_wasmgc_thread_state.wasm') + + self.run_process([ + EMCC, '-pthread', '-sSHARED_WASMGC', '-sERROR_ON_UNDEFINED_SYMBOLS=0', + '-sEXIT_RUNTIME', '-sPROXY_TO_PTHREAD', + '-sEXPORTED_FUNCTIONS=_main,_print_int,_spawn_pthread,_join_pthread', + 'test_shared_wasmgc_thread_state.c', '-o', out_js, + ]) + + self.run_process([ + WASM_MERGE, '--enable-threads', '--enable-reference-types', + '--enable-gc', '--enable-shared-everything', out_wasm, 'app', + 'thread_state.wat', 'wat', '-o', out_wasm, + ]) + + self.node_args.append('--experimental-wasm-shared') + + output = self.run_js(out_js) + self.assertEqual(sorted(output.splitlines()), ['100', '300', '42']) + @crossplatform def test_config_closure_compiler(self): self.run_process([EMCC, test_file('hello_world.c'), '--closure=1'])