Skip to content

Experimental shared Wasm GC support - #27427

Merged
tlively merged 10 commits into
mainfrom
shared-wasmgc
Jul 28, 2026
Merged

Experimental shared Wasm GC support#27427
tlively merged 10 commits into
mainfrom
shared-wasmgc

Conversation

@tlively

@tlively tlively commented Jul 28, 2026

Copy link
Copy Markdown
Member

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.

Add -sSHARED_WASMGC. This setting assumes the module will import a mutable shared anyref global from "env" "shared_root" and re-export it as "shared_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 WebWorkers, it will send the along the value of the "shared_root" export from the main thread, then wrap this value in a WebAssembly.Global and import it into the instance on the WebWorker. In this way, all Workers will end up with the same shared object in their "shared_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.
@tlively
tlively requested a review from sbc100 July 28, 2026 05:10
Comment thread tools/emscripten.py Outdated
send_items_map['memory'] = 'wasmMemory'

if settings.SHARED_WASMGC:
send_items_map['shared_root'] = '_shared_root'

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.

Lets add a comment here. Something like: "This import gets injected by xxx, post-link"

Comment thread tools/link.py Outdated
]

if settings.SHARED_WASMGC:
settings.DEFAULT_LIBRARY_FUNCS_TO_INCLUDE += ['shared_root']

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.

Lets give this a more usnique name, how about _wasmgc_root_node, or _wasmgc_shared_root?

Maybe good to also add a leading underscore (which then means 2 underscores in the JS code :) Sorry.

Comment thread src/settings.js Outdated
// [compile+link]
var SHARED_MEMORY = false;

// If true, enables support for experimental shared Wasm GC.

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.

Maybe elaborate a little here.. mention the that it enabled a shared too object?

Comment thread src/runtime_pthread.js Outdated
#endif

#if SHARED_WASMGC
_shared_root = getSharedRootGlobal(msgData.sharedRootVal);

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.

Maybe this should be called makeSharedRootGlobal.

Could we just do shared_root: 'makeSharedRootGlobal()' (i.e. remove the postset and remove the assertion in makeSharedRootGlobal).

Then here in runtime_pthread.js we could just do:

assert(msgData.sharedRootVal);
_shared_root.value = msgData.sharedRootVal;

That way all thread including the main thread create the global on startup and its just the value of the global that gets updated when the postMessage arrives?

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.

Good idea, will investigate.

Comment thread test/test_other.py Outdated

@requires_pthreads
def test_shared_wasmgc(self):
self.require_node_25()

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 can use the requires_node_25 decorator instead of this (unless it somehow conflicts with requires_pthreads)?

Comment thread test/test_other.py Outdated
out_js,
])

building.run_binaryen_command(

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.

Can you just do self.run_process([WASM_MERGE (see elsewhere where we run WASM_DIS list this).

The building.run_binaryen_command is more geared to running commands inside the compiler itself.

Comment thread test/test_other.py

# 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'])

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.

Would it make sense to log some non-zero dummy value?

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.

Yes, but doing anything more interesting than just sticking an i32.eqz in there is blocked on V8 fixing casts across threads.

@sbc100 sbc100 left a comment

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.

Weren't you going to bring back node canary testing for this?

@tlively

tlively commented Jul 28, 2026

Copy link
Copy Markdown
Member Author

Weren't you going to bring back node canary testing for this?

Canary isn't necessary to run the current test case. Once V8 fixes the casting bug, I will make the test case more interesting and bring back node canary to test it.

@tlively

tlively commented Jul 28, 2026

Copy link
Copy Markdown
Member Author

@sbc100, this should be ready for another look.

@sbc100

sbc100 commented Jul 28, 2026

Copy link
Copy Markdown
Collaborator

Can you update the PR description?

Comment thread src/lib/libpthread.js Outdated
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, {});
var global = instance.exports.g;

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.

Maybe just return instance.exports.g?

Comment thread test/test_other.py Outdated
])

self.run_process([
common.WASM_MERGE, '--enable-threads', '--enable-reference-types',

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.

We tend to drop the common. prefix for command names and add them to import xxx from common instead

@tlively
tlively enabled auto-merge (squash) July 28, 2026 21:38
@tlively
tlively merged commit f11cc0b into main Jul 28, 2026
43 checks passed
@tlively
tlively deleted the shared-wasmgc branch July 28, 2026 22:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants