diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d3778b6..b7aaba7 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,40 @@ 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: | + # 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 + 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 + 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/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" 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; +}