Skip to content
Merged
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
37 changes: 37 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
42 changes: 41 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion mcpp.toml
Original file line number Diff line number Diff line change
@@ -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"

Expand Down
7 changes: 7 additions & 0 deletions probe/longjmp/mcpp.toml
Original file line number Diff line number Diff line change
@@ -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"
46 changes: 46 additions & 0 deletions probe/longjmp/src/main.c
Original file line number Diff line number Diff line change
@@ -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 <setjmp.h>

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;
}
Loading