Skip to content

Commit f11cc0b

Browse files
authored
Experimental shared Wasm GC support (emscripten-core#27427)
Add -sSHARED_WASMGC. This setting assumes the module will import a mutable shared anyref global from "env" "_shared_heap_root" and re-export it as "_shared_heap_root". On the main thread, a WebAssembly.Global containing a null value will be provided as the import. It is expected that the module's start function will allocate a shared object and assign it to the imported global when (and only when) the imported value is null. When the Emscripten pthread runtime postMessages the module and other data to new Workers, it will send along the value of the "_shared_heap_root" export from the main thread, then wrap this value in a WebAssembly.Global and import it into the instance on the Worker. In this way, all Workers will end up with the same shared object in their "_shared_heap_root" globals. This is enough to bootstrap arbitrary additional shared state between the Workers. Since LLVM does not know about most Wasm GC instructions or types, the best way to create a multithreaded Wasm GC program right now is to use wasm-merge to combine a runtime Wasm module written in C with a hand-written .wat file that uses shared Wasm GC. Add a test demonstrating this workflow.
1 parent 784b48f commit f11cc0b

9 files changed

Lines changed: 153 additions & 1 deletion

File tree

site/source/docs/tools_reference/settings_reference.rst

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2509,6 +2509,26 @@ If 1, target compiling a shared Wasm Memory.
25092509

25102510
Default value: false
25112511

2512+
.. _shared_wasmgc:
2513+
2514+
SHARED_WASMGC
2515+
=============
2516+
2517+
If true, enables support for experimental shared Wasm GC. Expects the
2518+
module to contain a mutable shared anyref global to be imported as "env"
2519+
"_shared_heap_root" and exported as "_shared_heap_root". The import will be
2520+
provided a null value on the main thread, where the user code is expected to
2521+
initialize it with some shared object during the start function. This shared
2522+
object will then be provided as the import when instantiating the module on
2523+
additional Workers. This shared anyref global can be used to bootstrap
2524+
arbitrary shared Wasm GC state. Since LLVM cannot emit Wasm GC instructions
2525+
or shared anyref globals, users are expected to use wasm-merge to add the
2526+
_shared_heap_root global and additional Wasm GC code post-link.
2527+
2528+
.. note:: This is an experimental setting
2529+
2530+
Default value: false
2531+
25122532
.. _wasm_workers:
25132533

25142534
WASM_WORKERS

src/lib/libpthread.js

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -435,6 +435,9 @@ var LibraryPThread = {
435435
#if LOAD_SOURCE_MAP
436436
wasmSourceMap,
437437
#endif
438+
#if SHARED_WASMGC
439+
sharedHeapRootVal: wasmExports['_shared_heap_root'].value,
440+
#endif
438441
#if MAIN_MODULE
439442
dynamicLibraries,
440443
// Share all modules that have been loaded so far. New workers
@@ -1354,7 +1357,20 @@ var LibraryPThread = {
13541357
}
13551358
worker.postMessage({cmd: {{{ CMD_CHECK_MAILBOX }}}});
13561359
}
1357-
}
1360+
},
1361+
1362+
#if SHARED_WASMGC
1363+
_shared_heap_root__deps: ['$makeSharedHeapRootGlobal'],
1364+
_shared_heap_root: "makeSharedHeapRootGlobal()",
1365+
$makeSharedHeapRootGlobal: () => {
1366+
// Wasm module for acquiring a shared anyref WebAssembly.Global:
1367+
// (module (global (export "g") (mut (ref null (shared any))) (ref.null (shared any))))
1368+
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]);
1369+
var module = new WebAssembly.Module(bytes);
1370+
var instance = new WebAssembly.Instance(module, {});
1371+
return instance.exports.g;
1372+
},
1373+
#endif // SHARED_WASMGC
13581374
};
13591375

13601376
autoAddDeps(LibraryPThread, '$PThread');

