Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 21 additions & 10 deletions site/source/docs/tools_reference/settings_reference.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
18 changes: 16 additions & 2 deletions src/lib/libpthread.js
Original file line number Diff line number Diff line change
Expand Up @@ -436,7 +436,7 @@ var LibraryPThread = {
wasmSourceMap,
#endif
#if SHARED_WASMGC
sharedHeapRootVal: wasmExports['_shared_heap_root'].value,
sharedHeapRootVal: wasmExports['_shared_heap_root']?.value ?? null,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you need the ?? null it seems redundant to because if the null would only used if the LHS is already null-ish.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we didn't have the ?? and there was no export, this would end up sending undefined. But then I guess the receiving thread would not do anything with the undefined, so it would be ok. I'll simplify this.

#endif
#if MAIN_MODULE
dynamicLibraries,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -901,10 +904,18 @@ var LibraryPThread = {
}
#endif // OFFSCREENCANVAS_SUPPORT

#if SHARED_WASMGC
var gcSpawnArg = wasmExports['_gc_spawn_arg']?.value ?? null;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ditto

#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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does gcSpawnArg need to go in transferList? Is so then maybe this change is not needed?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, it can just be added as a normal property of the message.

#endif
) {
return pthreadCreateProxied(pthread_ptr, attr, startRoutine, arg);
}

Expand All @@ -925,6 +936,9 @@ var LibraryPThread = {
startRoutine,
pthread_ptr,
arg,
#if SHARED_WASMGC
gcSpawnArg,
#endif
#if OFFSCREENCANVAS_SUPPORT
moduleCanvasId,
offscreenCanvases,
Expand Down
5 changes: 5 additions & 0 deletions src/runtime_pthread.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you need the ?? null here?

}
#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
Expand Down
31 changes: 21 additions & 10 deletions src/settings.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
103 changes: 103 additions & 0 deletions test/test_other.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 <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')

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You don't (or at least you shouldn't) need self.in_dir here

out_wasm = self.in_dir('test_shared_wasmgc_thread_state.wasm')

self.run_process([
EMCC, '-pthread', '-sSHARED_WASMGC', '-sERROR_ON_UNDEFINED_SYMBOLS=0',

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is ERROR_ON_UNDEFINED_SYMBOLS=0 needed?

'-sEXIT_RUNTIME', '-sPROXY_TO_PTHREAD',
'-sEXPORTED_FUNCTIONS=_main,_print_int,_spawn_pthread,_join_pthread',

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of EXPORTED_FUNCTIONS you can add `EMSCRIPTEN_KEEPALIVE in the source

'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'])
Expand Down
Loading