Merge branch 'main' into shared-wasmgc-pthread-create
diff --git a/site/source/docs/tools_reference/settings_reference.rst b/site/source/docs/tools_reference/settings_reference.rst
index f8567c5..901f385 100644
--- a/site/source/docs/tools_reference/settings_reference.rst
+++ b/site/source/docs/tools_reference/settings_reference.rst
@@ -2514,16 +2514,27 @@
 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 2f65bfa..b98f448 100644
--- a/src/lib/libpthread.js
+++ b/src/lib/libpthread.js
@@ -436,7 +436,7 @@
         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 @@
         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 @@
     }
 #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 @@
       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 26d48b6..cb1ad13 100644
--- a/src/runtime_pthread.js
+++ b/src/runtime_pthread.js
@@ -139,6 +139,11 @@
         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
         // until the thread stack is initialized.
diff --git a/src/settings.js b/src/settings.js
index af2fecf..1d582ee 100644
--- a/src/settings.js
+++ b/src/settings.js
@@ -1661,16 +1661,27 @@
 // [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 af0d39f..92913a0 100644
--- a/test/test_other.py
+++ b/test/test_other.py
@@ -13349,6 +13349,109 @@
     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 <pthread.h>
+    #include <emscripten.h>
+    #include <emscripten/console.h>
+
+    __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'])