Skip to content
Closed
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
7 changes: 7 additions & 0 deletions ChangeLog.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,13 @@ See docs/process.md for more on how version tagging works.
- emcc will now error if `MINIMAL_RUNTIME_STREAMING_WASM_COMPILATION` or
`MINIMAL_RUNTIME_STREAMING_WASM_INSTANTIATION` are used with `SINGLE_FILE`.
These are fundamentally incompatible but were previously ignored. (#24849)
- Added two new proxying directives. foo__proxy: 'abort' will abort program
execution if JS function foo is called from a pthread or a Wasm Worker.
foo__proxy: 'abort_debug' will do the same, but only in ASSERTIONS builds, and
in non-ASSERTIONS builds will be no-op.
Use these proxying directives to annotate JS functions that should not be
getting called from Workers. (#22648)

- `--export-es6` flag was added to `file_packager.py` available when run
standalone, to enable ES6 imports of generated JavaScript code (#24737)

Expand Down
56 changes: 28 additions & 28 deletions src/jsifier.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
ATPRERUNS,
ATMAINS,
ATPOSTRUNS,
ENVIRONMENT_IS_WORKER_THREAD,
defineI64Param,
indentify,
makeReturn64,
Expand Down Expand Up @@ -437,39 +438,38 @@ function(${args}) {

const proxyingMode = LibraryManager.library[symbol + '__proxy'];
if (proxyingMode) {
if (proxyingMode !== 'sync' && proxyingMode !== 'async' && proxyingMode !== 'none') {
throw new Error(`Invalid proxyingMode ${symbol}__proxy: '${proxyingMode}' specified!`);
const possibleProxyingModes = ['sync', 'async', 'none', 'abort', 'abort_debug'];
if (!possibleProxyingModes.includes(proxyingMode)) {
throw new Error(`Invalid proxyingMode ${symbol}__proxy: '${proxyingMode}' specified! Possible modes: ${possibleProxyingModes.join(',')}`);
}
if (SHARED_MEMORY && proxyingMode != 'none') {
const sync = proxyingMode === 'sync';
if (PTHREADS) {
snippet = modifyJSFunction(snippet, (args, body, async_, oneliner) => {
if (oneliner) {
body = `return ${body}`;
snippet = modifyJSFunction(snippet, (args, body, async_, oneliner) => {
if (oneliner) {
body = `return ${body}`;
}
const rtnType = sig && sig.length ? sig[0] : null;
const proxyFunc =
MEMORY64 && rtnType == 'p' ? 'proxyToMainThreadPtr' : 'proxyToMainThread';
var prefix = '';

if (proxyingMode == 'sync' || proxyingMode == 'async') {
if (PTHREADS) {
deps.push('$' + proxyFunc);
prefix = `if (ENVIRONMENT_IS_PTHREAD) return ${proxyFunc}(${proxiedFunctionTable.length}, 0, ${+(proxyingMode === 'sync')}${args ? ', ' : ''}${args});`;
}
const rtnType = sig && sig.length ? sig[0] : null;
const proxyFunc =
MEMORY64 && rtnType == 'p' ? 'proxyToMainThreadPtr' : 'proxyToMainThread';
deps.push('$' + proxyFunc);
return `
${async_}function(${args}) {
if (ENVIRONMENT_IS_PTHREAD)
return ${proxyFunc}(${proxiedFunctionTable.length}, 0, ${+sync}${args ? ', ' : ''}${args});
if (WASM_WORKERS && ASSERTIONS) {
prefix += `\nassert(!ENVIRONMENT_IS_WASM_WORKER, "Attempted to call proxied function '${mangled}' in a Wasm Worker, but in Wasm Worker enabled builds, proxied function architecture is not available!");`;
}
} else if (proxyingMode == 'abort' || (proxyingMode == 'abort_debug' && ASSERTIONS)) {
const insideWorker = ENVIRONMENT_IS_WORKER_THREAD();
prefix = `if (${insideWorker}) abort("Attempted to call function '${mangled}' inside a pthread/Wasm Worker, but by using the proxying directive '${proxyingMode}', this function has been declared to only be callable from the main browser thread");`;
}

return `${async_}function(${args}) {
${prefix}
${body}
}\n`;
});
} else if (WASM_WORKERS && ASSERTIONS) {
// In ASSERTIONS builds add runtime checks that proxied functions are not attempted to be called in Wasm Workers
// (since there is no automatic proxying architecture available)
snippet = modifyJSFunction(
snippet,
(args, body) => `
function(${args}) {
assert(!ENVIRONMENT_IS_WASM_WORKER, "Attempted to call proxied function '${mangled}' in a Wasm Worker, but in Wasm Worker enabled builds, proxied function architecture is not available!");
Copy link
Collaborator

Choose a reason for hiding this comment

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

How come the fdwrite syscall wasn't hitting this assertion already?

${body}
}\n`,
);
}
});
proxiedFunctionTable.push(mangled);
}
}
Expand Down
4 changes: 4 additions & 0 deletions src/lib/libwasi.js
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,10 @@ var WasiLibrary = {
if (printCharBuffers[1].length) printChar(1, {{{ charCode("\n") }}});
if (printCharBuffers[2].length) printChar(2, {{{ charCode("\n") }}});
},
// When we don't have a filesystem enabled, we do not need to proxy fd_write
// to the main thread, because all we do is log out() and err() calls.
// This enables printf() to work from Wasm Workers.
fd_write__proxy: 'none',
Copy link
Collaborator

Choose a reason for hiding this comment

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

Can you make this a separate PR? It seems like a good idea but unrelated to the abort stuff.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I put it back because it is needed by the correctness of this PR.

Adding the new ASSERTIONS time check that functions are not attempted to be proxied in MINIMAL_RUNTIME mode exposed this function being proxied. It is correct here to declare this as proxy none, and otherwise the test suite won't pass in this PR.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Ah I see so there is a new assertion at play here, independent of the new abort stuff.

I do think it would be nice to split that out so that the adding of the new abort feature can be added in isolation, but if you feel strongly that its no worth it then this change LGTM.

From a casual reading of the diff its quite hard to see how/where this new assertion was added (it looks like that assertion is pre-existing, but maybe not in all cases?)

Copy link
Collaborator Author

@juj juj Aug 15, 2025

Choose a reason for hiding this comment

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

From a casual reading of the diff its quite hard to see how/where this new assertion was added (it looks like that assertion is pre-existing, but maybe not in all cases?)

The previous code was

if (PTHREADS) {
   ...
} else if (WASM_WORKERS && ASSERTIONS) {
  // add check: "assert(!ENVIRONMENT_IS_WASM_WORKER, "Attempted to call proxied function '${mangled}' in a Wasm Worker, but in Wasm Worker enabled builds, proxied function architecture is not available!");"
}

The refactored code is

if (PTHREADS) {
  ...
}
if (WASM_WORKERS && ASSERTIONS) {
  // add check: "assert(!ENVIRONMENT_IS_WASM_WORKER, "Attempted to call proxied function '${mangled}' in a Wasm Worker, but in Wasm Worker enabled builds, proxied function architecture is not available!");"
}

When the original code was written PTHREADS vs WASM_WORKERS were mutually exclusive features - you couldn't build with both of them at the same time, so the two code blocks were effectively equivalent.

But then at some point that restriction was lifted that you could build with both of them, so the new code now accounts for that.

I agree it is a bit subtle, I am not sure if I did that fix knowingly at the time, but I am kind of wrangling a queue of more than 30 PRs here, so I'd prefer not to rewrite Git history PR commits just for the prose. I think this new check is more accurate to what we'll want.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Thanks for the explanation.

It still seem odd though since I would have through that most of our tests are for either WASM_WORKERS or PTHREADS... so that assertion for fdwrite should be firing in that case?

Copy link
Collaborator

Choose a reason for hiding this comment

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

Figuring out exactly what is going on here seems like a good reason to split out the fdwrite change... but i get that you might be tired on splitting up changes at this point :)

Personally I'd love to see the assertions fix + fdwrite fix land first, then the new modes. But I really appreciate all the PRs you've been landing recently and I don't want to discourage your too much so lgtm either way.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Ok, I'll defer this feature altogether now for the time being, to avoid any chances.

fd_write__deps: ['$flush_NO_FILESYSTEM', '$printChar'],
fd_write__postset: () => addAtExit('flush_NO_FILESYSTEM()'),
#else
Expand Down
2 changes: 1 addition & 1 deletion src/parseTools.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -1091,7 +1091,7 @@ function ENVIRONMENT_IS_MAIN_THREAD() {
return `(!${ENVIRONMENT_IS_WORKER_THREAD()})`;
}

function ENVIRONMENT_IS_WORKER_THREAD() {
export function ENVIRONMENT_IS_WORKER_THREAD() {
assert(PTHREADS || WASM_WORKERS);
var envs = [];
if (PTHREADS) envs.push('ENVIRONMENT_IS_PTHREAD');
Expand Down
43 changes: 43 additions & 0 deletions test/pthread/proxy_abort.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
#include <emscripten.h>
#include <pthread.h>
#include <assert.h>

// Tests behavior of __proxy: 'abort' and __proxy: 'abort_debug' in pthreads.

void proxied_js_function(void);

int might_throw(void(*func)()) {
int threw = EM_ASM_INT({
// Patch over abort() so that we can gracefully finish the test without
// an unhandled exception hanging the test.
abort = function(text) {
throw text;
};

try {
getWasmTableEntry($0)();
} catch(e) {
console.error('Threw an exception: ' + e);
return e.toString().includes('this function has been declared to only be callable from the main browser thread');
}
console.error('Function did not throw');
return 0;
}, func);
return threw;
}

void *thread_main(void *arg) {
REPORT_RESULT(might_throw(proxied_js_function));
return 0;
}

char stack[1024];

int main() {
proxied_js_function(); // Should be callable from main thread

pthread_t thread;
pthread_attr_t attr;
pthread_attr_init(&attr);
pthread_create(&thread, &attr, thread_main, 0);
}
14 changes: 14 additions & 0 deletions test/test_browser.py
Original file line number Diff line number Diff line change
Expand Up @@ -5272,6 +5272,20 @@ def test_wasm_worker_no_proxied_js_functions(self):
self.btest('wasm_worker/no_proxied_js_functions.c', expected='0',
cflags=['--js-library', test_file('wasm_worker/no_proxied_js_functions.js')])

# Tests that the proxying directives foo__proxy: 'abort' and foo__proxy: 'abort_debug' work.
@also_with_minimal_runtime
@parameterized({
'': ('1', ['--js-library', test_file('wasm_worker/proxy_abort.js')]),
'no_assertions': ('1', ['--js-library', test_file('wasm_worker/proxy_abort.js'), '-sASSERTIONS=0']),
'debug': ('1', ['--js-library', test_file('wasm_worker/proxy_abort_debug.js'), '-sASSERTIONS']),
'debug_no_assertions': ('0', ['--js-library', test_file('wasm_worker/proxy_abort_debug.js'), '-sASSERTIONS=0']),
})
def test_proxy_abort(self, expect_result, args):
self.btest('pthread/proxy_abort.c', expected=expect_result, cflags=args + ['-pthread', '-sPTHREAD_POOL_SIZE=1'])

self.set_setting('WASM_WORKERS')
self.btest('wasm_worker/proxy_abort.c', expected=expect_result, cflags=args)

# Tests emscripten_semaphore_init(), emscripten_semaphore_waitinf_acquire() and emscripten_semaphore_release()
@also_with_minimal_runtime
def test_wasm_worker_semaphore_waitinf_acquire(self):
Expand Down
38 changes: 38 additions & 0 deletions test/wasm_worker/proxy_abort.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
#include <emscripten.h>
#include <emscripten/wasm_worker.h>
#include <assert.h>

// Tests behavior of __proxy: 'abort' and __proxy: 'abort_debug' in Wasm Workers.

void proxied_js_function(void);

int might_throw(void(*func)()) {
int threw = EM_ASM_INT({
// Patch over abort() so that we can gracefully finish the test without
// an unhandled exception hanging the test.
abort = function(text) {
throw text;
};

try {
getWasmTableEntry($0)();
} catch(e) {
console.error('Threw an exception: ' + e);
return e.toString().includes('this function has been declared to only be callable from the main browser thread');
}
console.error('Function did not throw');
return 0;
}, func);
return threw;
}

void worker_main() {
REPORT_RESULT(might_throw(proxied_js_function));
}

char stack[1024];

int main() {
proxied_js_function(); // Should be callable from main thread
emscripten_wasm_worker_post_function_v(emscripten_malloc_wasm_worker(1024), worker_main);
}
6 changes: 6 additions & 0 deletions test/wasm_worker/proxy_abort.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
addToLibrary({
proxied_js_function__proxy: 'abort',
proxied_js_function: function() {
console.log('In proxied_js_function');
}
});
6 changes: 6 additions & 0 deletions test/wasm_worker/proxy_abort_debug.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
addToLibrary({
proxied_js_function__proxy: 'abort_debug',
proxied_js_function: function() {
console.log('In proxied_js_function');
}
});
Loading