Repository navigation
webassembly standard variant broken with modern Emscripten: two bugs in library.js and api.js #19380
Description
Activity
Thanks for the report.
Indeed the ccall of
mp_hal_get_interrupt_charshould have no arguments, that needs to be fixed.For the asyncify issue when calling
runPython(): it's true that when asyncify is enabled the ccall must use{async:true}. But I think to fix that there needs to be a new API entry pointrunPythonWithAsyncify()(similar toreplProcessCharWithAsyncify()).I have a bunch of patches that resolve this problem.
0001-main.c-fix-external-call-depth-unused.patch
--- a/ports/webassembly/main.c +++ b/ports/webassembly/main.c @@ -47,7 +47,7 @@ // This counter tracks the current depth of calls into C code that originated // externally, ie from JavaScript. When the counter is 0 that corresponds to // the top-level call into C. -static size_t external_call_depth = 0; +static size_t external_call_depth __attribute__((unused)) = 0; // Emscripten defaults to a 64k C-stack, so our limit should be less than that. #define CSTACK_SIZE (32 * 1024)
0002-library.js-fix-interrupt-char-abi.patch
--- a/ports/webassembly/library.js +++ b/ports/webassembly/library.js @@ -35,8 +35,8 @@ const mp_interrupt_char = Module.ccall( "mp_hal_get_interrupt_char", "number", - ["number"], - ["null"], + [], + [], ); const fs = require("fs");
0003-api.js-fix-runpython-async.patch
--- a/ports/webassembly/api.js +++ b/ports/webassembly/api.js @@ -133,16 +133,17 @@ Module._free(value); }, pyimport: pyimport, - runPython(code) { + async runPython(code) { const len = Module.lengthBytesUTF8(code); const buf = Module._malloc(len + 1); Module.stringToUTF8(code, buf, len + 1); const value = Module._malloc(3 * 4); - Module.ccall( + await Module.ccall( "mp_js_do_exec", "number", ["pointer", "number", "pointer"], [buf, len, value], + { async: true }, ); Module._free(buf); return proxy_convert_mp_to_js_obj_jsside_with_free(value); @@ -250,7 +251,7 @@ } try { - mp.runPython(contents); + await mp.runPython(contents); } catch (error) { if (error.name === "PythonError") { if (error.type === "SystemExit") {
@dpgeorge
Should I open pull request to upstream?
I can add entry point you did proposeFeel free to open a PR for the
mp_hal_get_interrupt_char()fix.But for the other issues it's a more complicated. If you enable ASYNCIFY then all
api.jsentry points really need to be async, but that completely changes the API. Need to investigate how pyodide does it.For now, I suggest using the pyscript variant.
Reacted by Dmytro Yaroshenko@dpgeorge seems like mp_hal_get_interrupt_char fix done
It looks like Pyodide does not use ASYNCIFY at all (grepping their code base shows no signs of ASYNCIFY). So they do not encounter this issue. They do support JSPI though (preliminary support).
I think the only reasonable course of action here is for MicroPython to drop support for ASYNCIFY. It's just not possible to make it work well with the API in
api.js. Also, ASYNCIFY is very inefficient.Instead of ASYNCIFY we should try to integrate JSPI support.
But in the meantime, I suggest using the pyscript variant of the webassembly port (
make VARIANT=pyscript). That works well and does not use ASYNCIFY.See also #19427.
Reacted by Volodymyr Shymanskyy- added 5 commits that reference this issue
on Jul 27, 2026 See #19594 for JSPI support.
- added 3 commits that reference this issue
on Aug 23, 2026
Port, board and/or hardware
webassemblyport,standardvariant (the one built with-s ASYNCIFY).MicroPython version
Tested with:
Reproduction
Build
Run any script that triggers exception handling (setjmp/longjmp)
Expected behaviour
Script runs and prints
caught.Observed behaviour
Bug 1 -
library.js: wrongccallargument signature formp_hal_get_interrupt_charmp_hal_get_interrupt_charis declared in C asint mp_hal_get_interrupt_char(void)- it takes no arguments.library.jscalls it with one spurious argument:Modern Emscripten (≥ 3.1.68) performs strict ABI checking on
ccalland aborts:Fix: pass empty arrays for both
argTypesandargs:Bug 2 -
api.js:runPythoncallsmp_js_do_execwithout{ async: true }The
standardvariant is built with-s ASYNCIFYand-s SUPPORT_LONGJMP=emscripten.These flags cause Emscripten to instrument any C function that transitively uses
longjmp(used by MicroPython's exception machinery) - includingmp_js_do_exec.api.jscalls it viaModule.ccall(...)without the{ async: true }option:As soon as the first Python
try/except(or any other code path that callslongjmp) is executed, Emscripten's Asyncify assertion fires:This happens even for trivially-short scripts because MicroPython's
try/exceptimplementation always goes throughlongjmp.Fix: make
runPythonasync and pass{ async: true }toccall; update the call site inrunCLIas well:Additional Information
pyscriptvariant (-s ALLOW_MEMORY_GROWTH, no Asyncify) is not affected by Bug 2.It uses
mp_js_do_exec_asyncvia a different code path that is already properly async.masteras of the date of this report.sedat build time) is maintained athttps://github.com/o-murphy/micropython-bclibc (
usermod/Makefile,wasm_docker_buildmacro).Code of Conduct
Yes, I agree