src/runtime_pthread.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,10 @@ if (ENVIRONMENT_IS_PTHREAD) {
112112
wasmSourceMap = resetPrototype(WasmSourceMap, msgData.wasmSourceMap);
113113
#endif
114114

115+
#if SHARED_WASMGC
116+
__shared_heap_root.value = msgData.sharedHeapRootVal;
117+
#endif
118+
115119
#if !WASM_ESM_INTEGRATION
116120
#if MINIMAL_RUNTIME
117121
// Pass the shared Wasm module in the Module object for MINIMAL_RUNTIME.

src/settings.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1661,6 +1661,20 @@ var USE_SQLITE3 = false;
16611661
// [compile+link]
16621662
var SHARED_MEMORY = false;
16631663

1664+
// If true, enables support for experimental shared Wasm GC. Expects the
1665+
// module to contain a mutable shared anyref global to be imported as "env"
1666+
// "_shared_heap_root" and exported as "_shared_heap_root". The import will be
1667+
// provided a null value on the main thread, where the user code is expected to
1668+
// initialize it with some shared object during the start function. This shared
1669+
// object will then be provided as the import when instantiating the module on
1670+
// additional Workers. This shared anyref global can be used to bootstrap
1671+
// arbitrary shared Wasm GC state. Since LLVM cannot emit Wasm GC instructions
1672+
// or shared anyref globals, users are expected to use wasm-merge to add the
1673+
// _shared_heap_root global and additional Wasm GC code post-link.
1674+
// [link]
1675+
// [experimental]
1676+
var SHARED_WASMGC = false;
1677+
16641678
// Enables support for Wasm Workers. Wasm Workers enable applications
16651679
// to create threads using a lightweight web-specific API that builds on top
16661680
// of Wasm SharedArrayBuffer + Atomics API.

test/common.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@
7777
EMCONFIG = exe_path_from_root('em-config')
7878
EMRUN = exe_path_from_root('emrun')
7979
WASM_DIS = os.path.join(building.get_binaryen_bin(), 'wasm-dis')
80+
WASM_MERGE = os.path.join(building.get_binaryen_bin(), 'wasm-merge')
8081
LLVM_OBJDUMP = shared.llvm_tool_path('llvm-objdump')
8182
PYTHON = sys.executable
8283

test/test_other.py

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
NON_ZERO,
4444
PYTHON,
4545
TEST_ROOT,
46+
WASM_MERGE,
4647
WEBIDL_BINDER,
4748
RunnerCore,
4849
check_node_version,
@@ -13275,6 +13276,90 @@ def test_pthread_js_exception(self):
1327513276
self.set_setting('EXIT_RUNTIME')
1327613277
self.do_runf('other/test_pthread_js_exception.c', 'missing is not defined', assert_returncode=NON_ZERO, cflags=['-pthread'])
1327713278

13279+
@requires_pthreads
13280+
@requires_node_25
13281+
def test_shared_wasmgc(self):
13282+
create_file('test_shared_wasmgc.c', r'''
13283+
#include <pthread.h>
13284+
#include <emscripten.h>
13285+
#include <emscripten/console.h>
13286+
13287+
__attribute__((import_module("wat"))) void shared_gc_main(void);
13288+
13289+
void print_int(int val) {
13290+
emscripten_console_logf("%d", val);
13291+
}
13292+
13293+
void* thread_main(void* arg) {
13294+
shared_gc_main();
13295+
return NULL;
13296+
}
13297+
13298+
int main() {
13299+
shared_gc_main();
13300+
13301+
pthread_t t1, t2, t3;
13302+
pthread_create(&t1, NULL, thread_main, NULL);
13303+
pthread_create(&t2, NULL, thread_main, NULL);
13304+
pthread_create(&t3, NULL, thread_main, NULL);
13305+
13306+
pthread_join(t1, NULL);
13307+
pthread_join(t2, NULL);
13308+
pthread_join(t3, NULL);
13309+
13310+
return 0;
13311+
}
13312+
''')
13313+
13314+
create_file('shared_gc.wat', r'''
13315+
(module
13316+
(type $counter (shared (struct (field (mut i32)))))
13317+
13318+
(import "app" "print_int" (func $print_int (param i32)))
13319+
13320+
(global $root
13321+
(export "_shared_heap_root")
13322+
(import "env" "_shared_heap_root")
13323+
(mut (ref null (shared any)))
13324+
)
13325+
13326+
(func $init
13327+
(if (ref.is_null (global.get $root))
13328+
(then
13329+
(global.set $root (struct.new $counter (i32.const 0)))
13330+
)
13331+
)
13332+
)
13333+
(start $init)
13334+
13335+
(func (export "shared_gc_main")
13336+
(call $print_int (ref.is_null (global.get $root)))
13337+
)
13338+
)
13339+
''')
13340+
13341+
out_js = self.in_dir('test_shared_wasmgc.js')
13342+
out_wasm = self.in_dir('test_shared_wasmgc.wasm')
13343+
13344+
self.run_process([
13345+
EMCC, '-pthread', '-sSHARED_WASMGC', '-sERROR_ON_UNDEFINED_SYMBOLS=0',
13346+
'-sEXIT_RUNTIME', '-sPROXY_TO_PTHREAD',
13347+
'-sEXPORTED_FUNCTIONS=_main,_print_int', 'test_shared_wasmgc.c', '-o',
13348+
out_js,
13349+
])
13350+
13351+
self.run_process([
13352+
WASM_MERGE, '--enable-threads', '--enable-reference-types',
13353+
'--enable-gc', '--enable-shared-everything', out_wasm, 'app',
13354+
'shared_gc.wat', 'wat', '-o', out_wasm,
13355+
])
13356+
13357+
self.node_args.append('--experimental-wasm-shared')
13358+
13359+
# TODO: Once multithreaded casting is fixed, increment and print the counter value.
13360+
output = self.run_js(out_js)
13361+
self.assertEqual(output.splitlines(), ['0', '0', '0', '0'])
13362+
1327813363
@crossplatform
1327913364
def test_config_closure_compiler(self):
1328013365
self.run_process([EMCC, test_file('hello_world.c'), '--closure=1'])

tools/emscripten.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -821,6 +821,11 @@ def add_standard_wasm_imports(send_items_map):
821821
if settings.IMPORTED_MEMORY:
822822
send_items_map['memory'] = 'wasmMemory'
823823

824+
# This import should come from user code merged into the module with
825+
# wasm-merge post-link.
826+
if settings.SHARED_WASMGC:
827+
send_items_map['_shared_heap_root'] = '__shared_heap_root'
828+
824829
if settings.AUTODEBUG:
825830
extra_sent_items += [
826831
'log_execution',

tools/link.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -517,6 +517,11 @@ def setup_pthreads():
517517
'$invokeEntryPoint',
518518
]
519519

520+
# This import should come from user code merged into the module with
521+
# wasm-merge post-link.
522+
if settings.SHARED_WASMGC:
523+
settings.DEFAULT_LIBRARY_FUNCS_TO_INCLUDE += ['_shared_heap_root']
524+
520525
if settings.MINIMAL_RUNTIME:
521526
building.user_requested_exports.add('exit')
522527

tools/settings.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,7 @@
157157
('NODERAWSOCKETS', 'WASMFS', 'the node:net backend is not wired into WASMFS sockets'),
158158
('NODERAWSOCKETS', 'PROXY_POSIX_SOCKETS', 'they are alternative socket backends'),
159159
('NODERAWSOCKETS', 'SOCKET_WEBRTC', 'they are alternative socket backends'),
160+
('SHARED_WASMGC', 'NO_PTHREADS', 'SHARED_WASMGC requires threads to be enabled'),
160161
]
161162

162163
EXPERIMENTAL_SETTINGS = {
@@ -166,6 +167,7 @@
166167
'CROSS_ORIGIN_STORAGE': '-sCROSS_ORIGIN_STORAGE is experimental; the underlying browser API is not yet shipped in any browser',
167168
'SUPPORT_BIG_ENDIAN': '-sSUPPORT_BIG_ENDIAN is experimental, not all features are fully supported.',
168169
'WASM_ESM_INTEGRATION': '-sWASM_ESM_INTEGRATION is still experimental and not yet supported in browsers',
170+
'SHARED_WASMGC': '-sSHARED_WASMGC is experimental and subject to change',
169171
}
170172

171173
# For renamed settings the format is:

0 commit comments

Comments
 (0)