mirror of
https://github.com/HeyPuter/gex.git
synced 2026-11-13 20:12:43 -10:00
Merge merge/wine-fork 414f43dc: real fork() for native wasm32 programs (asyncify), wasm32 programs in the browser, busybox forking build
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01PeRutwFgZ66XpxjasKrPn1
This commit is contained in:
commit
e3917bac4b
20 files changed
+882
-52
No files matched your search
+125
-24
@@ -413,9 +413,82 @@ musl declares or converts:
|
||||
`CLONE_VM|CLONE_VFORK`) is a new process, a copy of the address space,
|
||||
starting at the same thread entry. The native shim makes it inside the
|
||||
syscall (the parent's wasm frames cannot be resumed elsewhere).
|
||||
- **Plain `fork()`** is unsupported (no stack to resume on).
|
||||
- **`fork()`** (and `vfork()`, which is `fork`) works by unwinding and
|
||||
rewinding the wasm stack (§3.11.1).
|
||||
- **`execve`** of another wasm32 container works.
|
||||
|
||||
#### 3.11.1 fork (2026-10-05)
|
||||
|
||||
gex cannot copy a wasm call stack: its frames are the engine's, not
|
||||
linear memory. A program that links `fork` is instrumented after the
|
||||
link with binaryen's **asyncify** (`tools/wine-wasm/wcc`), which lets
|
||||
every function that can reach the `gex.fork_point` import save its locals
|
||||
and call position to a buffer in linear memory, return, and later be
|
||||
re-entered and resume from it. That is how WASIX forks. JSPI does not do
|
||||
it: it suspends one stack in place inside one instance and cannot
|
||||
duplicate it into another instance or Worker, and wasmtime has no
|
||||
equivalent.
|
||||
|
||||
- **musl** (`src/process/wasm32/__gex_fork.c`; `arch/wasm32/syscall_arch.h`
|
||||
sends `_Fork`'s constant `__syscall(SYS_fork)` there): every thread's
|
||||
entry runs its function under a driver (`__gex_fork_run`, not
|
||||
instrumented). `__gex_fork` maps a 1 MiB record (asyncify's `{cur, end}`
|
||||
buffer, the C stack pointer) and calls `gex.fork_point`, where the host
|
||||
starts the unwind; every frame up to the driver saves itself and
|
||||
returns. The driver stops the unwind, notes its own function in the
|
||||
record and makes the child with `clone(SIGCHLD)` (a new process: a copy
|
||||
of the address space, record included) whose thread starts at the
|
||||
thread entry and calls `__gex_fork_child`. Both start the rewind and
|
||||
call the driver's function again; each frame restores itself and calls
|
||||
its callee again down to `gex.fork_point`, where the host stops the
|
||||
rewind. `fork` returns the pid in the parent and 0 in the child, the C
|
||||
stack pointer as it was. Only the calling thread exists in the child.
|
||||
- **The host** (native `libprov.rs`, browser `#wasmprogImports`) gives the
|
||||
program `gex.asyncify(op, arg)`, asyncify's exports on the calling
|
||||
instance, and `gex.fork_point(rec)` (start unwind, or stop rewind). The
|
||||
copy is the kernel's ordinary fork of a process; in the browser the
|
||||
child's Worker installs the same module over the parent's data.
|
||||
- **wcc:** asyncify instruments only the functions `forkreach.py` finds can
|
||||
reach `gex.fork_point` by a type-correct call graph (an indirect call of
|
||||
type T reaches the table's functions of type T; any other traps), not
|
||||
asyncify's default of everything that makes an indirect call. busybox:
|
||||
403 KB uninstrumented, 638 KB with asyncify's own analysis, 540 KB with
|
||||
the type-based one. asyncify cannot instrument `try_table` or tail calls,
|
||||
so Unix-side code lowers setjmp onto the legacy exception instructions and
|
||||
wcc translates them to the standard ones after (`--translate-to-exnref`),
|
||||
and no wasm32 code uses tail calls (the libc is shared). A program that
|
||||
never forks is not asyncified (only translated).
|
||||
- **Tests:** `tests/wasm32/fork.c` (fork without exec, separate memory;
|
||||
from 20 frames deep through a function pointer, every frame's locals
|
||||
intact; in a threaded program), busybox as an MMU build (hush forks for
|
||||
subshells, pipelines and `$(...)`), natively (`run.sh`, `busybox.sh`) and
|
||||
in the browser (`run-browser.sh`).
|
||||
|
||||
#### 3.11.2 What the musl fork changes, and why
|
||||
|
||||
The user would rather have no musl fork; it is kept to an architecture
|
||||
port. Everything below is a file of the `wasm32` arch (new files, which
|
||||
upstream musl's layout already provides for), except two one-line changes
|
||||
to shared files. `git diff 0784374d` (musl 1.2.5) in `wt/musl-wasm` is the
|
||||
whole of it.
|
||||
|
||||
| where | what | why |
|
||||
|---|---|---|
|
||||
| `configure` (1 line) | `wasm32*) ARCH=wasm32` | the target triple names the arch |
|
||||
| `include/sys/epoll.h` (1 line) | `epoll_event` packed on wasm32 too | the i386 kernel's 12-byte layout; wasm32 would pad it to 16 |
|
||||
| `arch/wasm32/*` | types, the i386 syscall numbers, `syscall_arch.h` (syscalls through `gex.syscall(ctx)`, signal frames at the return, `SYS_fork` to `__gex_fork`), atomics, `kstat` | the port itself |
|
||||
| `crt/wasm32/crt1.c` | entry from gex's `__gex_start`; main under fork's driver; `_init` runs `__wasm_call_ctors` | a wasm module has no `_start` gex can use, no `.init_array` |
|
||||
| `src/internal/wasm32/gex_thread.s` | `__gex_start` / `__gex_thread_start` (F_body entries), per-thread globals, C stack pointer get/set, handler calls | wasm has no registers to keep these in |
|
||||
| `src/thread/wasm32/*` | `__clone` (thread entry, new process), the child's start, thread area, `__unmapself`, `syscall_cp` | wasm has no stack switching or `clone` asm |
|
||||
| `src/env/wasm32/*` | TLS (wasm-ld's `__tls_base`), `main(void)` fallback | wasm-ld's TLS model |
|
||||
| `src/process/wasm32/*` | fork (asyncify), vfork as fork | §3.11.1 |
|
||||
| `src/setjmp/wasm32/*`, `src/signal/wasm32/*` | the SjLj runtime clang's lowering calls (`__wasm_setjmp`, `__wasm_longjmp`, the tag); signal frames, restorers | wasm EH, the signal model above |
|
||||
| `src/ldso/wasm32/*` | `dlopen`/`dlsym` over the closed-world registry | §3.12 |
|
||||
|
||||
Removed 2026-10-05: the vfork emulation (a `vfork` macro in `unistd.h`,
|
||||
`execve`/`_Exit` overrides) and the constructor call patched into
|
||||
`__libc_start_main`.
|
||||
|
||||
**Signals, the one control-flow difference.** gex cannot interrupt wasm
|
||||
code or move its PC, so signals are delivered at syscall returns:
|
||||
- The kernel builds the ordinary i386 frame in the context: `esp` = the
|
||||
@@ -1073,35 +1146,35 @@ Steps:
|
||||
natively instead of translated. Map `/bin/sh` and the coreutils names to
|
||||
it in the guest root (natively by a gexc option, in the page's bundle).
|
||||
Biggest win per effort: every script exec today translates dash/coreutils.
|
||||
**State (2026-10-05, merge/wine-unix):** busybox 1.37.0 builds with
|
||||
`tools/wine-wasm/build-busybox.sh OUT` (config `busybox.config`: NOMMU,
|
||||
LFS, hush as sh, ~50 applets incl. sed/grep/awk/find/xargs, fancy tail;
|
||||
~400 KB of wasm). `gexc run --busybox B ...` mounts B read-only at
|
||||
**State (2026-10-05, merge/wine-fork):** busybox 1.37.0 builds with
|
||||
`tools/wine-wasm/build-busybox.sh OUT` (config `busybox.config`: an MMU
|
||||
build with real fork (§3.11.1), LFS, hush as sh, ~50 applets incl.
|
||||
sed/grep/awk/find/xargs, fancy tail; 540 KB of wasm). `gexc run --busybox B ...` mounts B read-only at
|
||||
`/bin/<applet>` and `/usr/bin/<applet>` for every applet
|
||||
(`BUSYBOX_APPLETS`; `GEX_BUSYBOX_APPLETS` overrides) and runs B for a
|
||||
program named `/bin/sh` etc.; `RunOptions.exe` makes `/proc/self/exe`
|
||||
busybox, which hush re-executes for `$(...)` and subshells.
|
||||
busybox. In the browser, `run-node.mjs --busybox B` bundles B as
|
||||
`/bin/busybox` hard-linked at the same names (`busybox.applets`, one
|
||||
list for both).
|
||||
tests/wasm32/busybox.sh (needs `BUSYBOX`) compares pipelines, command
|
||||
substitution, redirections, externals and exit statuses with the host
|
||||
sh, and runs busybox under a translated x86-64 shell.
|
||||
- **vfork** (musl fork, `src/process/wasm32/__gexw_vfork.c`): gex cannot
|
||||
fork a wasm32 program, so `vfork()` is a setjmp macro (unistd.h); the
|
||||
"child" runs on the parent's stack with fds, cwd, umask, signal mask
|
||||
and actions snapshotted; its `execve` is a posix_spawn of that state
|
||||
(clone(VM|VFORK) is a new process) and its `_exit` spawns
|
||||
`/proc/self/exe` with `__GEXW_VFORK_EXIT=N` to exit with the status;
|
||||
then the parent's state is restored and vfork returns the pid. A child
|
||||
that does more than dup/close/chdir/signals before exec (hush's NOMMU
|
||||
paths re-exec, so busybox does not) would leak its changes into the
|
||||
parent's memory, as a real vfork would.
|
||||
- musl's `__libc_start_main` now calls `__wasm_call_ctors` (constructors
|
||||
never ran on wasm32 before).
|
||||
- **fork** is real (§3.11.1); the earlier vfork emulation (a setjmp
|
||||
macro, exec and _exit as posix_spawn) is gone.
|
||||
- The program's constructors run (crt1's `_init` calls
|
||||
`__wasm_call_ctors`; they never ran on wasm32 before).
|
||||
- Fixed on the way: a wasm32 child switched the host-wide translator
|
||||
compat (i386) mode, breaking an x86-64 parent after its wait
|
||||
(`wasmprog::is_wasm32`). A translated *i386* child of an x86-64 parent
|
||||
still has that limitation (documented in `run_program_with`).
|
||||
- Not yet: the page bundle's `/bin/sh` mapping (browser `--busybox`),
|
||||
and the prefix scripts' use of it.
|
||||
- Browser: native wasm32 programs run in the browser runtime
|
||||
(`shims/browser/src/cgen/wasmprog.rs`): their system calls never give
|
||||
the stack back except to end the program's frames, so a blocking call
|
||||
parks the Worker in place, and a thread or a child is staged and handed
|
||||
to the host from the program's `gex.syscall`. A wait on the page itself
|
||||
(its keyboard, the Worker's own network) is polled, since the frames
|
||||
hold the Worker's event loop.
|
||||
- Not yet: the prefix scripts' use of it.
|
||||
2. **ELF export providers** for glibc's hot entry points (`memcpy`/`memset`/
|
||||
`str*`, `malloc` family, libm), the PE providers' model on ELF images;
|
||||
needs gex-cgen's provider binding by ELF symbol (gex-cgen re-keys units:
|
||||
@@ -1134,7 +1207,7 @@ Steps:
|
||||
| gex side (this plan, tools/wine-wasm, shim, tests) | `wt/wine`, branch `merge/wine-wasm` |
|
||||
| wasmtime 49 upgrade (rustc 1.96 pinned) | `wt/wasmtime49`, branch `merge/wasmtime49` |
|
||||
| Wine fork | `wt/wine-wasm`, branch `wasm32`, remote `mw` |
|
||||
| musl fork | `wt/musl-wasm`, branch `gex-wasm32`, remote `mw` |
|
||||
| musl fork | `wt/musl-wasm`, branch `gex-wasm32`, remote `mw`; patches: §3.11.2 |
|
||||
| Wine 11.15 reference | `wt/nt-wine-src` (read-only) |
|
||||
|
||||
**Rebuilding the outputs** (`$W` = stream W's scratch directory):
|
||||
@@ -1211,9 +1284,37 @@ Steps:
|
||||
- W's check prefixes: `$W/pfx-native` (3 DLLs), `$W/pfx-dev` (9),
|
||||
`$W/pfx-gui` (11); `$W/pfx-w32u` is the coordinator's gate copy (do not
|
||||
restage while a gate runs). `$W/check.sh` runs them all.
|
||||
- §5.1 item 1 (busybox as /bin/sh, natively) on merge/wine-unix; next:
|
||||
the browser mapping, then item 2 (ELF export providers: own branch,
|
||||
tell the coordinator before gex-cgen re-keys).
|
||||
- §5.1 item 1 (busybox as /bin/sh) natively on merge/wine-unix; real
|
||||
fork (§3.11.1) and wasm32 programs in the browser on merge/wine-fork
|
||||
(musl `wt/musl-fork`, branch `gex-wasm32-fork` until gex-wasm32 takes
|
||||
it; sysroot `$W/sr-fork`, busybox `$W/bb-fork`, prefix
|
||||
`$W/pfx-gui-fork` built with that toolchain; `$W/check-fork.sh`).
|
||||
**Resume (2026-10-05, after the restart):** merge/wine-fork (off
|
||||
dbabed6b) has real fork natively and in the browser, wasm32 programs in
|
||||
the browser, run-node `--busybox`, and the docs (§3.11.1 design,
|
||||
§3.11.2 patch list). `$W/check-fork.sh` ran to the end: nt --no-cgen
|
||||
77/77 on x86, pfx-native, pfx-dev, pfx-gui, pfx-gui-fork; cgen-prefix
|
||||
77/77 (x86, pfx-native); tests/wasm32 incl. fork and busybox; dll
|
||||
abi/shared; browser nt x86 49/49, pfx-gui 23/23; wasm32 in the browser
|
||||
(fork, signals, sjlj, threads, busybox). Fixed after the restart:
|
||||
pelink translates legacy EH in winepe.wasm (winebuild's glue is
|
||||
compiled as Unix-side code; pfx-gui-fork failed every x86 test), and
|
||||
build-sysroot.sh reconfigures when the musl tree changes (`$W/sr-fork`
|
||||
had been configured against wt/musl-wasm). **Gate artifacts:**
|
||||
`$W/sr-gate` (fresh, from wt/musl-fork 155f77bf, binaryen 133) and
|
||||
`$W/bb-gate` (busybox built with it); master's gate keeps
|
||||
`$W/sysroot` and `$W/busybox`. Next steps:
|
||||
1. gate (coordinator), then merge master again once F+E land.
|
||||
2. When it lands: fast-forward musl's gex-wasm32 to gex-wasm32-fork and
|
||||
push.
|
||||
3. musl patch reduction (the user's standing goal, §3.11.2): only two
|
||||
shared-file lines remain (configure, epoll.h); look at whether the
|
||||
SjLj runtime, dlopen registry and signal frames can live outside musl
|
||||
(a gex wasm32 support library linked by wcc) without jank. musl's
|
||||
generic dlopen/__dlsym are weak stubs, so a library linked before
|
||||
libc can replace them.
|
||||
4. Then §5.1 item 2 (ELF export providers: own branch, tell the
|
||||
coordinator before gex-cgen re-keys).
|
||||
- wine-w32's Makefile now uses wt/wine-crt's wcc (sed); merge into
|
||||
merge/wine-wasm after its gate.
|
||||
- **CRTs (plan, kept for reference):** native msvcrt needs an i386-semantics `except_wasm32.c`
|
||||
|
||||
@@ -1861,6 +1861,7 @@ export class Gex {
|
||||
child.brk ?? 0n,
|
||||
);
|
||||
if (child.compat) e.gex_set_compat?.(1);
|
||||
if (child.wasm32) e.gex_wasmprog_adopt?.(BigInt(child.wasm32));
|
||||
// The parent's block numbering carries on in the child (its low region),
|
||||
// over slot maps of its own (`dense::child`).
|
||||
this.#dense = !!(child.opts ?? {}).dense && !!this.#guest.exports.blocks && !!e.gex_set_dense;
|
||||
@@ -2106,6 +2107,9 @@ export class Gex {
|
||||
// has never filled. Catching up on the next trap raced the branch and
|
||||
// deadlocked; doing it here once makes the first run deterministic.
|
||||
await this.#catchUp();
|
||||
// And the function table's installs (a wasm32 program's module, units):
|
||||
// its entry is a bridge into one.
|
||||
this.#cgenCatchUp();
|
||||
return { entry, ctx: Number(thread.ctx) };
|
||||
}
|
||||
|
||||
@@ -2123,6 +2127,8 @@ export class Gex {
|
||||
// A 32-bit process forks a 32-bit child; its Worker's shim must decode
|
||||
// in compatibility mode from the first block.
|
||||
compat: e.gex_compat?.() ?? 0,
|
||||
// A wasm32 program's entry: the child installs the same module.
|
||||
wasm32: e.gex_wasmprog_entry?.() ?? 0n,
|
||||
// Where the parent's heap had reached. Without the number the child's
|
||||
// kernel thinks it has no heap and grows one from zero, over its contexts.
|
||||
brk: e.gex_fork_brk?.() ?? 0n,
|
||||
@@ -2499,6 +2505,8 @@ export class Gex {
|
||||
// Wine's PE module (native wasm32 Wine, cgen/wasmdll.rs): its bytes are
|
||||
// in the shim's heap, read from the prefix.
|
||||
const wasmdll = prov >= 0 && (e.gex_cgen_pmod_wasm_len?.(prov) >>> 0) > 0;
|
||||
// A native wasm32 program (cgen/wasmprog.rs): its bytes come the same way.
|
||||
const wasmprog = wasmdll && !!e.gex_cgen_pmod_wasmprog?.(prov);
|
||||
const mod = wasmdll
|
||||
? this.#cgenFileModule(this.#cgenStr(e.gex_cgen_pmod_file, prov),
|
||||
new Uint8Array(this.#memory.buffer, e.gex_cgen_pmod_wasm_ptr(prov) >>> 0, e.gex_cgen_pmod_wasm_len(prov) >>> 0).slice())
|
||||
@@ -2522,7 +2530,8 @@ export class Gex {
|
||||
// A unit's functions never use clang's shadow stack (those are stubs),
|
||||
// but a position-independent module imports its pointer all the same.
|
||||
// A stateful provider's is this Worker's (one C stack per thread).
|
||||
__stack_pointer: memBase ? this.#providerStack() : new WebAssembly.Global({ value: 'i32', mutable: true }, 0),
|
||||
// A wasm32 program sets its own at each thread's entry.
|
||||
__stack_pointer: memBase && !wasmprog ? this.#providerStack() : new WebAssembly.Global({ value: 'i32', mutable: true }, 0),
|
||||
},
|
||||
gex: {
|
||||
...this.#imports, lookup: st.lookup, ftab: st.ftab,
|
||||
@@ -2533,11 +2542,11 @@ export class Gex {
|
||||
nt_qpc: e.gex_nt_qpc,
|
||||
provider_alloc_top: e.gex_provider_alloc_top,
|
||||
} : {}),
|
||||
...(memBase ? this.#providerImports() : {}),
|
||||
...(wasmdll ? { syscall: this.#nativeSyscall } : {}),
|
||||
...(memBase && !wasmprog ? this.#providerImports() : {}),
|
||||
...(wasmprog ? this.#wasmprogImports(() => inst) : wasmdll ? { syscall: this.#nativeSyscall } : {}),
|
||||
},
|
||||
});
|
||||
if (memBase && e.gex_provider_relocate(memBase)) inst.exports.__wasm_apply_data_relocs?.();
|
||||
if (wasmprog ? e.gex_wasmprog_relocate(prov) : memBase && e.gex_provider_relocate(memBase)) inst.exports.__wasm_apply_data_relocs?.();
|
||||
const n = regsOf(0, 0) >>> 0;
|
||||
const buf = e.gex_alloc(4 * Math.max(1, n)) >>> 0;
|
||||
let regs;
|
||||
@@ -2758,6 +2767,53 @@ export class Gex {
|
||||
}
|
||||
}
|
||||
|
||||
/** What a native wasm32 program imports from `gex` (cgen/wasmprog.rs):
|
||||
* its system calls, which return unless the program's frames end (a
|
||||
* thread or a child they staged is handed to the host here), and fork's
|
||||
* asyncify operations on its own instance (the musl fork's
|
||||
* src/process/wasm32/__gex_fork.c). */
|
||||
#wasmprogImports(inst) {
|
||||
const e = this.#shim.exports;
|
||||
const ops = ['asyncify_get_state', 'asyncify_start_unwind', 'asyncify_stop_unwind', 'asyncify_start_rewind', 'asyncify_stop_rewind'];
|
||||
const asyncify = (op, arg) => {
|
||||
const f = inst().exports[ops[op]];
|
||||
if (!f) {
|
||||
if (op === 0) return 0;
|
||||
throw new Error(`gex.asyncify: the program has no ${ops[op]} (fork needs wcc's asyncify)`);
|
||||
}
|
||||
if (op === 0) return f();
|
||||
if (op === 1 || op === 3) f(arg);
|
||||
else f();
|
||||
return 0;
|
||||
};
|
||||
return {
|
||||
syscall: (ctx, pc) => {
|
||||
e.gex_syscall(ctx, pc);
|
||||
if (e.gex_wasmprog_staged()) {
|
||||
this.#drainFork().catch((err) => console.error(`[gex] starting a process: ${err.message ?? err}`));
|
||||
this.#drainThread().catch((err) => console.error(`[gex] starting a thread: ${err.message ?? err}`));
|
||||
}
|
||||
},
|
||||
asyncify,
|
||||
fork_point: (rec) => {
|
||||
const st = asyncify(0, 0);
|
||||
if (st === 0) asyncify(1, rec);
|
||||
else if (st === 2) asyncify(4, 0);
|
||||
else throw new Error(`gex.fork_point in asyncify state ${st}`);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/** A native wasm32 program the shim staged (a load, an exec, a fork's
|
||||
* child): published and installed here before the guest runs. */
|
||||
#wasmprogServe() {
|
||||
const e = this.#shim.exports;
|
||||
const n = e.gex_wasmprog_serve?.() ?? -1;
|
||||
if (n < 0) return;
|
||||
this.#cgenCatchUp();
|
||||
e.gex_cgen_pmod_installed(n);
|
||||
}
|
||||
|
||||
/** Wine's PE module's `gex.syscall` (cgen/wasmdll.rs): a system call from
|
||||
* native code (gexrt marks its pc, bit 61) that traps to JS -- a unit to
|
||||
* install, a thread to start -- is served here and returns, so the native
|
||||
@@ -2798,6 +2854,7 @@ export class Gex {
|
||||
* its entry list does. */
|
||||
#cgenServe() {
|
||||
const e = this.#shim.exports;
|
||||
this.#wasmprogServe();
|
||||
// A Worker with the function JIT on but no units of its own still
|
||||
// replays the function table's log.
|
||||
if (!this.#cgenMode && e.gex_fjit_on?.()) {
|
||||
|
||||
@@ -7,6 +7,9 @@
|
||||
// --file <guest>=<host> bundle a host file into the guest's filesystem
|
||||
// --dir <guest>=<host> bundle a whole directory tree
|
||||
// --extract <guest>=<host> write a file back out when the program ends
|
||||
// --busybox <host> a wasm32 busybox as /bin/busybox, hard-linked
|
||||
// as /bin/<applet> and /usr/bin/<applet> for
|
||||
// tools/wine-wasm/busybox.applets
|
||||
// --argv0 <name> what the guest sees as argv[0]
|
||||
// --cwd <dir> the directory the guest starts in
|
||||
// --env <KEY=VALUE> an environment variable (repeatable); none
|
||||
@@ -53,6 +56,9 @@ let cwd = null;
|
||||
const extract = [];
|
||||
let argv0 = null;
|
||||
const env = [];
|
||||
// Hard links (guest path -> the bundled file it names) and modes.
|
||||
const hard = {};
|
||||
const modes = {};
|
||||
let cgenMode = 0;
|
||||
let noJit = false;
|
||||
let aotRun = false;
|
||||
@@ -129,6 +135,14 @@ const rest = [];
|
||||
const eq = spec.indexOf('=');
|
||||
if (eq <= 0) { console.error('--extract wants <guest>=<host>'); process.exit(2); }
|
||||
extract.push([spec.slice(0, eq), spec.slice(eq + 1)]);
|
||||
} else if (av[i] === '--busybox') {
|
||||
const host = av[++i] ?? '';
|
||||
files['/bin/busybox'] = readFileSync(host);
|
||||
modes['/bin/busybox'] = 0o755;
|
||||
const list = readFileSync(new URL('../../../tools/wine-wasm/busybox.applets', import.meta.url), 'utf8');
|
||||
for (const name of list.split('\n').filter(Boolean)) {
|
||||
for (const dir of ['/bin', '/usr/bin']) hard[`${dir}/${name}`] = '/bin/busybox';
|
||||
}
|
||||
} else if (av[i] === '--cwd') cwd = av[++i];
|
||||
else if (av[i] === '--env') env.push(av[++i] ?? '');
|
||||
else if (av[i] === '--strace') strace = true;
|
||||
@@ -475,6 +489,8 @@ try {
|
||||
cwd,
|
||||
env: env.length ? env : undefined,
|
||||
dirs,
|
||||
hard,
|
||||
modes,
|
||||
// GEX_PINS names a layout written by `gexc build --closure`. Every
|
||||
// object in it is placed exactly where the build assumed, which is
|
||||
// what makes a module built against those addresses correct -- and it
|
||||
|
||||
@@ -36,6 +36,7 @@ use std::sync::atomic::{AtomicU32, Ordering};
|
||||
use std::sync::{Arc, Mutex, OnceLock};
|
||||
|
||||
mod wasmdll;
|
||||
pub(crate) mod wasmprog;
|
||||
|
||||
/// 0 off, 1 use a unit where one exists, 2 also refuse a program image
|
||||
/// without one (`--cgen`, as natively).
|
||||
|
||||
@@ -213,15 +213,21 @@ fn bind(install: u32, pairs: &[(u64, u32)]) {
|
||||
lock(&BINDS).push(Bind { regs, dirs, slots });
|
||||
}
|
||||
|
||||
/// Provider module `number`'s bytes when it is Wine's PE module (in the
|
||||
/// shim's heap), else 0.
|
||||
/// Provider module `number`'s bytes when it is Wine's PE module or a
|
||||
/// wasm32 program (in the shim's heap), else 0.
|
||||
#[no_mangle]
|
||||
pub extern "C" fn gex_cgen_pmod_wasm_ptr(number: u32) -> u32 {
|
||||
if let Some((p, _)) = super::wasmprog::wasm(number) {
|
||||
return p;
|
||||
}
|
||||
lock(&WINE_PE).as_ref().filter(|w| w.number == number).map_or(0, |w| w.wasm.as_ptr() as u32)
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn gex_cgen_pmod_wasm_len(number: u32) -> u32 {
|
||||
if let Some((_, n)) = super::wasmprog::wasm(number) {
|
||||
return n;
|
||||
}
|
||||
lock(&WINE_PE).as_ref().filter(|w| w.number == number).map_or(0, |w| w.wasm.len() as u32)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
//! Native wasm32 programs in the browser (the native host's `wasmprog`,
|
||||
//! docs/wine-native.md §3.11): a Linux program compiled to wasm32 against
|
||||
//! gex's musl, in an ELF32 container whose header (0x40 bytes below the
|
||||
//! entry) names the module. Nothing is translated: the module is installed
|
||||
//! as a stateful provider (`PROVIDER | number` in the function table) at
|
||||
//! the program's two entries, its data at the header's `__memory_base`.
|
||||
//!
|
||||
//! The protocol with `gex.mjs`:
|
||||
//! - `gex_load`, an `execve` and a fork's child stage the program
|
||||
//! (`stage`); `gex_wasmprog_serve()` (JS, before the guest runs) publishes
|
||||
//! it and returns its provider module number, or -1;
|
||||
//! - JS catches the table's log up, which instantiates the module from
|
||||
//! `gex_cgen_pmod_wasm_ptr/len` with `gex.syscall` = `#wasmprogSyscall`
|
||||
//! and asyncify's `gex.asyncify` / `gex.fork_point` (fork,
|
||||
//! `src/process/wasm32/__gex_fork.c` in the musl fork), and applies its
|
||||
//! data relocations where `gex_wasmprog_relocate` says (once, for a
|
||||
//! fresh image: not in a fork's child, whose data is the parent's);
|
||||
//! - its system calls never trap back to JS except to end the program's
|
||||
//! frames (exit, exec, a fatal signal): a wait parks the Worker in place
|
||||
//! (`lib.rs`, `wasm32_wait`), a thread or a fork is staged and returns,
|
||||
//! and `#wasmprogSyscall` hands it to the host.
|
||||
|
||||
use super::*;
|
||||
|
||||
struct Prog {
|
||||
number: u32,
|
||||
wasm: Vec<u8>,
|
||||
relocate: bool,
|
||||
}
|
||||
|
||||
static PROGS: Mutex<Vec<Prog>> = Mutex::new(Vec::new());
|
||||
/// The program to publish (entry, whether its data is fresh).
|
||||
static STAGED: Mutex<Option<(u64, bool)>> = Mutex::new(None);
|
||||
/// This process's program's entry while it is a wasm32 one, for a fork.
|
||||
static CURRENT: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
|
||||
|
||||
pub(crate) const MAGIC: &[u8; 8] = b"GEXWASM\0";
|
||||
|
||||
/// Whether `program` is a native wasm32 one (its header, from the image's
|
||||
/// segments).
|
||||
pub(crate) fn is_wasm32(program: &gex_ld::Program) -> bool {
|
||||
let at = program.exe.entry.wrapping_sub(0x40);
|
||||
program.exe.segments.iter().any(|s| {
|
||||
at >= s.vaddr && at + 8 <= s.vaddr + s.bytes.len() as u64 && {
|
||||
let o = (at - s.vaddr) as usize;
|
||||
&s.bytes[o..o + 8] == MAGIC
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// The process's image changed (load, exec): `program` is published before
|
||||
/// it runs if it is a wasm32 one.
|
||||
pub(crate) fn stage(program: Option<&gex_ld::Program>) {
|
||||
CURRENT.store(0, Ordering::Relaxed);
|
||||
*lock(&STAGED) = program.filter(|p| is_wasm32(p)).map(|p| (p.exe.entry, true));
|
||||
}
|
||||
|
||||
/// A fork's child of a wasm32 program at `entry`: the same module, over the
|
||||
/// parent's data.
|
||||
#[no_mangle]
|
||||
pub extern "C" fn gex_wasmprog_adopt(entry: u64) {
|
||||
if entry != 0 {
|
||||
*lock(&STAGED) = Some((entry, false));
|
||||
}
|
||||
}
|
||||
|
||||
/// The running program's entry if it is a wasm32 one (the fork record's).
|
||||
#[no_mangle]
|
||||
pub extern "C" fn gex_wasmprog_entry() -> u64 {
|
||||
CURRENT.load(Ordering::Relaxed)
|
||||
}
|
||||
|
||||
/// Publish the staged program: its provider module number, or -1.
|
||||
#[no_mangle]
|
||||
pub extern "C" fn gex_wasmprog_serve() -> i32 {
|
||||
let Some((entry, relocate)) = lock(&STAGED).take() else { return -1 };
|
||||
match publish(entry, relocate) {
|
||||
Ok(n) => n as i32,
|
||||
Err(e) => {
|
||||
crate::warn_line(&format!("gex: wasm32 program: {e}\n"));
|
||||
crate::with_host(|h| crate::exit_process(h, 127));
|
||||
-1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn publish(entry: u64, relocate: bool) -> Result<u32, String> {
|
||||
let mem = crate::guest_bytes();
|
||||
let at = entry.wrapping_sub(0x40) as usize;
|
||||
let h = mem.get(at..at + 36).ok_or("header outside memory")?;
|
||||
if &h[..8] != MAGIC {
|
||||
return Err("no GEXWASM header".into());
|
||||
}
|
||||
let w = |o: usize| u32::from_le_bytes(h[o..o + 4].try_into().unwrap()) as u64;
|
||||
if w(8) != 1 {
|
||||
return Err(format!("header version {} (want 1)", w(8)));
|
||||
}
|
||||
let (wasm_at, wasm_len, mem_base, thread_entry) = (w(12), w(16), w(20), w(32));
|
||||
let wasm = mem.get(wasm_at as usize..(wasm_at + wasm_len) as usize).ok_or("module outside memory")?.to_vec();
|
||||
let names = gex_cgen::provider::slot_names(&wasm).map_err(|e| e.to_string())?;
|
||||
let slot = |n: &str| names.iter().position(|s| s.as_deref() == Some(n)).map(|k| k as u32).ok_or(format!("{n} is not in its table"));
|
||||
let pairs = vec![(entry, slot("__gex_start")?), (thread_entry, slot("__gex_thread_start")?)];
|
||||
let number = lock(&PROVIDERS).len() as u32;
|
||||
ftab(|m| m.publish(names.len() as u32, PROVIDER | number, Tier::Unit, &pairs));
|
||||
let addrs: Vec<u64> = pairs.iter().map(|&(a, _)| a).collect();
|
||||
for &a in &addrs {
|
||||
gex_kernel::ftab::fcache_clear(crate::guest_bytes(), a);
|
||||
}
|
||||
let (regs, mut dirs, slots) = bridges(&addrs);
|
||||
// Every entry's page in the directory, not only the pages numbered now: a
|
||||
// fork's child has the parent's numbering (`gex_adopt_page`) but may have
|
||||
// had its directory rebuilt without it (`gex_reset_directory`).
|
||||
crate::with_host(|h| {
|
||||
for &a in &addrs {
|
||||
let page = a / gex_abi::DISPATCH_PAGE;
|
||||
if let Some(b) = h.jit.page_table.lookup(page) {
|
||||
dirs.push((page, b));
|
||||
}
|
||||
}
|
||||
});
|
||||
let path = crate::with_host(|h| {
|
||||
// gex-kernel's wasm32 personality: signal frames stay in the context
|
||||
// for the program's syscall stub.
|
||||
h.kernel.wasm32 = true;
|
||||
h.kernel.exe_path.clone()
|
||||
});
|
||||
lock(&PROVIDERS).push(ProviderModule { file: format!("wasmprog:{path}"), regs, dirs, slots, memory_base: mem_base as u32 });
|
||||
lock(&PROGS).push(Prog { number, wasm, relocate });
|
||||
CURRENT.store(entry, Ordering::Relaxed);
|
||||
Ok(number)
|
||||
}
|
||||
|
||||
/// Provider module `number`'s bytes when it is a wasm32 program.
|
||||
pub(super) fn wasm(number: u32) -> Option<(u32, u32)> {
|
||||
lock(&PROGS).iter().find(|p| p.number == number).map(|p| (p.wasm.as_ptr() as u32, p.wasm.len() as u32))
|
||||
}
|
||||
|
||||
/// Whether provider module `number` is a wasm32 program.
|
||||
#[no_mangle]
|
||||
pub extern "C" fn gex_cgen_pmod_wasmprog(number: u32) -> i32 {
|
||||
lock(&PROGS).iter().any(|p| p.number == number) as i32
|
||||
}
|
||||
|
||||
/// Whether this instantiation of program `number` applies its data
|
||||
/// relocations: once, for a fresh image.
|
||||
#[no_mangle]
|
||||
pub extern "C" fn gex_wasmprog_relocate(number: u32) -> i32 {
|
||||
lock(&PROGS).iter_mut().find(|p| p.number == number).is_some_and(|p| std::mem::replace(&mut p.relocate, false)) as i32
|
||||
}
|
||||
@@ -3934,6 +3934,8 @@ pub extern "C" fn gex_load(ptr: u32, len: u32, argv: u32, argv_len: u32) -> u64
|
||||
},
|
||||
None => program.entry(),
|
||||
};
|
||||
// A native wasm32 program: published before it runs (cgen/wasmprog.rs).
|
||||
cgen::wasmprog::stage(nt_abi.is_none().then_some(&program));
|
||||
set_host(Host {
|
||||
magic_head: HOST_MAGIC,
|
||||
kernel,
|
||||
@@ -6391,6 +6393,12 @@ pub extern "C" fn gex_syscall(ctx: i32, pc: i64) {
|
||||
h.jit.ever_threaded = true;
|
||||
}
|
||||
stage_thread(tid, ctx_addr, start);
|
||||
if h.kernel.wasm32 {
|
||||
// A wasm32 program's frames cannot be left: the call
|
||||
// returns, and `#wasmprogSyscall` starts the thread.
|
||||
WASM32_STAGED.store(true, std::sync::atomic::Ordering::Relaxed);
|
||||
return 0;
|
||||
}
|
||||
// The stack goes back so the host can see the record and
|
||||
// start the Worker, exactly as a `fork` does. The parent
|
||||
// resumes where it was: `clone` returns the tid to it.
|
||||
@@ -6509,6 +6517,17 @@ pub extern "C" fn gex_syscall(ctx: i32, pc: i64) {
|
||||
child_stack,
|
||||
tls,
|
||||
} => {
|
||||
if h.kernel.wasm32 {
|
||||
// A wasm32 process clones at its thread entry (the
|
||||
// context's PC, as the musl fork's __clone leaves it);
|
||||
// the call returns, and `#wasmprogSyscall` hands the
|
||||
// child to the host.
|
||||
let start = gm.read_u64(kstate.ctx_addr + ctx::PC as u64).unwrap_or(0);
|
||||
do_fork(h, pid, pc as u64, child_stack, tls);
|
||||
FORK[1].store(start, std::sync::atomic::Ordering::Relaxed);
|
||||
WASM32_STAGED.store(true, std::sync::atomic::Ordering::Relaxed);
|
||||
return 0;
|
||||
}
|
||||
do_fork(h, pid, pc as u64, child_stack, tls);
|
||||
// Give the stack back though the parent carries on: the child
|
||||
// is a copy of this memory that must be taken now, when the
|
||||
@@ -6571,7 +6590,9 @@ pub extern "C" fn gex_syscall(ctx: i32, pc: i64) {
|
||||
recompile_objects();
|
||||
// 3 means the running thread gave up the wasm stack: pick another, waiting
|
||||
// first if it is waiting on another Worker (see `park_until_ready`).
|
||||
let stop = if stop == 3 {
|
||||
let stop = if stop == 3 && with_host(|h| h.kernel.wasm32) {
|
||||
wasm32_wait(pc as u64)
|
||||
} else if stop == 3 {
|
||||
// Ask first, wait second: most parked calls are already ready by now
|
||||
// (the pipe has room, the child has exited), and a readiness scan first
|
||||
// -- a call into the global kernel per shared handle -- cost more than
|
||||
@@ -6715,6 +6736,50 @@ const LOW_END: u64 = gex_abi::DISPATCH_DIR + gex_abi::DISPATCH_MAX_PAGES * 4;
|
||||
// parent's regions, `wait4`/`kill`/`exit` route through the kernel's table, and
|
||||
// each process is a Worker the engine schedules.
|
||||
|
||||
/// A thread or a fork a wasm32 program's syscall staged without trapping
|
||||
/// (`gex_wasmprog_staged`).
|
||||
static WASM32_STAGED: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
|
||||
|
||||
/// Whether a wasm32 program's last syscall staged a thread or a child for
|
||||
/// the host (and forget it).
|
||||
#[no_mangle]
|
||||
pub extern "C" fn gex_wasmprog_staged() -> i32 {
|
||||
WASM32_STAGED.swap(false, std::sync::atomic::Ordering::Relaxed) as i32
|
||||
}
|
||||
|
||||
/// A wasm32 program's blocking call (cgen/wasmprog.rs): its frames cannot be
|
||||
/// given back to JS, so the Worker waits here until the call can finish, and
|
||||
/// the call returns. Anything else that ends the wait (the process gone)
|
||||
/// traps as usual.
|
||||
fn wasm32_wait(pc: u64) -> i32 {
|
||||
let mut said = false;
|
||||
loop {
|
||||
let picked = schedule();
|
||||
let here = with_host(|h| h.threads[cur()].kstate.ctx_addr);
|
||||
if picked == 1 {
|
||||
if resume() == Some(pc + gex_abi::SYSCALL_INST_BYTES) && resume_ctx().unwrap_or(here) == here {
|
||||
set_resume_none();
|
||||
set_resume_ctx_none();
|
||||
return 0;
|
||||
}
|
||||
return picked;
|
||||
}
|
||||
if EXITED.load(std::sync::atomic::Ordering::Relaxed) || with_host(|h| matches!(h.threads[cur()].waiting, Waiting::Exited)) {
|
||||
return picked;
|
||||
}
|
||||
if !said && with_host(|h| h.threads.iter().any(|t| wait_needs_page(t, &h.kernel))) {
|
||||
// A wait on the page itself (its keyboard, the Worker's own
|
||||
// network) needs this Worker's event loop, which a wasm32
|
||||
// program's frames hold: it is polled here instead.
|
||||
warn_line("gex: a wasm32 program waits on the page (stdin or the network): polled\n");
|
||||
said = true;
|
||||
}
|
||||
if gex_park(20) < 0 {
|
||||
park_until_ready();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn do_fork(h: &mut Host, pid: i32, pc: u64, child_stack: u64, tls: Option<u64>) {
|
||||
let ctx_addr = h.threads[cur()].kstate.ctx_addr;
|
||||
// The parent carries straight on, keeping its memory, threads and tables:
|
||||
@@ -7627,6 +7692,9 @@ fn do_exec(h: &mut Host, path: &str, argv: &[String], envp: &[String]) -> u64 {
|
||||
h.kernel.compat = p.compat();
|
||||
gex_xlate::set_compat(p.compat());
|
||||
}
|
||||
// Set again when a wasm32 program is published (cgen/wasmprog.rs).
|
||||
h.kernel.wasm32 = false;
|
||||
cgen::wasmprog::stage(program.as_ref());
|
||||
h.kernel.live_threads = 1;
|
||||
h.kernel.next_ctx_slot = 1;
|
||||
// Signal handlers do not survive an exec; the mask does.
|
||||
|
||||
@@ -2216,13 +2216,10 @@ fn catch_up_table(store: &mut Store<HostState>) -> Result<()> {
|
||||
}
|
||||
|
||||
/// What `--busybox` presents (the applets tools/wine-wasm/busybox.config
|
||||
/// builds).
|
||||
pub const BUSYBOX_APPLETS: &[&str] = &[
|
||||
"sh", "ash", "ls", "cat", "sort", "head", "tail", "tr", "wc", "mkdir", "cp", "mv", "rm", "rmdir", "date",
|
||||
"uname", "sed", "grep", "egrep", "fgrep", "awk", "find", "xargs", "env", "id", "basename", "dirname", "echo",
|
||||
"printf", "test", "[", "expr", "cut", "touch", "ln", "chmod", "pwd", "sleep", "true", "false", "readlink",
|
||||
"realpath", "uniq", "tee", "stat", "dd", "seq", "yes", "printenv", "which",
|
||||
];
|
||||
/// builds; the list is tools/wine-wasm/busybox.applets, which the browser's
|
||||
/// run-node.mjs reads too).
|
||||
pub static BUSYBOX_APPLETS: std::sync::LazyLock<Vec<&'static str>> =
|
||||
std::sync::LazyLock::new(|| include_str!("../../../tools/wine-wasm/busybox.applets").lines().filter(|l| !l.is_empty()).collect());
|
||||
|
||||
/// Build the guest's namespace.
|
||||
///
|
||||
|
||||
@@ -410,6 +410,21 @@ pub(crate) fn register(linker: &mut Linker<HostState>) -> Result<()> {
|
||||
linker.func_wrap(IMPORT_MODULE, "provider_alloc_top", |mut caller: Caller<'_, HostState>, size: i32| -> wasmtime::Result<i32> { crate::host((|| -> anyhow::Result<i32> {
|
||||
Ok(alloc_top(caller.data_mut(), size as u32 as u64, "[provider heap]")? as i32)
|
||||
})()) })?;
|
||||
// fork in a native wasm32 program (the musl fork's
|
||||
// src/process/wasm32/__gex_fork.c, asyncified by wcc): asyncify's
|
||||
// exports on the calling instance, and the fork point itself, which
|
||||
// starts the unwind (normal state) or ends the rewind.
|
||||
linker.func_wrap(IMPORT_MODULE, "asyncify", |mut caller: Caller<'_, HostState>, op: i32, arg: i32| -> wasmtime::Result<i32> {
|
||||
crate::host(asyncify(&mut caller, op, arg))
|
||||
})?;
|
||||
linker.func_wrap(IMPORT_MODULE, "fork_point", |mut caller: Caller<'_, HostState>, rec: i32| -> wasmtime::Result<()> { crate::host((|| -> anyhow::Result<()> {
|
||||
match asyncify(&mut caller, 0, 0)? {
|
||||
0 => asyncify(&mut caller, 1, rec)?,
|
||||
2 => asyncify(&mut caller, 4, 0)?,
|
||||
s => bail!("gex.fork_point in asyncify state {s}"),
|
||||
};
|
||||
Ok(())
|
||||
})()) })?;
|
||||
// Diagnostics: text a provider writes to its stderr.
|
||||
linker.func_wrap(IMPORT_MODULE, "provider_log", |caller: Caller<'_, HostState>, ptr: i32, len: i32| -> wasmtime::Result<()> { crate::host((|| -> anyhow::Result<()> {
|
||||
if let Some(m) = caller.data().memory.as_ref() {
|
||||
@@ -423,6 +438,37 @@ pub(crate) fn register(linker: &mut Linker<HostState>) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// asyncify's export `op` on the calling instance: 0 `asyncify_get_state`
|
||||
/// (0 when the module is not asyncified), 1 `start_unwind(arg)`,
|
||||
/// 2 `stop_unwind`, 3 `start_rewind(arg)`, 4 `stop_rewind`.
|
||||
fn asyncify(caller: &mut Caller<'_, HostState>, op: i32, arg: i32) -> Result<i32> {
|
||||
let name = match op {
|
||||
0 => "asyncify_get_state",
|
||||
1 => "asyncify_start_unwind",
|
||||
2 => "asyncify_stop_unwind",
|
||||
3 => "asyncify_start_rewind",
|
||||
4 => "asyncify_stop_rewind",
|
||||
_ => bail!("gex.asyncify: no operation {op}"),
|
||||
};
|
||||
let Some(wasmtime::Extern::Func(f)) = caller.get_export(name) else {
|
||||
if op == 0 {
|
||||
return Ok(0);
|
||||
}
|
||||
bail!("gex.asyncify: the program has no {name} (fork needs wcc's asyncify)");
|
||||
};
|
||||
Ok(match op {
|
||||
0 => f.typed::<(), i32>(&*caller)?.call(&mut *caller, ())?,
|
||||
1 | 3 => {
|
||||
f.typed::<i32, ()>(&*caller)?.call(&mut *caller, arg)?;
|
||||
0
|
||||
}
|
||||
_ => {
|
||||
f.typed::<(), ()>(&*caller)?.call(&mut *caller, ())?;
|
||||
0
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// [`register`] for a 64-bit guest memory: the same functions with i64
|
||||
/// addresses, sizes and function-table indices (stream U, additive).
|
||||
fn register64(linker: &mut Linker<HostState>) -> Result<()> {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
#!/bin/sh
|
||||
# A wasm32 busybox as the guest's /bin/sh and coreutils (`gexc run
|
||||
# --busybox`): a script with pipelines, command substitution, redirections,
|
||||
# externals and a failing command, against the same script under the
|
||||
# host's sh. BUSYBOX names the binary (tools/wine-wasm/build-busybox.sh);
|
||||
# --busybox`): a script with pipelines, command substitution, subshells,
|
||||
# redirections, externals and a failing command (hush forks for them),
|
||||
# against the same script under the host's sh. BUSYBOX names the binary (tools/wine-wasm/build-busybox.sh);
|
||||
# without it this is a skip.
|
||||
#
|
||||
# tests/wasm32/busybox.sh GEXC
|
||||
@@ -21,6 +21,10 @@ for i in 1 2 3; do echo $i; done | tail -1
|
||||
awk "{n++} END {print n}" in.txt
|
||||
false || echo "false failed"
|
||||
/nonexistent-command 2>/dev/null; echo "status $?"
|
||||
(cd / && pwd); x=1; (x=2; echo "sub $x"); echo "top $x"
|
||||
{ echo a; echo b; } | sort -r | tr "\n" " "; echo
|
||||
echo $(echo $(echo nested))
|
||||
f() { echo "in f $1"; }; f one | cat
|
||||
echo done'
|
||||
native=$(sh -c "$script" 2>&1)
|
||||
gex=$("$GEXC" run --busybox "$BUSYBOX" /bin/sh -c "$script" 2>&1)
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
/* fork without exec: parent and child both go on, each with its own memory;
|
||||
* fork from deep in a call chain reached through a function pointer (locals
|
||||
* of every frame survive in both);
|
||||
* fork in a threaded program (only the calling thread is in the child). */
|
||||
#include <pthread.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/wait.h>
|
||||
#include <unistd.h>
|
||||
|
||||
static int shared_counter = 100;
|
||||
|
||||
static int status_of(pid_t pid)
|
||||
{
|
||||
int st;
|
||||
if (waitpid(pid, &st, 0) != pid) return -1;
|
||||
return WIFEXITED(st) ? WEXITSTATUS(st) : 128 + WTERMSIG(st);
|
||||
}
|
||||
|
||||
/* A call chain whose frames all hold live state across the fork. */
|
||||
static int deep(int depth, int acc, char *buf)
|
||||
{
|
||||
char local[32];
|
||||
snprintf(local, sizeof local, "d%d", depth);
|
||||
if (depth == 0) {
|
||||
pid_t pid = fork();
|
||||
if (pid < 0) return -1;
|
||||
if (pid == 0) {
|
||||
strcat(buf, "c");
|
||||
return acc + 1000;
|
||||
}
|
||||
int st = status_of(pid);
|
||||
strcat(buf, "p");
|
||||
return acc + st;
|
||||
}
|
||||
int r = deep(depth - 1, acc + depth, buf);
|
||||
/* the frame's own local, intact on the way back */
|
||||
if (strcmp(local + 1, "") == 0 || atoi(local + 1) != depth) return -9999;
|
||||
return r;
|
||||
}
|
||||
|
||||
static void *spin(void *arg)
|
||||
{
|
||||
for (;;) pause();
|
||||
return arg;
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
setvbuf(stdout, 0, _IONBF, 0);
|
||||
/* 1: both go on, separate memory */
|
||||
pid_t pid = fork();
|
||||
if (pid < 0) { perror("fork"); return 1; }
|
||||
if (pid == 0) {
|
||||
shared_counter += 1;
|
||||
printf("child counter %d\n", shared_counter);
|
||||
_exit(7);
|
||||
}
|
||||
int st = status_of(pid);
|
||||
printf("parent counter %d, child exit %d\n", shared_counter, st);
|
||||
|
||||
/* 2: deep, entered through a function pointer */
|
||||
char buf[64] = "";
|
||||
int (*volatile chain)(int, int, char *) = deep;
|
||||
int r = chain(20, 0, buf);
|
||||
if (r >= 1000) {
|
||||
/* the child, back at the top of the chain */
|
||||
printf("deep child %d %s\n", r, buf);
|
||||
exit(r - 1000 == 210 ? 3 : 4);
|
||||
}
|
||||
printf("deep parent %d %s\n", r, buf);
|
||||
|
||||
/* 3: threads */
|
||||
pthread_t t;
|
||||
pthread_create(&t, 0, spin, 0);
|
||||
pid = fork();
|
||||
if (pid == 0) {
|
||||
/* one thread here: this one */
|
||||
printf("threaded child\n");
|
||||
_exit(5);
|
||||
}
|
||||
printf("threaded parent, child exit %d\n", status_of(pid));
|
||||
return 0;
|
||||
}
|
||||
Executable
+55
@@ -0,0 +1,55 @@
|
||||
#!/bin/sh
|
||||
# wasm32 programs in the browser runtime (shims/browser/js/run-node.mjs: the
|
||||
# shim and glue a page loads, under Node): each tests/wasm32/*.c against its
|
||||
# native run, as tests/wasm32/run.sh does for gexc; with BUSYBOX, the
|
||||
# busybox.sh script under the browser's /bin/sh (run-node --busybox).
|
||||
#
|
||||
# tests/wasm32/run-browser.sh (GEX_WASM_SYSROOT set; the x86 shared
|
||||
# shim and seed in web/public/gex:
|
||||
# GEX_ARCH=x86 GEX_SHARED=1 shims/browser/build.sh)
|
||||
set -u
|
||||
HERE=$(cd "$(dirname "$0")" && pwd)
|
||||
ROOT=$(cd "$HERE/../.." && pwd)
|
||||
: "${GEX_WASM_SYSROOT:?set GEX_WASM_SYSROOT (tools/wine-wasm/build-sysroot.sh)}"
|
||||
SEED=$ROOT/web/public/gex/seed.x86.shared.wasm
|
||||
[ -f "$SEED" ] || { echo "no $SEED; build with GEX_ARCH=x86 GEX_SHARED=1 shims/browser/build.sh" >&2; exit 2; }
|
||||
WORK=${WORK:-$(mktemp -d)}
|
||||
fail=0
|
||||
run() {
|
||||
GEX_ARCH=x86 GEX_SHARED=1 timeout "${TMO:-300}" node "$ROOT/shims/browser/js/run-node.mjs" "$@" 2>/dev/null | grep -v '^\[host\]\|^\[gex\] fork -> \|^\[worker\] '
|
||||
}
|
||||
for src in "$HERE"/*.c; do
|
||||
name=$(basename "$src" .c)
|
||||
cc -O1 -o "$WORK/$name.native" "$src" -lpthread || { fail=1; continue; }
|
||||
"$ROOT/tools/wine-wasm/wcc" -O2 -o "$WORK/$name" "$src" || { fail=1; continue; }
|
||||
native=$(cd "$WORK" && "./$name.native" 2>/dev/null); ns=$?
|
||||
web=$(run --file "/t/$name=$WORK/$name" --cwd /t --argv0 "./$name" "$SEED" "$WORK/$name")
|
||||
ws=$(run --file "/t/$name=$WORK/$name" --cwd /t --argv0 "./$name" "$SEED" "$WORK/$name" >/dev/null; echo $?)
|
||||
if [ "$native" = "$web" ] && [ "$ns" = "$ws" ]; then
|
||||
echo "ok $name (browser, exit $ns)"
|
||||
else
|
||||
echo "FAIL $name (browser): native exit $ns, browser exit $ws"
|
||||
printf '%s\n' "$native" > "$WORK/$name.out-native"
|
||||
printf '%s\n' "$web" > "$WORK/$name.out-web"
|
||||
diff "$WORK/$name.out-native" "$WORK/$name.out-web" | head -20
|
||||
fail=1
|
||||
fi
|
||||
done
|
||||
if [ -n "${BUSYBOX:-}" ] && [ -f "$BUSYBOX" ]; then
|
||||
# the script busybox.sh runs, from its own text
|
||||
script=$(sed -n "/^script='/,/^echo done'/p" "$HERE/busybox.sh" | sed "1s/^script='//; \$s/'\$//")
|
||||
printf 'b\na\nc\nb\n' > "$WORK/in.txt"
|
||||
native=$(cd "$WORK" && sh -c "$script" 2>&1)
|
||||
web=$(run --busybox "$BUSYBOX" --file "/t/in.txt=$WORK/in.txt" --cwd /t --argv0 sh "$SEED" "$BUSYBOX" -c "$script")
|
||||
if [ "$native" = "$web" ]; then
|
||||
echo "ok busybox (browser)"
|
||||
else
|
||||
echo "FAIL busybox (browser)"
|
||||
printf '%s\n' "$native" > "$WORK/bb.out-native"; printf '%s\n' "$web" > "$WORK/bb.out-web"
|
||||
diff "$WORK/bb.out-native" "$WORK/bb.out-web" | head -20
|
||||
fail=1
|
||||
fi
|
||||
else
|
||||
echo "skip busybox (browser; set BUSYBOX)"
|
||||
fi
|
||||
exit $fail
|
||||
@@ -5,10 +5,9 @@
|
||||
# tools/wine-wasm/build-busybox.sh OUT_DIR [VERSION] (GEX_WASM_SYSROOT set)
|
||||
#
|
||||
# Downloads busybox VERSION (default 1.37.0) into OUT_DIR, configures it
|
||||
# with busybox.config (NOMMU: vfork, which the musl fork's wasm32 vfork
|
||||
# emulates -- the child runs on the parent's stack until it execs or exits,
|
||||
# which become a posix_spawn; LFS; hush; tail -N), builds with wcc. Prints
|
||||
# the binary's path.
|
||||
# with busybox.config (an MMU build: fork is real, wcc asyncifies the
|
||||
# program for it; LFS; hush; tail -N), builds with wcc. Prints the
|
||||
# binary's path.
|
||||
set -eu
|
||||
here=$(cd "$(dirname "$0")" && pwd)
|
||||
out=$(mkdir -p "$1" && cd "$1" && pwd)
|
||||
|
||||
@@ -19,8 +19,9 @@ JOBS=${JOBS:-4}
|
||||
mkdir -p "$OUT/musl-build" "$OUT/crt-build" "$OUT/sysroot/lib"
|
||||
|
||||
cd "$OUT/musl-build"
|
||||
# reconfigure when the flags changed (configure bakes them in)
|
||||
if [ "$(cat .gex-flags 2>/dev/null)" != "$WASM_CFLAGS" ]; then
|
||||
# reconfigure when the flags or the musl tree changed (configure bakes
|
||||
# both in: the Makefile is a link into the source)
|
||||
if [ "$(cat .gex-flags 2>/dev/null)" != "$MUSL $WASM_CFLAGS" ]; then
|
||||
rm -f config.mak
|
||||
[ -f Makefile ] && make -s clean >/dev/null 2>&1 || true
|
||||
fi
|
||||
@@ -28,7 +29,7 @@ if [ ! -f config.mak ]; then
|
||||
CC=clang CFLAGS="$WASM_CFLAGS" AR=llvm-ar RANLIB=llvm-ranlib \
|
||||
"$MUSL/configure" --target=wasm32-unknown-linux-musl --prefix="$OUT/sysroot" \
|
||||
--disable-shared --enable-static >/dev/null
|
||||
printf '%s' "$WASM_CFLAGS" > .gex-flags
|
||||
printf '%s' "$MUSL $WASM_CFLAGS" > .gex-flags
|
||||
fi
|
||||
# musl's makefile tracks no header dependencies: a changed header (the
|
||||
# wasm32 syscall stub is one) means rebuilding everything.
|
||||
@@ -68,4 +69,15 @@ rm -f "$OUT/sysroot/lib/libclang_rt.builtins-wasm32.a"
|
||||
llvm-ar crs "$OUT/sysroot/lib/libclang_rt.builtins-wasm32.a" $objs
|
||||
# The ICALL pass (wcc applies it to Wine's PE-side code, -D_WIN32).
|
||||
"$HERE/icall/build.sh" "$OUT/sysroot/lib"
|
||||
# binaryen, for wcc's asyncify of programs that fork (wasm-opt >= 117 for
|
||||
# exception handling): GEX_BINARYEN names an existing install.
|
||||
BINARYEN_VERSION=133
|
||||
if [ -n "${GEX_BINARYEN:-}" ]; then
|
||||
ln -sfn "$(realpath "$GEX_BINARYEN")" "$OUT/sysroot/binaryen"
|
||||
elif [ ! -x "$OUT/sysroot/binaryen/bin/wasm-opt" ]; then
|
||||
t=binaryen-version_$BINARYEN_VERSION
|
||||
curl -sSL "https://github.com/WebAssembly/binaryen/releases/download/version_$BINARYEN_VERSION/$t-x86_64-linux.tar.gz" | tar xz -C "$OUT"
|
||||
rm -rf "$OUT/sysroot/binaryen"
|
||||
mv "$OUT/$t" "$OUT/sysroot/binaryen"
|
||||
fi
|
||||
echo "sysroot: $OUT/sysroot"
|
||||
@@ -0,0 +1,50 @@
|
||||
sh
|
||||
ash
|
||||
ls
|
||||
cat
|
||||
sort
|
||||
head
|
||||
tail
|
||||
tr
|
||||
wc
|
||||
mkdir
|
||||
cp
|
||||
mv
|
||||
rm
|
||||
rmdir
|
||||
date
|
||||
uname
|
||||
sed
|
||||
grep
|
||||
egrep
|
||||
fgrep
|
||||
awk
|
||||
find
|
||||
xargs
|
||||
env
|
||||
id
|
||||
basename
|
||||
dirname
|
||||
echo
|
||||
printf
|
||||
test
|
||||
[
|
||||
expr
|
||||
cut
|
||||
touch
|
||||
ln
|
||||
chmod
|
||||
pwd
|
||||
sleep
|
||||
true
|
||||
false
|
||||
readlink
|
||||
realpath
|
||||
uniq
|
||||
tee
|
||||
stat
|
||||
dd
|
||||
seq
|
||||
yes
|
||||
printenv
|
||||
which
|
||||
@@ -43,7 +43,7 @@ CONFIG_BUSYBOX_EXEC_PATH="/proc/self/exe"
|
||||
#
|
||||
# CONFIG_STATIC is not set
|
||||
# CONFIG_PIE is not set
|
||||
CONFIG_NOMMU=y
|
||||
# CONFIG_NOMMU is not set
|
||||
# CONFIG_BUILD_LIBBUSYBOX is not set
|
||||
# CONFIG_FEATURE_LIBBUSYBOX_STATIC is not set
|
||||
# CONFIG_FEATURE_INDIVIDUAL is not set
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
# Shared by the wine-wasm scripts: clang's flags for wasm32 code that runs as
|
||||
# a gex native module (position-independent, shared memory, one instance per
|
||||
# thread). Kept to the features V8 and wasmtime 27 both have.
|
||||
# thread). Kept to the features V8 and wasmtime 27 both have. No tail calls:
|
||||
# binaryen's asyncify (fork, wcc) cannot instrument them, and the libc is
|
||||
# shared by every program.
|
||||
WASM_TARGET=wasm32-unknown-linux-musl
|
||||
WASM_FEATURES="-matomics -mbulk-memory -mmutable-globals -msign-ext -mnontrapping-fptoint -mtail-call"
|
||||
WASM_FEATURES="-matomics -mbulk-memory -mmutable-globals -msign-ext -mnontrapping-fptoint"
|
||||
# setjmp/longjmp lower onto wasm exception handling (V8 has it; wasmtime 27,
|
||||
# gex's native engine, does not: a module that uses setjmp needs the
|
||||
# wasmtime upgrade to run natively). clang gives a wasm32-*-linux target no
|
||||
|
||||
Executable
+83
@@ -0,0 +1,83 @@
|
||||
#!/usr/bin/env python3
|
||||
"""The functions of a wasm module that can reach an import, by a call graph
|
||||
in which an indirect call of type T can reach every table function of type T
|
||||
(a call_indirect traps on any other): what asyncify must instrument for fork
|
||||
(wcc), instead of everything that makes an indirect call.
|
||||
|
||||
forkreach.py WASM-DIS MODULE.wasm IMPORT-MODULE.NAME [ROOT ...]
|
||||
|
||||
Prints the reaching functions but the ROOTs (fork's driver and what is
|
||||
above it), comma-separated, for asyncify-onlylist. Exits 1 when the module
|
||||
cannot be analysed this way (a name asyncify's list cannot carry)."""
|
||||
import re, subprocess, sys
|
||||
|
||||
dis, path, target = sys.argv[1:4]
|
||||
roots = set(sys.argv[4:])
|
||||
mod, imp = target.split('.', 1)
|
||||
text = subprocess.run([dis, path], capture_output=True, text=True, check=True).stdout
|
||||
|
||||
NAME = r'\$([^\s()]+)'
|
||||
types = {} # type name -> signature
|
||||
import_name = None # the import's function name
|
||||
funcs = {} # name -> signature
|
||||
calls = {} # name -> set of direct callees
|
||||
icalls = {} # name -> set of signatures called indirectly
|
||||
table = set() # functions in element segments
|
||||
|
||||
|
||||
def sig_of(s):
|
||||
params = re.findall(r'\(param(?: \$[^\s()]+)?((?: [a-z0-9]+)+)\)', s)
|
||||
results = re.findall(r'\(result((?: [a-z0-9]+)+)\)', s)
|
||||
return ' '.join(p.strip() for p in params) + ' -> ' + ' '.join(r.strip() for r in results)
|
||||
|
||||
|
||||
cur = None
|
||||
for line in text.split('\n'):
|
||||
m = re.match(r' \(type ' + NAME + r' \(func(.*)\)\)\s*$', line)
|
||||
if m:
|
||||
types[m.group(1)] = sig_of(m.group(2))
|
||||
continue
|
||||
m = re.match(r' \(import "([^"]*)" "([^"]*)" \(func ' + NAME + r'(.*)\)\)\s*$', line)
|
||||
if m:
|
||||
if (m.group(1), m.group(2)) == (mod, imp):
|
||||
import_name = m.group(3)
|
||||
continue
|
||||
m = re.match(r' \(elem .*', line)
|
||||
if m:
|
||||
table.update(re.findall(NAME, line.split(')', 1)[1] if 'global.get' in line else line))
|
||||
continue
|
||||
m = re.match(r' \(func ' + NAME + r'(.*)$', line)
|
||||
if m:
|
||||
cur = m.group(1)
|
||||
funcs[cur] = sig_of(m.group(2))
|
||||
calls[cur] = set()
|
||||
icalls[cur] = set()
|
||||
continue
|
||||
if cur is None:
|
||||
continue
|
||||
for c in re.findall(r'\((?:return_)?call ' + NAME, line):
|
||||
calls[cur].add(c)
|
||||
for t in re.findall(r'\((?:return_)?call_indirect (?:' + NAME + r' )?\(type ' + NAME + r'\)', line):
|
||||
icalls[cur].add(types.get(t[1], '?'))
|
||||
|
||||
if import_name is None:
|
||||
sys.exit(1)
|
||||
by_sig = {}
|
||||
for f in table:
|
||||
if f in funcs:
|
||||
by_sig.setdefault(funcs[f], set()).add(f)
|
||||
|
||||
reach = {import_name}
|
||||
changed = True
|
||||
while changed:
|
||||
changed = False
|
||||
for f in funcs:
|
||||
if f in reach:
|
||||
continue
|
||||
if calls[f] & reach or any(by_sig.get(s, set()) & reach for s in icalls[f]):
|
||||
reach.add(f)
|
||||
changed = True
|
||||
out = sorted(f for f in reach if f in funcs and f not in roots)
|
||||
if any(re.search(r'[,"\\\s]', f) for f in out):
|
||||
sys.exit(1)
|
||||
print(','.join(out))
|
||||
@@ -320,6 +320,18 @@ def link(dlls, out, gexrt, others=None):
|
||||
cmd.insert(cmd.index(gexrt), obj)
|
||||
subprocess.check_call(cmd)
|
||||
print(f'{out}: {len(gaps)} functions nothing defines, trapping: {" ".join(n for n, _ in gaps)}')
|
||||
# wcc compiles code without -D_WIN32 as Unix-side code, whose setjmp
|
||||
# lowers onto the legacy exception instructions (for asyncify); that
|
||||
# includes winebuild's glue, where an adapter of msvcrt's _setjmp calls
|
||||
# a function clang lowers as setjmp. gex runs the standard instructions
|
||||
# only, so the module is translated, as wcc's own links are.
|
||||
opt = os.path.join(os.environ.get('GEX_WASM_SYSROOT', ''), 'binaryen', 'bin', 'wasm-opt')
|
||||
opt = os.environ.get('GEX_WASM_OPT') or opt
|
||||
if os.path.exists(opt):
|
||||
subprocess.check_call([opt, out, '-g', '--strip-dwarf', '--enable-threads', '--enable-bulk-memory',
|
||||
'--enable-mutable-globals', '--enable-sign-ext', '--enable-nontrapping-float-to-int',
|
||||
'--enable-exception-handling', '--enable-reference-types',
|
||||
'--translate-to-exnref', '-o', out])
|
||||
# The forwards, for gex to bind a -import re-export of a forwarded name
|
||||
# (kernel32's HeapFree is kernelbase's, which forwards to ntdll's
|
||||
# RtlFreeHeap): a custom section `gexw.forwards`, lines `name dll!target`.
|
||||
|
||||
+88
-1
@@ -31,6 +31,12 @@ args = sys.argv[1:]
|
||||
plugin = os.path.join(sysroot, 'lib', 'GexIcall.so')
|
||||
if '-D_WIN32' in args and os.path.exists(plugin) and not os.environ.get('GEX_WASM_NO_ICALL'):
|
||||
CC += ['-fpass-plugin=' + plugin]
|
||||
# Unix-side code (programs, their libraries) lowers setjmp onto the legacy
|
||||
# exception instructions: binaryen's asyncify (fork, below) can instrument
|
||||
# try/catch but not try_table. The link step translates the result to the
|
||||
# standard instructions (--translate-to-exnref), which is what gex runs.
|
||||
if '-D_WIN32' not in args:
|
||||
CC = [a.replace('-wasm-use-legacy-eh=false', '-wasm-use-legacy-eh=true') for a in CC]
|
||||
if any(a in ('-c', '-S', '-E', '-M', '-MM', '--version', '-v') or a.startswith(('-print-', '-dump'))
|
||||
for a in args) or not args:
|
||||
os.execvp(CC[0], CC + args)
|
||||
@@ -87,14 +93,95 @@ if relocatable:
|
||||
# referenced): a DLL for pelink.py
|
||||
cmd = ['wasm-ld', '-r', '-L' + lib, '-o', out] + objs + ld_in + [f for f in ld_flags if not f.startswith('--export')]
|
||||
sys.exit(subprocess.call(cmd))
|
||||
|
||||
def imports(path):
|
||||
"""(module, name) of every function import of the module at path."""
|
||||
w = open(path, 'rb').read()
|
||||
def leb(p):
|
||||
r = sh = 0
|
||||
while True:
|
||||
x = w[p]; p += 1
|
||||
r |= (x & 0x7f) << sh; sh += 7
|
||||
if not x & 0x80:
|
||||
return r, p
|
||||
def name(p):
|
||||
n, p = leb(p)
|
||||
return w[p:p + n].decode(), p + n
|
||||
out, p = [], 8
|
||||
while p < len(w):
|
||||
sid = w[p]; n, q = leb(p + 1); end = q + n
|
||||
if sid == 2:
|
||||
cnt, q = leb(q)
|
||||
for _ in range(cnt):
|
||||
m, q = name(q); f, q = name(q)
|
||||
kind = w[q]; q += 1
|
||||
if kind == 0:
|
||||
_, q = leb(q); out.append((m, f))
|
||||
elif kind == 1:
|
||||
q += 1; fl, q = leb(q); _, q = leb(q)
|
||||
if fl & 1: _, q = leb(q)
|
||||
elif kind == 2:
|
||||
fl, q = leb(q); _, q = leb(q)
|
||||
if fl & 1: _, q = leb(q)
|
||||
elif kind == 3:
|
||||
q += 2
|
||||
elif kind == 4:
|
||||
q += 1; _, q = leb(q)
|
||||
break
|
||||
p = end
|
||||
return out
|
||||
|
||||
|
||||
# Functions above fork's driver (src/process/wasm32/__gex_fork.c in the musl
|
||||
# fork): the unwind for a fork stops at the driver, so they keep no state.
|
||||
FORK_ROOTS = ['__gex_start', '__gex_start_c', '__gex_thread_start', '__gex_thread_c',
|
||||
'__gex_fork_run', '__gex_fork_drive', '__gex_fork_child']
|
||||
|
||||
|
||||
def finish(path):
|
||||
"""binaryen over a linked module: asyncify when it can fork (it imports
|
||||
gex.fork_point: every function that can reach it saves and restores its
|
||||
frame), and the legacy exception instructions translated to the standard
|
||||
ones. Names kept (-g): gex binds the entries by name."""
|
||||
opt = os.environ.get('GEX_WASM_OPT') or os.path.join(sysroot, 'binaryen', 'bin', 'wasm-opt')
|
||||
passes = []
|
||||
if ('gex', 'fork_point') in imports(path):
|
||||
passes += ['--asyncify', '[email protected]_point']
|
||||
# Only what can reach a fork by a type-correct call graph
|
||||
# (forkreach.py); asyncify's own analysis takes every indirect call
|
||||
# for a fork point, which is nearly every function.
|
||||
dis = os.path.join(os.path.dirname(opt), 'wasm-dis')
|
||||
reach = subprocess.run([sys.executable, os.path.join(here, 'forkreach.py'), dis, path, 'gex.fork_point'] + FORK_ROOTS,
|
||||
capture_output=True, text=True)
|
||||
if reach.returncode == 0 and not os.environ.get('GEX_WCC_FORK_ALL'):
|
||||
lst = os.path.join(tmp, 'onlylist')
|
||||
open(lst, 'w').write(reach.stdout.strip().replace(',', '\n') + '\n')
|
||||
passes += ['--pass-arg=asyncify-onlylist@@' + lst]
|
||||
else:
|
||||
passes += ['--pass-arg=asyncify-removelist@' + ','.join(FORK_ROOTS)]
|
||||
passes += ['-O2']
|
||||
passes += ['--translate-to-exnref']
|
||||
feats = ['--enable-threads', '--enable-bulk-memory', '--enable-mutable-globals', '--enable-sign-ext',
|
||||
'--enable-nontrapping-float-to-int', '--enable-exception-handling', '--enable-reference-types']
|
||||
# (DWARF dropped: binaryen cannot rewrite clang's version 5)
|
||||
r = subprocess.call([opt, path, '-g', '--strip-dwarf'] + feats + passes + ['-o', path])
|
||||
if r:
|
||||
sys.exit('wcc: wasm-opt failed on ' + path)
|
||||
|
||||
|
||||
if shared:
|
||||
cmd = common + ['-shared', '--no-entry', '--export-dynamic', '-o', out] + objs + ld_in + ld_flags + \
|
||||
[lib + '/libclang_rt.builtins-wasm32.a']
|
||||
sys.exit(subprocess.call(cmd))
|
||||
r = subprocess.call(cmd)
|
||||
if r:
|
||||
sys.exit(r)
|
||||
finish(out)
|
||||
sys.exit(0)
|
||||
wasm = out + '.wasm'
|
||||
cmd = common + ['-pie', '--no-entry', '-o', wasm, lib + '/crt1.o'] + objs + ld_in + ld_flags + \
|
||||
['-lc', lib + '/libclang_rt.builtins-wasm32.a']
|
||||
r = subprocess.call(cmd)
|
||||
if r:
|
||||
sys.exit(r)
|
||||
finish(wasm)
|
||||
sys.exit(subprocess.call([sys.executable, os.path.join(here, 'wasm-elf.py'), wasm, out]))
|
||||
Reference in new issue
Block a user