Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_ea9f71488b6e448da93d6c2dabf4879c
30 KiB
gex-eject: standalone projects from a gex session
gex-eject turns what a gex run learned into a project that builds and runs
on its own, the way a pewasm project does: the program's code translated
ahead of time, the files it needs, a runtime, and a place for your own code
(library replacements) that survives regeneration.
gexc run --record rec ./prog 5 # 1. record a session
gexc eject rec --out prog-project # 2. write the project
prog-project/build.sh # 3. build it (translate, bind replacements)
prog-project/run.sh # 4. run it: prog 5, as recorded
prog-project/run.sh 7 # or with other arguments
The code is in crates/gex-eject (the project model, the eject, the build,
the run), shims/native/src/aot.rs (the runtime side: prebuilt modules and
replacement calls) and crates/gex-xlate/src/intrinsic.rs
(Intrinsic::Replace). The session format is docs/profile-format.md.
This covers eject v0 and v1 (merge plan M4, M5); the status is at the end.
The project
prog-project/
gex-project.json what the project is generated
rootfs/ the closure and the files the session read generated
session/ session.json and profile.json it came from generated
runtime/gexc the runtime: a copy of the gexc that ejected generated
gen/gex_replace.h the guest ABI, for replacement code generated
build.sh the build step generated
run.sh the native launcher generated
.gitignore ignores build/ generated
user/ yours created once
eject.json settings: replacements, coverage, arguments
src/*.c replacement functions
files/ extra files for the guest's filesystem
build/ build output (build.sh)
modules/*.wasm one tier-1 module per program image
user.wasm user/src/*.c, compiled
rootfs/ rootfs/ with user/files/ over it
build.json what was built from what
- Generated files are rewritten by every eject, from the session and the
runtime alone. Re-ejecting the same session with the same gexc gives the
same bytes (
diff -rof two ejects is empty). user/is created from templates when it is missing and never written again. A re-eject into an existing project keeps it.build/is deleted by an eject, because it was built from what the eject replaces.- An eject refuses to write into a non-empty directory that is not a project.
What goes into rootfs/
From session.json (see docs/profile-format.md):
- Objects (the executable, and for a dynamic program the interpreter and
libraries), each read from where the run read it (
host_path, the sysroot, or the host) and checked against the hash the session recorded. A changed file fails the eject. - Files the session read, at their guest paths, with their hashes in
gex-project.json. - Directories it opened, and its working directory, are created.
Left out, and listed in gex-project.json so the reader can see them:
missing: paths the session probed and did not find. They stay missing.outputs: files it created or only wrote./proc,/dev,/sys: gex makes those.
A file the session read from /tmp fails the eject: under a sysroot, which is
how a project runs, gex mounts a fresh tmpfs on /tmp.
Adding files: put them in user/files/ at their guest path
(user/files/etc/foo.conf is /etc/foo.conf) and rebuild. They win over
rootfs/. Editing rootfs/ itself fails the build: it is generated, and its
hashes are checked.
Building
build.sh:
- compiles
user/src/*.c, if there are any, intobuild/user.wasmwithclang --target=wasm32(flags below); - runs
runtime/gexc eject build ., which- checks
rootfs/againstgex-project.json; - binds and checks every replacement (below);
- places the closure (below) and translates each object into a tier-1
module (
build/modules/NAME-HASH.wasm) with gex's library intrinsics (themem*functions) and the replacements; - copies
rootfs/anduser/files/intobuild/rootfs/; - writes
build/build.json, including a hash of everything a user can change (gex-project.json,user/eject.json, replacement modules).
- checks
Coverage (user/eject.json "coverage"):
"profile"(default): every block the recorded session ran (profile.json), with every page those touch translated whole, and nothing followed off them onto fresh pages: what was seen. An object the session ran no code in gets its entry point only. Without aprofile.jsonthis is"static"."static":"profile"plus what discovery finds from the symbol table and the data. Covers paths other inputs take."exhaustive": every decodable address.
Measured on the host's dynamically linked gzip (x86-64, glibc): "static"
translated 342,515 blocks of libc into a 168 MB module that took Cranelift
7 CPU-minutes; "profile" translated 45,591 (24 MB). The corpus kernels and
a static gzip run the same under both. A static gzip ejected from a -9
compression still compresses at -1 (same pages), and fails loudly at -d.
Dynamically linked programs. The closure is placed the way gexc build --closure places one: the executable at PIE_BASE, the interpreter at
INTERP_BASE (both where gex's loader puts them), and the libraries up from
MMAP_BOTTOM in the session's order, 64 KiB aligned. The program's module
owns the memory and dispatch tables, and every other object's module
continues its slot numbering. At run time the libraries are pinned
(RunOptions::pins, as gexc run --pin), and their modules are installed
into the runtime's module registry before the program starts, so threads and
forked children get them too. A pin that does not land is reported under
--verbose; code at the wrong address then misses loudly.
Running
run.sh is runtime/gexc eject run .:
./run.sh # the recorded argv (or user/eject.json "args")
./run.sh ARGS... # other arguments after argv[0]
./run.sh --strace -- ARGS... # trace system calls
./run.sh --jit-fallback -- ARGS...
The guest sees build/rootfs as /, starts in the recorded working
directory, with the recorded environment. Its image module comes from the
build (looked up by the loaded image's identity: entry, mode and placed
segments), never from translation.
Nothing is translated at run time unless --jit-fallback is given. A
branch to code the build does not have fails loudly:
gexc: gex failed at guest pc=0x102382c: gex-eject: code at 0x102382c (in the program's
image) was not translated when the ejected project ... was built: the recorded session
never reached it and discovery did not find it. Record a session that reaches it and
re-eject, set "coverage" to "static" or "exhaustive" in user/eject.json, or run with
--jit-fallback
Code the program generates at run time (an anonymous executable mapping) is
reported as such: only --jit-fallback can run it.
Likewise a program the project has no module for (an execve of a program
the session did not run) fails naming it, and a project changed since its
build (user/eject.json, a replacement module) refuses to run until
build.sh has been run again.
--jit-fallback (on a run), or "jit_fallback": true in user/eject.json
(for every run of the build, native and web), turns runtime translation back
on. It is off by default. Both runtimes already contain the translator, so
it ships nothing extra yet; with gex-kernel's feature gating it becomes what
decides whether the translator ships.
Library replacement
A replacement serves a guest function with a user wasm function. In
user/eject.json:
{
"replace": [
{ "object": "ack", "sha256": "sha256:3c1f...", "symbol": "ack",
"export": "my_ack", "abi": "c" },
{ "import": "getenv", "object": "gzip", "export": "my_getenv" },
{ "library": "libSDL2-2.0.so.0", "wasm": "user/sdl2.wasm", "prefix": "" }
]
}
Each entry names what it replaces in exactly one of four ways:
| Target | Binds | Staleness check |
|---|---|---|
"symbol": "f" |
the function f defines in object, at its definition: direct calls, PLT calls through the GOT, pointers, tail calls |
sha256 (the object) and bytes (the function), each if given |
"offset": "0x...", "size": "0x..." |
the function at that link-time address of object, for a stripped object |
bytes required, sha256 if given |
"import": "f" |
object's PLT stub for f (every object's, with no object): only that importer's calls; the definition is untouched |
sha256 if given |
"library": "libX.so" |
every function the library exports, each served by the module's export of the same name (after prefix) |
sha256 if given |
| Field | Meaning |
|---|---|
object |
The object: its guest path, file name or soname. |
sha256 |
The object's hash, from gex-project.json. If the object changes, the build fails and names the stale replacement instead of binding it to a function that may have changed. |
bytes |
sha256: of the replaced function's own bytes ([address, address + size), size from the symbol table or the entry). Catches a changed function in a changed object, and keeps an offset from binding to whatever moved there. |
export |
The wasm function (default: the symbol; fn_<offset> for an offset). |
prefix |
For a library: what the module's export names start with. |
wasm |
The module, relative to the project (default build/user.wasm, which build.sh compiles from user/src/*.c). Any producer's module works if it meets the rules below. |
abi |
"c" (default) or "ctx". |
gexc eject override <project> <object> <symbol|0xoffset> [--size N]
prints a checked entry, with both hashes filled in:
$ gexc eject override . ack 0x1011720 --size 0x60
{
"object": "ack",
"sha256": "sha256:723b0a60...",
"offset": "0x1011720",
"size": "0x60",
"bytes": "sha256:934f6a41...",
"export": "fn_1011720",
"abi": "c"
}
Swapping a whole library. {"library": "libsq.so"} binds every
function the library exports. A function the module does not provide is
bound to a placeholder that stops the run naming it, when (and only when) it
is called, like pewasm's aborting import placeholders; the build lists them.
An IFUNC export fails the build (its address is a resolver). The library
itself is still loaded, so ld.so resolves against it and its initialisers
run; none of its exported functions' code does.
How it is bound. The build puts Intrinsic::Replace(id) at the bound
address (the definition, or for an import the PLT stub), which generalises gex's memcpy-style intrinsics
(gex-xlate/src/intrinsic.rs) from a fixed set of wasm instructions to any
user function. That block is one host call, sysreg_write(pc, REPLACE_SYSREG, id), and a return. So every way of reaching the address runs
the replacement: a direct call, a PLT stub through its resolved GOT slot, a
function pointer, a tail call. None of the original function's code runs
(entries into its middle, which a normal program never makes, still reach the
original blocks).
The three ABIs.
"c": an ordinary C function, typed by its wasm signature. The runtime passes the guest's integer argument registers (x0-x7, orrdi,rsi,rdx,rcx,r8,r9) to its i32/i64 parameters in order, and its floating-point argument registers (v0-v7orxmm0-xmm7) to its f32/f64 ones, and writes the result tox0/rax(an i32 zero-extended) orv0/xmm0. More arguments than registers is a build error; use"ctx"."ctx":void f(gex_ctx c), like a pewasm import binding. The function reads and writes the guest's registers through the context itself, with the macros ingen/gex_replace.h(GEX_ARG(c, i),GEX_RET(c),GEX_SP(c),GEX_STACK_ARG(c, i),GEX_FARG_F64(c, i), ...). It must not return for the guest: the block does that after it."entry":void f(int ctx)that is the function, its return included: on return, the context's PC says where the guest goes next, and the block (Intrinsic::Override) dispatches there. Whatgexc eject decompilewrites (below). Only for a function bound bysymboloroffset.
Any replacement module may import, besides env.memory, the gex.* host
functions translated code calls (syscall, safepoint, sysreg_read,
sysreg_write, cache_op, fma, illegal, breakpoint,
dispatch_miss, as gen/gex_abi.h declares them). They are the very
functions the guest's own modules get, so the syscall allow-list applies to
them too; a host call that stops the guest (an exit, a fork, a signal)
unwinds through the replacement exactly as through translated code, which
is sound when the replacement left the context architectural first, as the
function tier's C does.
Memory. A guest address is a linear-memory offset, so a guest pointer is
a C pointer (GEX_PTR(addr) for a 64-bit value). The replacement runs in the
guest's memory. Its own data, heap and stacks live in
[0xF0000000, 0xF1000000), above MMAP_TOP: the guest is never given an
address there and the browser shim's arena never claims one (the shim itself
keeps its stack and data in [STACK_TOP, MMAP_BOTTOM)). The first 8 MiB are
data and heap, then one 128 KiB stack for each of up to 64 guest threads,
which the runtime points __stack_pointer at. Natively the region is
recorded as a mapping, so fork copies it; in the browser the memory is
grown to cover it at the first replacement call.
Module rules (checked by the build, before anything runs):
- It may import only
env.memory, with a maximum of at least the guest's 65535 pages: shared for the native runtime, unshared for the web build. Anything else (an Emscripten runtime's imports, WASI) is refused, naming the import. - If it imports memory, it exports a mutable i32
__stack_pointer. - The export exists and has a type the ABI can call.
build.sh compiles each user/src/*.c with the function tier's flags
(gex_cgen::toolchain::CFLAGS: --target=wasm32 -O2 -mtail-call -msimd128 -matomics -mbulk-memory ... -ffreestanding -nostdlib) plus
-fvisibility=default -Igen, and links twice:
wasm-ld --no-entry --export-dynamic --import-memory --max-memory=4294901760 \
--global-base=4026531840 --export=__stack_pointer --export=__heap_base \
--shared-memory -o build/user.wasm build/obj/*.o # native
... -o build/user.web.wasm build/obj/*.o # the web build's plain memory
USER_CFLAGS (clang) and USER_LDFLAGS (wasm-ld) add to it. There is no
libc: the code is freestanding.
Examples, all run on both guests unless noted:
| Program | Replaced | ABI | Calls | Output |
|---|---|---|---|---|
corpus ack 3 |
the program's ack |
"c" |
8 | same result; kernel 79 ms → 9 ms (AArch64) |
corpus strings 3 |
musl's strtol (and its alias strtoll) |
"ctx", writes *endptr itself |
720,000 | same result |
host gzip -9 (x86-64, dynamic glibc) |
glibc's getenv, called through gzip's PLT |
"c" |
2 | same bytes |
a program linked against libsq.so (x86-64) |
the whole library (sq, cube; unused_fn left missing) |
"c" |
10 + 10 | 2310 → 2330; dropping cube from the module stops the run at its first call, naming it |
| the same | only the program's import of sq |
"c" |
10 | 2320 |
The first:
/* user/src/ack.c */
#include <stdint.h>
static uint32_t calls;
uint32_t my_ack(uint32_t m, uint32_t n) {
calls++;
while (m != 0) {
n = n == 0 ? 1 : my_ack(m, n - 1);
m--;
}
return n + 1;
}
uint32_t my_ack_calls(void) { return calls; }
Decompiling a function
gexc eject decompile <project> <object> <symbol|0xoffset> [--size N] [--callees] [--force]
writes the function tier's C for a guest function into
user/src/decompiled_<name>.c and prints the user/eject.json entry that
binds it (with the object's hash and the function's bytes' hash, so a changed
binary reports the override as stale):
$ gexc eject decompile . ack ack
user/src/decompiled_ack.c: 1 function of C; bind it in user/eject.json with:
{ "object": "ack", "sha256": "sha256:723b...", "symbol": "ack",
"bytes": "sha256:60bd...", "export": "decompiled_ack", "abi": "entry" }
- The C is exactly what the function tier (
gex-cgen, Phase B) emits for the function, from the same recovery (gex_xlate::func::recover, with the object's.eh_frame), summaries and filters (gex_cgen::select), as one standalone unit (gex_cgen::standalone): guest registers are C locals, guest memory goes throughgen/gex_abi.h's helpers (the tier-1 bounds semantics), system calls and safepoints through thegex.*host functions. Its export,decompiled_<name>(int ctx), is bound with the"entry"ABI. - It round-trips: unmodified and bound, it gives the original's output
(natively and in the web build). Checked on both 64-bit guests: corpus
ack(ack),qsort(qsort with--callees, kernel with--callees: the whole tree including the libc it calls),strings(kernel with--callees), and gzip (longest_match, 468,088 calls;updcrc;deflatewith--callees: 114 functions on AArch64, 74 on x86-64). - What it does not hold, it leaves as the function tier does: a call to
a function not in the file writes every register back, sets PC to the
callee and returns; the rest of the function then runs as the original
translated code once the callee returns into it. So an edit after such a
call does not take effect.
--calleesputs every function it reaches by direct call (that the tier takes) into the file too, so those calls stay in C. Indirect calls go through the file's owngex_call_knownand leave when the target is not in it. - Refused, loudly: a function recovery did not find, one the tier does not take (self-modifying pages, x87 statements with no C lowering yet), and 32-bit (i386) code.
- The file is yours: decompile refuses to overwrite it without
--force.
The web build (Node and the browser)
gexc eject ... --web-shim gex_browser.wasm vendors gex's browser runtime
into runtime/web/: the shim you give it (built from the same source, for
the same guest: shims/browser, wasm32-unknown-unknown, with
shims/browser/build.sh's link flags) and the gex.mjs this gexc was built
with. build.sh then also writes build/web/:
- the program translated for the browser's single-Worker shim (a plain,
unshared memory). A static program is one image module. A dynamic one is
run the way gex's own pages run a closure (
gexc build --closure --shared-image): every object, the executable included, is a demand-filled library module, and the process starts from a seed module; manifest.json: modules, files (build/rootfs), arguments, environment, pins, replacements, the guest ABI the replacements are called with, and the syscall allow-list.
Launchers, sharing runtime/web/eject.mjs:
node run.mjs [--jit-fallback] [--strace] [--] [ARGS...] # Node
node serve.mjs 8000 # then open http://localhost:8000/?args=5
The page needs cross-origin isolation (gex's runtime uses
SharedArrayBuffer); serve.mjs sends the two headers. Replacements run
in JavaScript's hands: the user module is instantiated on the guest's memory
and called with the same c/ctx ABI as natively. build.sh links
build/user.web.wasm for it, since a module importing a shared memory will
not link to the shim's plain one (checked at build).
Checked on both guests in Node (run.mjs, JIT off): the corpus kernels,
static gzip and, on x86-64, the host's dynamically linked glibc
gzip, and a program whose whole library is swapped for wasm. Checked in
headless Chromium: the ack page, with arguments.
Limits of the web build:
- One process, one thread. A session that forked or ran threads gets a manifest that says so, and the launchers refuse it.
- File times are the bundle's (
manifest.jsonmtimes, frombuild/rootfs, which keeps the times of the files the session read), and a file the guest writes is stamped when it is written, sogzipwithout-nmatches the native run. (Untilmerge/web-fixesthe browser's tmpfs answered 0.)
Found and fixed on the way (shims/browser): with the syscall trace on, a
miss the single-Worker shim translated (--jit-fallback --strace) asked
gex_closure_owner from inside the kernel borrow and panicked ("the kernel
was re-entered"); it asks the held form now. And the break of a dynamic program
started above the interpreter (Program::brk), which sits at
INTERP_BASE == BRK_TOP, so it could never grow and glibc's malloc fell
back to mmap: a different path through libc than natively. It now follows
the executable, as natively. And the launcher copies what the guest writes
before handing it to Node, whose asynchronous stdout otherwise sent bytes the
guest had since overwritten.
The slim runtime (runtime level)
-
System calls: only those the recorded session made are allowed, by canonical number, natively (
gex-native's syscall import) and in the browser (the launcher's guard). Anything else stops the run, naming it:gexc: gex failed at guest pc=0x1011b04: gex-eject: system call mmap (222) was never made by the session the ejected project ... was ejected from, and its runtime allows only those. Record a session that makes it, or add it to "allow_syscalls" in user/eject.jsonuser/eject.json:"allow_syscalls": ["mmap"]adds some (by name or number);"syscalls": "all"turns the list off. -
Single process: a session that never forked gives a runtime that refuses
fork(natively; the web build is single-process anyway). -
Not yet: gex-kernel subsystems behind cargo features (waits for S12's N4, as gex-kernel is being edited heavily).
Features
An ejected build starts as the whole runtime a gex page runs, and
user/eject.json "features" takes parts out of it: each is true (kept,
the default) or false (left out). The list is crates/gex-eject/src/ features.rs; a feature switched off that no build can remove yet fails the
build and says why, rather than doing nothing.
| Feature | What it is | Off |
|---|---|---|
compositor |
the scene compositor: every window in one WebGPU canvas, drawn by a worker, with server-side decorations, its own cursor and hit testing (compositor.js, compositor-worker.js, gpu-device.js) |
each window is its own canvas in the page, laid out by web/src/bare-desktop.js and composited by the browser; no decorations (Wine draws its own frames). The page is built with __GEX_COMPOSITOR__ false (vite define, from GEX_FEATURES_OFF=compositor), so the code is not in the bundle |
gl |
the guest's OpenGL: Mesa on WebGPU (gex/gex-wgpu-replay.*) |
the page ships no Mesa |
multiprocess |
more than one guest process | not yet: a W3 program is several processes |
translation |
run-time translation (tier-1, the function JIT) behind the units | not yet: the shim's translator is not a separate build (kernel and shim feature gating) |
Only a W3 page's build applies them so far; a Linux or NT-layer project with a feature off fails its build.
W3 pages
A W3 program (Wine as native wasm32: every Wine process a guest process
running wine, its PE side in winepe.wasm, the Windows images'
code in function-tier units) runs in the same page (nt.html) as any
demo. Ejecting one makes that page a standalone site, with the features
the user keeps:
# 1. record: run the page to its window (headless Chromium)
node tools/eject/record-w3.mjs http://127.0.0.1:8000/ rec --serve SITE --bundle mspaint.tar
# 2. eject: the bundle, the units it used, the runtime, a build step
gexc eject rec --out mspaint-project --web-src web --unit-cache UNITS --features -compositor
# 3. build: the page with those features, the site's inputs in build/web
mspaint-project/build.sh
# 4. the static site
node web/eject-site.mjs mspaint-project site --strip --prune --dedupe
node tests/eject/w3-page.mjs site # the leg: it reaches "Untitled - Paint"
node tests/eject/w3-page.mjs site --placed # and Wine moves, sizes and stacks its windows
- The recording (
w3-session.json) is how the page was started (its__nt.session: exe, arguments, environment, working directory, size, page parameters) and what every Worker said in[gex-record]lines (gex.mjs): the processes (each one's main image and command line), the units each image used and the images that had none, and the native wasm32 modules (wine,winepe.wasm, wasm DLLs) by content key. - The project holds the recording (
session/), the bundle (bundle/rootfs.tar, hash checked), the units (units/KEY/), the browser shim, kernel and seed (runtime/web/gex/) andgex-project.json("kind": "w3").page_sourcenames the gex checkout'sweb/: the page is built at build time, because the features decide what goes into it. - The build (
gexc eject build) runsweb/build-eject-page.shwithGEX_FEATURES_OFF, puts the project's runtime wasm in it, drops what a feature off ships (Mesa forgl), checks the bundler really left out whatcompositoroff names, and writesbuild/web/(page/,rootfs.tar,units/,w3.json: the launch). - The site (
web/eject-site.mjs) is the page as plain files, the bundle and the units asFILE.zstwith a preload list,pack.json(their sizes and the build id), the launcher (which starts nt.html as recorded) and the hosting files (serve.mjs,_headers). There is no service worker: the host sends the isolation headers (COOPsame-origin, COEPrequire-corp), and nt.html (pack=) fetches each packed file and decodes it itself, straight into the one buffer that is then moved to pid 1's Worker, which lets go of it once the kernel has the files (moved on to a closedMessagePort:ArrayBuffer.prototype.transfer(0)alone leaves a buffer that arrived bypostMessageresident until the Worker collects). The first load is the only one, and one copy of the bundle is resident: the kernel's. The browser's HTTP cache does the caching (FILE.zst?v=BUILD).
Everything else is the page as gex runs it: several processes, the kernel, run-time translation behind the units.
The bundle split
A W3 bundle is a whole Wine prefix plus Wine itself: for Paint 2509 files
(447 MiB), of which a run to the window opens 221. user/eject.json
"bundle": "split" (the W3 template's setting; a config without it means
"whole") ships only those in the site's bundle:
- Recording: the kernel marks every file something opens
(
TmpFs::opened,k_fs_openedin shims/kernel), the page reads the set from a side Worker (__ntOpened(), web/src/fs-opened.js) and record-w3.mjs keeps it as the session'sfiles_read. - Build:
gexc eject buildputs that list in build/web/w3.json ("bundle": {"split": true, "read": [...]}); a split without a read set (an older recording) fails the build and says so. - Site: web/eject-site.mjs keeps the read files (and the loader) in
rootfs.tar and makes every other file a lazy file (nt-page.js
lazy=): stored once by content asproject/build/web/lazy/SHA, listed with its size and time inlazy.json. A guest that reads one gets it from the host in 1 MiB ranges, synchronously, the first time; the site's serve.mjs answers ranges (a host without them still works, sending the whole file per read). A lazy file's time is the bundle's (mtimes=1: Wine's wineboot rebuilds a prefix whose wine.inf looks newer than its install).
Nothing the program needs is lost: what the recording did not open is a fetch away. What it costs is a wait the first time an unrecorded path reads a file (Paint's Ctrl+P reads winspool.drv and compstui.dll that way). Record the paths users take (for now: run them before the recorder's settle ends) to bring them into the bundle.
Paint (the same recording ejected twice, --strip --prune --dedupe;
worldmachine, bench, headless Chromium, 3 alternated runs each):
| whole | split | |
|---|---|---|
| download | 76.5 MB | 54.2 MB (29% less) |
| to the window | 22.0, 22.0, 22.0 s | 21.0, 20.9, 21.1 s (1.0 s sooner) |
| browser footprint at settle | 3.39-3.41 GiB | 3.01-3.05 GiB (0.37 GiB less) |
| lazy reads to the window | 0 |
Limits
- Dynamic linking for the recorded program only. An
execveof a static program the session also ran works (its image module is found by identity); an exec'd dynamic program gets no closure modules, so its interpreter's first block misses loudly. - Import binding is PLT stubs. A call through the GOT without a PLT
(
-fno-plt) or a function pointer loaded from the GOT is not an import binding's; bind the definition (or the library) for those. - A library swap keeps the library loaded, so
ld.socan resolve against it and its initialisers run. - One ISA per gexc, as everywhere in gex: eject a session with the gexc
that ran it (
gexcfor AArch64, thex86build for x86-64 and i386). - The native runtime is the whole gexc. The syscall list and the single-process rule are enforced; the code for the rest is still there.
- Replacement calls go through the host (natively a host call, in the browser a JS call), not a direct wasm call: one round trip per call. Fine for library-sized work, not for a function called in a tight loop.
- Distributing a project is distributing the program's code and data (and
its libraries'): that is the user's licensing responsibility.
gexc ejectsays so and never publishes anything.
Status
- v0 (M4): eject, build, run; library replacement by symbol.
- v1 (M5): overrides by offset with a bytes hash and stale detection;
import and whole-library binding; the web build and launchers; the
runtime-level slim runtime;
--jit-fallback(off by default). gexc eject decompile(M5 item 5): the function tier's C as an editable override (the"entry"ABI).- Features (
user/eject.json"features") and W3 pages:compositorandglswitch; the rest wait for gex-kernel and shim feature gating. - The W3 bundle split (
"bundle": "split"): the files the recorded run opened in the bundle, the rest lazy (Paint: 29% less download, 0.37 GiB less memory, 1.0 s sooner to the window). Not yet: units of only the functions a recording ran (the units are most of what is left).