From 68a3e789dfce7a8dd1443489a5be52e700675187 Mon Sep 17 00:00:00 2001 From: Speak Date: Wed, 11 Nov 2026 13:10:28 +0900 Subject: [PATCH 1/3] longjmp: the boundary stated, a probe to hold it, and a step that skips until the engine knows the key - C's non-local jumps are a link-time decision the way -pthread is: setjmp.h compiles either way, and whether a siglongjmp over live frames works depends on the lowering the whole link chose. The switch belongs to the artefact, so the consumer states it in the same abi table threads use: [target.'cfg(os = "emscripten")'.abi] longjmp = "wasm". Without the table, non-local jumps are not supported - probe/longjmp/ is a C program whose siglongjmp crosses two frames, each holding a volatile local the jump must not have restored, and a second jump returns over both to the origin - continuous integration attempts that probe's build: an engine without the member refuses the manifest naming it, the step reads the refusal and reports a skip; an engine that resolves the key builds it, runs it under node, and the step is the gate that turns with the engine's release --- .github/workflows/ci.yml | 36 +++++++++++++++++++++++++++++++ README.md | 42 +++++++++++++++++++++++++++++++++++- probe/longjmp/mcpp.toml | 7 ++++++ probe/longjmp/src/main.c | 46 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 130 insertions(+), 1 deletion(-) create mode 100644 probe/longjmp/mcpp.toml create mode 100644 probe/longjmp/src/main.c diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d3778b6..e059f16 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,6 +14,9 @@ name: CI # defect # task gating the ten kal_task_* symbols exist in a `-pthread` link and # in no other, and the interfaces word follows the link +# longjmp a C program that siglongjmps over two frames runs under +# node when the engine resolves the abi key, and the step +# reports a skip while no engine does # # ONE TOOLCHAIN, NOT TWO, AND THAT IS A PROPERTY OF THE TARGET RATHER THAN A # GAP. Every other implementation here is built by a matrix of compiler @@ -409,6 +412,39 @@ jobs: || { echo "::error::the task program did not exit 0 under node"; exit 1; } echo " ok a task starts, runs and joins under node, and both contexts are told their own stack" + # NON-LOCAL JUMPS, WHICH ARE A LINK-TIME DECISION THE WAY `-pthread` IS. + # + # `setjmp.h` compiles either way; whether a `siglongjmp` over live + # frames works depends on the lowering the whole link chose, so the + # switch belongs to the artefact and the probe's own root manifest + # states it (`longjmp = "wasm"`, the same table threads use). The + # engine under this pin does not know the member yet, and a manifest + # that names it is refused naming the member -- which is the skip: + # the step attempts the build, reads the refusal, and reports a skip + # rather than a green it did not earn. The day an engine resolves the + # key, the same build succeeds, the program runs, and this step is the + # gate that turns with it. + - name: A C program that siglongjmps over two frames, when the engine knows the key + run: | + set -uo pipefail + cd probe/longjmp + out=$(mcpp build --target wasm32-emscripten 2>&1) + if [ $? -ne 0 ]; then + if printf '%s\n' "$out" | grep -q "has no member 'longjmp'"; then + echo " - this engine has no member 'longjmp' in the abi table; the probe is skipped" + exit 0 + fi + printf '%s\n' "$out" + echo "::error::the probe's build failed for a reason other than the missing member" + exit 1 + fi + printf '%s\n' "$out" | tail -3 + if ! ( timeout 300 mcpp run --target wasm32-emscripten ); then + echo "::error::the sigsetjmp/siglongjmp program did not exit 0 under node" + exit 1 + fi + echo " ok a siglongjmp over two frames returns, with each frame's volatile kept" + - name: Restore the manifest if: always() run: git checkout -- mcpp.toml diff --git a/README.md b/README.md index a6d074c..0a7bec3 100644 --- a/README.md +++ b/README.md @@ -126,6 +126,38 @@ channel did exist (`[build] dialect_cxxflags`); what was missing was a way to scope it to one target and to let this feature state its requirement, which the typed table provides. +## C's non-local jumps, and the same table's third key + +A `sigsetjmp`/`siglongjmp` pair is a **link-time** decision on this platform, +for the reason threads are: `setjmp.h` compiles either way, and whether a jump +over live frames works depends on which lowering the whole link chose -- the +wasm exception runtime's tables and the symbols that land on them, or the +host's JS trampoline that the invocation goes through when the link did not +carry them. A jump through the trampoline is not a jump: the invocation leaves +the module, and the frames it means to unwind over end it instead. So the +switch belongs to the artefact, not to any package's own units, and the +consumer states it in the same abi table threads use: + +```toml +[target.'cfg(os = "emscripten")'.abi] +longjmp = "wasm" +``` + +**Without the table, non-local jumps are not supported**, which is a boundary +of the link and not a defect a package could patch: the lowering cannot be +chosen after the module is instantiated, and no flag of this package's own +translation units reaches the frames of the consumer's. + +The assertion is carried by a program rather than a claim: `probe/longjmp/` is +a C program whose `siglongjmp` crosses two frames, each holding a `volatile` +local the jump must not have restored, and continuous integration builds and +runs it under node once an engine resolves the key. Measured 2026-11-11 with +mcpp 2026.11.10.1 and `emsdk 6.0.9`: the engine has no member `longjmp` in the +abi table (its members are `threads` and `exceptions`), and a manifest naming +it is refused naming the member -- so the step reads that refusal and reports +a skip, and the boundary above is the state until an engine that resolves the +key is released and the same step turns green with it. + ## The region a context stands on `kal_task_stack` reports the stack of the calling context, and this platform @@ -144,6 +176,14 @@ The continuous integration gate that already builds a task program under `-pthread` also asks each of its two contexts for its own region and asserts that the region contains a local of that context. +## What 0.14.1 adds + +Nothing the twelve interfaces do. C's non-local jumps are stated as a +link-time decision of the artefact -- `longjmp = "wasm"` in the same abi table +threads use -- the probe program `probe/longjmp/` carries the assertion, and +continuous integration runs it once an engine resolves the key, reporting a +skip until one does. + ## What 0.14 adds (openkal 0.25) `kal_fs_hard_link` is present and refuses: MEMFS stores one name for each node @@ -201,7 +241,7 @@ implementation in its source: openkal = "0.25.0" [target.'cfg(os = "emscripten")'.dependencies] -openkal-emscripten = "0.14.0" +openkal-emscripten = "0.14.1" ``` ## Licence diff --git a/probe/longjmp/mcpp.toml b/probe/longjmp/mcpp.toml new file mode 100644 index 0000000..72a778d --- /dev/null +++ b/probe/longjmp/mcpp.toml @@ -0,0 +1,7 @@ +[package] +name = "longjmp-probe" +version = "0.1.0" +description = "A C program that siglongjmps over two frames, carrying the ABI switch the lowering needs. Not part of the implementation's build: continuous integration builds it only when the engine resolves the key below, and the refusal an engine without the key produces is the skip." + +[target.'cfg(os = "emscripten")'.abi] +longjmp = "wasm" diff --git a/probe/longjmp/src/main.c b/probe/longjmp/src/main.c new file mode 100644 index 0000000..9e2b064 --- /dev/null +++ b/probe/longjmp/src/main.c @@ -0,0 +1,46 @@ +/* THE PROBE FOR C'S NON-LOCAL JUMPS, as this platform lowers them. + * + * Two frames of C stand between the sigsetjmp and the siglongjmp, and each + * frame holds a volatile local that the jump must not have restored: a + * register-cached int that came back as its pre-jump value would describe a + * lowering that rewound the frames rather than unwound them, which is the + * observation this program exists to make. A second jump goes back over both + * frames to the origin, so each buffer is used once in each direction. */ +#include + +static volatile int visited; + +static void level_two(sigjmp_buf* back) { + visited |= 4; + siglongjmp(*back, 7); /* the return this frame never reaches */ +} + +static int level_one(sigjmp_buf* back) { + sigjmp_buf own; + volatile int kept = 1; + if (sigsetjmp(own, 1) == 0) { + kept = 2; + level_two(&own); + return -1; /* the jump skips this */ + } + /* Control arrived over level_two's frame. The volatile local of THIS frame + * kept the value it held when the jump was taken, and the deepest frame + * ran exactly once. */ + if (kept != 2 || visited != 5) return -2; + visited |= 8; + siglongjmp(*back, 9); /* a second jump, over this frame too */ + return -3; +} + +int main(void) { + sigjmp_buf origin; + volatile int kept = 1; + visited = 0; + if (sigsetjmp(origin, 1) == 0) { + kept = 2; + visited |= 1; + return level_one(&origin); + } + /* Both jumps held: the states are exactly the ones written before them. */ + return (kept == 2 && visited == 13) ? 0 : -4; +} From 3162266955247b743c29f311185021b74a701996 Mon Sep 17 00:00:00 2001 From: Speak Date: Wed, 11 Nov 2026 13:10:28 +0900 Subject: [PATCH 2/3] 0.14.1 Nothing the twelve interfaces do. C's non-local jumps are stated as a link-time decision of the artefact (longjmp = "wasm" in the same abi table threads use), the probe program probe/longjmp/ carries the assertion, and continuous integration runs it once an engine resolves the key, reporting a skip until one does. --- mcpp.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mcpp.toml b/mcpp.toml index bc89722..47addc9 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -1,7 +1,7 @@ [package] namespace = "mcpplibs" name = "openkal-emscripten" -version = "0.14.0" +version = "0.14.1" description = "An implementation of openkal for Emscripten, written ABOVE a C library rather than beneath one, because on this platform there is no kernel to issue calls to." license = "Apache-2.0" From f85eeb8ae48b5efa325f56780a482c1df0822075 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Wed, 11 Nov 2026 13:51:59 +0900 Subject: [PATCH 3/3] ci: the probe's expected failure is read, not errexit'd -- the capture joins the conditional --- .github/workflows/ci.yml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e059f16..b7aaba7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -426,10 +426,11 @@ jobs: # gate that turns with it. - name: A C program that siglongjmps over two frames, when the engine knows the key run: | - set -uo pipefail + # GitHub runs this step under `bash -e`; the failure of the probe's + # build is an expected outcome the step reads, so the capture has to + # be part of the conditional, not an assignment errexit kills first. cd probe/longjmp - out=$(mcpp build --target wasm32-emscripten 2>&1) - if [ $? -ne 0 ]; then + if ! out=$(mcpp build --target wasm32-emscripten 2>&1); then if printf '%s\n' "$out" | grep -q "has no member 'longjmp'"; then echo " - this engine has no member 'longjmp' in the abi table; the probe is skipped" exit 0