micropython / micropython/micropython

webassembly `standard` variant broken with modern Emscripten: two bugs in `library.js` and `api.js`

Open
#19,380 10 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

bug port-webassembly
Dominant language
C
Stars
22.1k
Forks
9k
Avg merge
6d 4h
Merged PRs (30d)
16

Description

Port, board and/or hardware

webassembly port, standard variant (the one built with -s ASYNCIFY).

MicroPython version

Tested with:

  • MicroPython v1.28.0 and master (both affected - bugs are present in both)
  • emscripten/emsdk:latest (≥ 3.1.68, Clang 18+; also reproduced with 6.0.1)
  • Node.js v22 (default in Ubuntu 24.04)
$ git describe --dirty
v1.28.0

# emscripten/emsdk:latest
$ emcc --version
emcc (Emscripten gcc/clang-like replacement + linker emulating GNU ld) 6.0.1 (25e4e8d6550d392ba9e0c2936bce7cf41ee47cc0)
Reproduction
Build
git clone --depth=1 --branch v1.28.0 https://github.com/micropython/micropython.git
cd micropython
make -C mpy-cross
make -C ports/webassembly
# or with a newer emsdk Docker image:
# docker run --rm -v "$PWD:/mpy" emscripten/emsdk:latest \
#   bash -c 'make -C /mpy/mpy-cross && make -C /mpy/ports/webassembly'
Run any script that triggers exception handling (setjmp/longjmp)
node ports/webassembly/build-standard/micropython.mjs - <<'EOF'
try:
    raise ValueError("test")
except ValueError:
    print("caught")
EOF
Expected behaviour

Script runs and prints caught.

Observed behaviour
Bug 1 - library.js: wrong ccall argument signature for mp_hal_get_interrupt_char

mp_hal_get_interrupt_char is declared in C as int mp_hal_get_interrupt_char(void) - it takes no arguments.
library.js calls it with one spurious argument:

// ports/webassembly/library.js
const mp_interrupt_char = Module.ccall(
    "mp_hal_get_interrupt_char",
    "number",
    ["number"],   // ← wrong: function takes void
    ["null"],     // ← wrong: no argument to pass
);

Modern Emscripten (≥ 3.1.68) performs strict ABI checking on ccall and aborts:

RuntimeError: Aborted(Assertion failed: …)

Fix: pass empty arrays for both argTypes and args:

const mp_interrupt_char = Module.ccall(
    "mp_hal_get_interrupt_char",
    "number",
    [],
    [],
);

Bug 2 - api.js: runPython calls mp_js_do_exec without { async: true }

The standard variant is built with -s ASYNCIFY and -s SUPPORT_LONGJMP=emscripten.
These flags cause Emscripten to instrument any C function that transitively uses longjmp (used by MicroPython's exception machinery) - including mp_js_do_exec.

api.js calls it via Module.ccall(...) without the { async: true } option:

// ports/webassembly/api.js
runPython(code) {
    …
    Module.ccall(          // ← missing { async: true }
        "mp_js_do_exec",
        "number",
        ["pointer", "number", "pointer"],
        [buf, len, value],
    );
    …
},

As soon as the first Python try/except (or any other code path that calls longjmp) is executed, Emscripten's Asyncify assertion fires:

RuntimeError: Aborted(Assertion failed: The call to mp_js_do_exec is running
asynchronously. If this was intended, add the async option to the ccall/cwrap call.)
    at Object.ccall (micropython.mjs:…)
    at Object.runPython (micropython.mjs:…)
    at runCLI (micropython.mjs:…)

This happens even for trivially-short scripts because MicroPython's try/except implementation always goes through longjmp.

Fix: make runPython async and pass { async: true } to ccall; update the call site in runCLI as well:

// api.js
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);
    await Module.ccall(
        "mp_js_do_exec",
        "number",
        ["pointer", "number", "pointer"],
        [buf, len, value],
        { async: true },    // ← required for Asyncify-instrumented functions
    );
    Module._free(buf);
    return proxy_convert_mp_to_js_obj_jsside_with_free(value);
},
// runCLI (same file)
await mp.runPython(contents);   // ← was: mp.runPython(contents)
Additional Information
  • The pyscript variant (-s ALLOW_MEMORY_GROWTH, no Asyncify) is not affected by Bug 2.
    It uses mp_js_do_exec_async via a different code path that is already properly async.
  • Bug 2 also affects master as of the date of this report.
  • A complete working patch (applied via sed at build time) is maintained at
    https://github.com/o-murphy/micropython-bclibc (usermod/Makefile, wasm_docker_build macro).
Code of Conduct

Yes, I agree

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with ports/webassembly/library.js and ports/webassembly/api.js, then build the standard variant with make -C ports/webassembly. Run the Node.js try/except reproduction from the issue to verify that the modern Emscripten assertions no longer occur. Done means the script prints caught while the pyscript variant remains unaffected.

Written by the indexing model from the issue text.

Assessment

Tech stack
javascript, wasm
Domain
embedded-iot
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Quiet
Clarity
Clearly specified
Newbie friendliness
68/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.