Merge merge/unity c784b5c6: Hollow Knight GC hang and STATE_BLOCKING crash fixed (musl-wasm64 3f1c1f08 provider TLS), embed test wasm JIT leg

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_b6478c5570464e3aa53c29026c16b36e
This commit is contained in:
velzieandClaude Opus 5.5 committed 2026-10-05 19:26:23 -04:00
commit c562065698
16 files changed
+856 -64

No files matched your search

+38 -1
View File
@@ -2063,6 +2063,36 @@ impl Client {
self.surfaces.get(&surface).map(|s| s.x11).unwrap_or(false)
}
/// Whether input can be delivered: the client has a pointer and a
/// keyboard, and a surface to give them to.
pub fn input_ready(&self) -> bool {
self.pointer.is_some() && self.keyboard.is_some() && self.pointer_target.or(self.focus_surface).is_some()
}
/// Where pointer input would go now, and the surfaces there are: for a
/// host explaining scripted input (`GEX_WL_INPUT`).
pub fn describe_input(&self) -> String {
let mut ids: Vec<_> = self.surfaces.keys().copied().collect();
ids.sort();
let list: Vec<String> = ids
.iter()
.map(|id| {
let s = &self.surfaces[id];
format!("{id}:{:?}{}{}{}", s.role, if s.x11 { " x11" } else { "" },
if s.committed_buffer.is_some() { " drawn" } else { "" },
s.external.map(|(w, h)| format!(" ext {w}x{h}")).unwrap_or_default())
})
.collect();
format!(
"pointer {:?} on {:?}, target {:?}, focus {:?}; surfaces [{}]",
self.pointer.is_some(),
self.pointer_on,
self.pointer_target,
self.focus_surface,
list.join(", ")
)
}
/// Move the pointer to a position on a particular surface, entering it (and
/// leaving whatever it was on) if the surface changed.
///
@@ -2157,10 +2187,17 @@ impl Client {
/// Pointer moved to surface-local `(x, y)` in pixels.
pub fn pointer_motion(&mut self, x: i32, y: i32, time: u32) {
let Some(ptr) = self.pointer else { return };
let Some(ptr) = self.pointer else {
trace_out("[wl] pointer motion before the client asked for a pointer");
return;
};
// Before the enter, so the enter it may trigger carries this position.
self.pointer_at = (x, y);
if !self.ensure_pointer_enter() {
trace_out(&format!(
"[wl] pointer motion dropped: no surface to enter (target {:?}, focus {:?})",
self.pointer_target, self.focus_surface
));
return;
}
// motion(time, surface_x, surface_y)
+182 -14
View File
@@ -695,17 +695,185 @@ Not yet:
natively under gexc as well (wasmtime compiles the published
modules), so native runs get the same tier.
- Unity's remaining internal calls as they show up; then Stardew/Celeste.
- **Resume (after the move; HEAD 1db5c816, `master` merged in):**
0. The coordgate on this branch still lacks its browser NT step (it was
stopped while E was timing), and nothing after 49a5773b has been
gated. Run it before the libprov changes go to master.
1. Build: `tools/unity-dotnet/mono/build.sh OUT`, then
`tools/unity-dotnet/sysroot` via its `sysroot` step. musl comes from
`MUSL_SRC`, which defaults to `../musl-wasm64` beside the worktree
(remote `mw`, branch `gex-wasm64`).
2. Find which lookup returns NULL. Candidates: `get_attr` on an
attribute in UnityEngine.CoreModule, a field name Unity's Mono had
that .NET's types lack, or a type whose lookup resolves through a
facade's forwarders.
3. Then on to the menu with graphics (the old `muck.sh` path: in-guest
Xwayland on gex's compositor), and to Unity's own internal calls.
- **Scripted input (2026-10-05):** `GEX_WL_INPUT=FILE` feeds the native
compositor timed `move`/`click`/`button`/`key`/`tap` events, timed from
its `frames N`th frame (`frames 0`: from its start), held until a client
has a seat and a surface. Checked end to end on worldmachine with
`tests/x11/xinput-echo.c` on the in-guest (rootful) Xwayland: motion, a
click and keys arrive. Muck's menu still ignores the clicks (unknown
why; Muck is set aside, see the coordinator's direction).
- **Hollow Knight (Unity 6000.0.61, linux-x64 Mono; 2026-10-05):** batch
mode loads the first scene natively with --fjit
(`muck-dotnet.sh` with `MUCK=<HK dir> EXE=hollow_knight.x86_64
UNITY_RELAUNCH=1 STEAM_APPID=367520`). Needed: a PIE rebuild of the
player's small main (`tests/games/unity-launcher.c`; Unity 6 links it at
0x200000, inside gex's reserved low region); `mono_dllmap_insert`
ignored; GC handles as the GC's raw values in runtime, CoreLib and
provider (Unity 6 tests a handle's bit 0 and its managed code calls
`GCHandle.FromIntPtr` on raw values); dead-handle reads tolerated; the
framework's x86-64 crypto and compression natives beside the BCL (HK's
localisation decrypts with AES). Also: guest signal handlers run at a
provider's syscalls (`__gex_signal_frames` in gexprov64.c).
- **Hollow Knight, parked 2026-10-05 (shutdown):** with graphics on
worldmachine (`scratch/unity/wm/muck.sh` with `GAMEDIR=<HK>
EXE=hollow_knight.x86_64 UNITY_RELAUNCH=1 STEAM_APPID=367520`; HK is
copied to `~/src/noita-wasm/games-linux/Hollow Knight` there) two
problems, after GL 4.6 comes up and physics initialises:
1. **With --fjit only:** `free(): invalid pointer` (deterministic), from
Unity's allocator's fallback `free` (UnityPlayer vaddr ~0xf8c0af, called
via 0xf8a0c0/0xf9559d). Not a `free` the program calls through the PLT
(an LD_PRELOAD watcher saw none), so glibc-internal or a corrupted
Unity block. Without --fjit it does not happen: a function-JIT
translation bug is the first suspect. Next: bisect with the fjit off
for UnityPlayer ranges, or `GEX_DUMP_ABORT=1` (now works for x86-64)
and map the frames.
2. **Without --fjit (and in batch mode with it):** a hang after
"Unloading 5 Unused Serialized files". `GEX_DUMP_AT=640` shows the
main thread inside the provider's `mono_gc_collect` (Unity's
UnloadUnusedAssets), sleeping in a loop (strace: only
clock_nanosleep); threads 47/48 in libc. Next: the same dump with the
wasm stacks (`GEX_DUMP_AT` now prints `WasmBacktrace` for each thread;
run with `GEX_WASM_BACKTRACE=1`) to see what the collection waits for
-- likely sgen's suspend of a thread attached to Mono (Unity has ~70)
that never acknowledges, or the provider stop-the-world.
Then the keyboard script to gameplay (`GEX_WL_INPUT`, works on X
clients) and interpreter vs JIT on worldmachine.
- **Hollow Knight after the restart (gooner, 2026-10-05; runs
`scratch/unity/runs/hk20`-`hk30`, batch mode, no --fjit):**
1. **The hang is fixed.** The `GEX_WASM_BACKTRACE` dump put the main
thread in sgen's `unified_suspend_stop_world` restart loop (`g_usleep`):
one thread stayed "in a critical location" because its saved SP lay
outside its registered stack. `GEXMONO_TRACE_THREADS=1` (patch 0003)
and `GEX_PROVIDER_STACKS=1` showed that SP was on thread 70's provider
stack while the MonoThreadInfo was thread 28's: musl's
`__gex_provider_attach` gave attached threads no thread-specific data,
so every attached thread's `pthread_getspecific` read the words at
address 0 and Mono's thread-info key handed thread 70 thread 28's info
(musl-wasm64 3f1c1f08; that commit also makes a provider-created
thread report gex's stack). The stop-the-world now names such a thread
once (`gex STW: thread ... still critical`).
2. **Next crash fixed (6407a1ed):** "Cannot transition thread from
STATE_BLOCKING with DO_BLOCKING": Unity calls the embedding API
(`mono_gc_get_used_size`, `mono_runtime_invoke`) from guest code a
P/Invoke called, i.e. inside the interpreter's GC-safe region, and the
old one-stack crossing hooks left the thread Blocking. Now calls into
guest code and entries from it are separate pairs (gexprov64.c
`gexp_hook_call_guest`/`_call_return`, `gexp_hook_from_guest`/
`_to_guest`, `_mark`/`_unwind` for a raise through guest frames).
3. HK then unloads 153 assets (the GC that hung) and runs its first
scene's scripts (no UIManager in batch mode, TextMeshPro font
warnings). `provider_stack` now reports the stack's real base (it was
derived from the current SP).
4. The --fjit `free(): invalid pointer` did not reproduce on gooner
(hk21: --fjit with graphics, llvmpipe, reached the same unload hang);
it was seen on worldmachine only. `GEX_FJIT_RANGE`/`GEX_FJIT_SKIP`
(hex `LO-HI,...`) restrict the function JIT by entry address for the
bisection.
5. **Black frames with graphics (gooner, llvmpipe; hk31-hk32):** with
--fjit HK creates its GL 4.5 device, loads a second scene and the
compositor gets ~30 presents/s, but every captured frame
(`f-*.ppm`, 185 of them over 25 min) is all black. Not the Wayland
backend: `xwayland-inguest.sh` now unsets `WAYLAND_DISPLAY` for the
program (Unity 6 and SDL could otherwise pick gex's compositor), still
black. Unknown whether Unity draws nothing (shaders?) or the blit to
the window is lost. hk33 (`GEXGL_TRACE_GENERATED=1
GEXGL_SUBMIT_TRACE=1`, periodic tallies every 5000 submits in
out.txt) was stopped at the reboot before it got far: rerun it, map
opcodes with `guest/gl/gexgl.h`, and check for draws vs clears/blits.
6. **The --fjit `free(): invalid pointer` bisection (worldmachine only;
its glibc is Arch 2.44, gooner's Debian 2.36):** script
`scratch/unity/wm/fjtry.sh NAME SKIP [RANGE]` (run through
`BENCH_HOST=worldmachine scratch/bench/bench.sh -- bash
scratch/unity/wm/fjtry.sh ...`, give each a distinct `XNUM=`; prints
BAD/GOOD/NONE/STALL; it installs `wm/gexc.new` if present). Addresses
on worldmachine: libc 0x7d748000-0x7d948000, UnityPlayer
0x7e041000-0x7fe00000. Results: full --fjit BAD (hkw1, hkw2);
RANGE=libc only GOOD (b3, reached "Unloading 5"); RANGE=UnityPlayer
only, libc+UnityPlayer, SKIP=UnityPlayer, SKIP=libc: all stall
before the GL device (b1, b2, b4-b8; b7/b8 after the VMA-scan fix in
`job`). So something besides libc and UnityPlayer is involved, and
jitting a subset while other code stays on tier-1 stalls (itself
worth a look: a tier-1/fjit interplay bug). b9-b11 (libc + the small
libs 0x7d948000-0x7e041000; libc + above UnityPlayer; everything below
0x7d948000) were killed by the reboot: rerun them first.
7. Next after these: GEX_WL_INPUT keyboard script into gameplay, then
interpreter vs JIT on worldmachine (stage with `scratch/unity/wm`:
copy target/dbg-x86/release/gexc, dnc/provider/mono.wasm,
dnc/lib/System.Private.CoreLib.dll, tests/games/*.sh; HK lives in
`~/src/noita-wasm/games-linux/Hollow Knight` there), then the next
GAMES-CORPUS.md games.
**Build and run recipe (gooner):** `. scratch/env.sh`; gexc:
`cargo build --release -p gexc --features gexc/x86 --target-dir
target/dbg-x86`; provider: `tools/unity-dotnet/mono/build.sh
~/gex-sandbox/scratch/unity/dnc sysroot runtime provider` (sysroot when
musl-wasm64 changed; it builds wt/musl-wasm64, branch gex-wasm64, pushed
to mw at 3f1c1f08); HK batch: `MUCK="$HOME/gex-sandbox/games-linux/Hollow
Knight" EXE=hollow_knight.x86_64 UNITY_RELAUNCH=1 STEAM_APPID=367520
GEXC=$PWD/target/dbg-x86/release/gexc sh tests/games/muck-dotnet.sh
~/gex-sandbox/scratch/unity/dnc RUNDIR SECS [--fjit]`; add
`GFX=$HOME/gex-sandbox/scratch/unity/gl64 XNUM=N EVERY=300` for
graphics. Regression checks used: `tests/unity-dotnet/embed/run.sh
~/gex-sandbox/scratch/unity/dnc target/dbg-x86/release/gexc` (PASS) and
Muck batch with --fjit (main loop runs).
Working-tree note: `dnc/src` has every patch staged (`git add -A`) so
`git diff` shows only new edits (how 0003 was made). Run `git -C
scratch/unity/dnc/src reset -q` before build.sh's `src` step.
- **Resume (after the 2026-10-05 evening reboot; HEAD 8bfbae51, master
e3917bac merged):**
0. Gate head 8bfbae51 offered to the coordinator (the branch gate has
never run on anything after d69c1cd6/3718d0ca). Extra legs that prove
this stream's work, all checked green on 8bfbae51 on gooner:
`tests/unity-dotnet/embed/run.sh ~/gex-sandbox/scratch/unity/dnc
target/dbg-x86/release/gexc` (interpreter, --fjit, and since 8bfbae51
the wasm JIT at threshold 1, plus the GC-roots control);
`tests/unity-dotnet/prov64/run.sh
~/gex-sandbox/scratch/unity/dnc/sysroot/sysroot GEXC` (note the doubled
`sysroot`); Muck batch with --fjit (`muck-dotnet.sh ... 600 --fjit`,
player.log reaches "Initializing packets"); HK batch (reaches its first
scene's scripts: "Couldn't find a UIManager"). Logs in
`scratch/unity/logs/`, runs `scratch/unity/runs/mk2`, `hk34`.
1. The fork clone `dnc/src` has a local commit `2a05cbbc5` (patch 0001)
on top of 6812ea26, so build.sh's `src` step fails ("local changes
would be overwritten"). The working tree was verified identical to
6812ea26 + patches/0001-0003 (tree hashes equal), so build with the
steps `sysroot runtime provider` (skip `src`).
2. **HK black frames (gooner, runs hk35-hk37, hk40):** not a GL
translation problem: Unity never presents. `GEX_GL_CAPTURE=FILE` (host
env) recordings (`scratch/unity/gltr.py FILE hist|frame N|draws`) of
hk36 and hk37 are byte-identical in size (70,337,112 bytes) and hold
no PRESENT, no draw, one CLEAR, no shader: the stream stops after a
TEX_STORAGE_2D during the first scene's load. `--strace` (hk37): the
render thread (guest tid 68, same ring as the main thread) made 4
submits and then waits in futex/clock_gettime, idle, while the main
thread keeps running scripts. So the player loop stops sending
frames. Suspects: (a) the window never gets focus or visibility in the
rootful in-guest Xwayland with no window manager, and HK (unlike
Muck) does not run in the background; hk40 (`GEX_WL_INPUT` pointer
moves into the window every 7 s) was to test this but died first, see
3; (b) the game logic: "Couldn't find a Game Manager" in both batch
and graphics runs. `GEXMONO_TRACE=1` (hk38) shows Unity's
FindObjectOfType loop testing every object against GameManager's class
with `mono_class_is_subclass_of` and finding none. A control on
Unity's own Mono (run `hkst`, no provider) is running to tell whether
that message is normal for this scene. Also seen: SDL "window backend
is (null)" and "Video subsystem has not been initialized" early on,
then a window at 800x600 anyway (same in hk31/hk32).
3. **--fjit memory corruption is intermittent, and not only on
worldmachine:** hk40 on gooner died at startup with `*** stack
smashing detected ***` (same flags as hk36, which ran). On worldmachine:
b9 (RANGE libc + small libs) GOOD; b10 (libc + above UnityPlayer) and
b11 (everything below 0x7d948000) stalled before player.log existed;
b12 (full --fjit, `GEX_DUMP_ABORT=1`) BAD, dump in
`scratch/unity/wm/res/b12-out.txt`: main thread during physics init,
return addresses UnityPlayer+0x8920af/+0x8900c0 (vaddr 0xf8c0af, Unity's
allocator after `pthread_getspecific`) -> libc free; b13 (SKIP the
allocator 0x7e8d0000-0x7e8de000) failed differently: thread 49
SIGSEGV at pc 0 after 4 min. On gooner, batch HK with RANGE=UnityPlayer
only (hkr1) and full --fjit (hkr0) both reach the first scene, so the
worldmachine "stalls" with partial ranges are not a deterministic
tier-1/fjit interplay bug. Conclusion so far: a nondeterministic
corruption under --fjit (canary, malloc chunk, a null call target),
more likely a race or a per-thread state slip (TLS/fs base?) than one
mistranslated function; range bisection cannot pin it. Next: repeat
identical runs to measure the failure rate, then look at thread
creation/fs-base and publication races in the function JIT.
+22 -1
View File
@@ -62,6 +62,8 @@ pub(crate) struct Store {
/// The stack pointer this thread's stateful providers share
/// (`libprov::stack`), made at the first one's instantiation.
pub provider_sp: Option<wasmtime::Global>,
/// The lowest address of that stack (`libprov::STACK` bytes).
pub provider_stack_lo: u64,
/// The wasm exception tag of `gex.provider_throw` (wasm64 providers; [`crate::libprov::tag`]).
pub provider_tag: Option<wasmtime::Tag>,
/// How far through the map's log this table is.
@@ -227,7 +229,7 @@ pub(crate) fn ensure(mut cx: impl AsContextMut<Data = HostState>) -> Result<()>
table.set(&mut c, gex_abi::ftab::FALLBACK as u64, wasmtime::Ref::Func(Some(fallback)))?;
let jit = c.data_mut().jit.as_mut().unwrap();
jit.instances.push(inst);
jit.ftab = Some(Store { table, resolve, bridge, root, provider_sp: None, provider_tag: None, replayed: 0 });
jit.ftab = Some(Store { table, resolve, bridge, root, provider_sp: None, provider_stack_lo: 0, provider_tag: None, replayed: 0 });
Ok(())
}
@@ -651,8 +653,27 @@ struct Job {
cache: Option<(std::path::PathBuf, String)>,
}
/// `GEX_FJIT_RANGE=LO-HI[,LO-HI...]` (hex guest addresses): the function JIT
/// takes only functions entered in those ranges; `GEX_FJIT_SKIP=` the same
/// form, never those. For bisecting a translation bug down to one function.
fn bisect_allows(addr: u64) -> bool {
type Ranges = Option<Vec<(u64, u64)>>;
fn parse(var: &str) -> Ranges {
let v = std::env::var(var).ok()?;
let hex = |s: &str| u64::from_str_radix(s.trim().trim_start_matches("0x"), 16).ok();
Some(v.split(',').filter_map(|r| { let (a, b) = r.split_once('-')?; Some((hex(a)?, hex(b)?)) }).collect())
}
static R: std::sync::OnceLock<(Ranges, Ranges)> = std::sync::OnceLock::new();
let (only, skip) = R.get_or_init(|| (parse("GEX_FJIT_RANGE"), parse("GEX_FJIT_SKIP")));
let inr = |rs: &Vec<(u64, u64)>| rs.iter().any(|&(s, e)| addr >= s && addr < e);
only.as_ref().is_none_or(inr) && !skip.as_ref().is_some_and(inr)
}
fn job(caller: &mut Caller<'_, HostState>, sh: &Arc<Mutex<JitShared>>, memory: &gex_mem::GuestMemory, addr: u64) -> Option<Job> {
gex_kernel::ftab::translator()?;
if !bisect_allows(addr) {
return None;
}
let (exec, ro, wx): (Vec<(u64, u64)>, Vec<(u64, u64)>, Vec<(u64, u64)>) = caller.data_mut().with_kernel(|k| {
let v = k.mem.vmas();
(
+57 -7
View File
@@ -847,6 +847,7 @@ fn run_program_with(
std::sync::atomic::Ordering::Relaxed,
);
JIT_ON.store(opts.jit && !opts.aot_only, std::sync::atomic::Ordering::Relaxed);
std::sync::LazyLock::force(&DUMP_START);
DUMP_ABORT.store(
std::env::var_os("GEX_DUMP_ABORT").is_some(),
std::sync::atomic::Ordering::Relaxed,
@@ -1697,6 +1698,20 @@ fn run_thread_loop(
// GEX_TRACE_SEGV=1: where (with GEX_WASM_BACKTRACE=1, the wasm frames).
if std::env::var_os("GEX_TRACE_SEGV").is_some() {
eprintln!("gex: guest memory access out of bounds: {e:?}");
// The context's PC is the last one stored: the faulting block's entry, or near it.
if let Some(memory) = store.data().memory.clone() {
let at = read_u64(memory.bytes(), (ctx_addr + ctx::PC as u64) as usize);
eprintln!("gex: thread {} context pc {at:#x}", store.data().thread.tid);
let maps = store.data_mut().with_kernel(|k| k.mem.proc_maps());
for l in maps.lines() {
let mut r = l.split(|c| c == '-' || c == ' ');
let (Some(a), Some(b)) = (r.next(), r.next()) else { continue };
let (Ok(a), Ok(b)) = (u64::from_str_radix(a, 16), u64::from_str_radix(b, 16)) else { continue };
if (a..b).contains(&at) {
eprintln!("gex: in {l}");
}
}
}
}
if action.handler == 0 {
return Ok(Stopped::Exited(Exit::Signal {
@@ -2451,6 +2466,10 @@ static JIT_ON: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::ne
/// unwinds nothing, so the state must be captured at the syscall or it is gone
/// (e.g. which function's canary failed, from the stack's return addresses).
static DUMP_ABORT: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
static DUMP_AT: std::sync::LazyLock<Option<f64>> =
std::sync::LazyLock::new(|| std::env::var("GEX_DUMP_AT").ok().and_then(|v| v.parse().ok()));
static DUMP_START: std::sync::LazyLock<std::time::Instant> = std::sync::LazyLock::new(std::time::Instant::now);
static DUMPED: std::sync::LazyLock<Mutex<std::collections::HashSet<i32>>> = std::sync::LazyLock::new(Default::default);
/// A watch on a range of guest memory: `GEX_WATCH=<hex addr>[+<hex len>]`. A
/// software watchpoint: every safepoint and syscall compares the bytes and
@@ -2537,7 +2556,7 @@ fn dump_guest(gm: &mut GuestMem<'_>, ctx_addr: u64, tid: i32, vmas: &[gex_kernel
|gm: &mut GuestMem<'_>, off: usize| gm.read_u64(ctx_addr + off as u64).unwrap_or(0);
let pc = read(gm, ctx::PC);
let sp = read(gm, ctx::SP);
eprintln!("gex: guest abort in thread {tid}");
eprintln!("gex: guest state of thread {tid}");
eprintln!(" pc {pc:#018x} {}", describe(pc));
eprintln!(" sp {sp:#018x} {}", describe(sp));
for row in 0..8 {
@@ -2901,6 +2920,11 @@ fn register_imports(linker: &mut Linker<HostState>) -> Result<()> {
.ok_or_else(|| anyhow!("guest memory unavailable"))?;
// Profiling: count the call against its function (S6).
profile::syscall_enter(&mut caller, &GuestMem::new(memory.bytes()), pc as u64);
let dump_bt = DUMP_AT.filter(|&at| DUMP_START.elapsed().as_secs_f64() >= at).and_then(|_| {
let tid = caller.data().thread.tid;
(!DUMPED.lock().unwrap_or_else(|e| e.into_inner()).contains(&tid))
.then(|| format!("{:?}", wasmtime::WasmBacktrace::force_capture(&caller)))
});
let data = memory.bytes();
let state = caller.data_mut();
// A thread that loops on a blocking syscall -- a condition
@@ -2932,15 +2956,41 @@ fn register_imports(linker: &mut Linker<HostState>) -> Result<()> {
// A guest that is about to kill itself is the last moment its
// state means anything. `abort` unwinds nothing, so by the time
// the process is gone there is no way to ask where it was.
// `GEX_DUMP_AT=SECONDS`: each thread's state once, at its first syscall after that long -- where a
// run that has stopped making progress is (a thread waiting in a loop of sleeps shows up; one
// blocked for good does not).
if let Some(at) = *DUMP_AT {
if DUMP_START.elapsed().as_secs_f64() >= at
&& DUMPED.lock().unwrap_or_else(|e| e.into_inner()).insert(state.thread.tid)
{
let ctx_addr = state.thread.ctx_addr;
let tid = state.thread.tid;
let vmas = state.with_kernel(|k| k.mem.vmas().to_vec());
eprintln!("gex: GEX_DUMP_AT: thread {tid} at syscall {nr}");
dump_guest(&mut gm, ctx_addr, tid, &vmas);
if let Some(bt) = &dump_bt {
eprintln!(" wasm: {bt}");
}
}
}
if DUMP_ABORT.load(std::sync::atomic::Ordering::Relaxed) {
let signo = match nr {
gex_kernel::syscalls::nr::KILL | gex_kernel::syscalls::nr::TKILL => {
gm.read_u64(state.thread.ctx_addr + ctx::x(1) as u64)
// x86-64 numbers and argument registers (rsi = x(6), rdx = x(2)) when the guest is x86-64.
let signo = if gex_abi::X86 {
match gm.read_u64(state.thread.ctx_addr + ctx::x(0) as u64).unwrap_or(!0) {
62 | 200 => gm.read_u64(state.thread.ctx_addr + ctx::x(6) as u64),
234 => gm.read_u64(state.thread.ctx_addr + ctx::x(2) as u64),
_ => Err(gex_kernel::Errno::EINVAL),
}
gex_kernel::syscalls::nr::TGKILL => {
gm.read_u64(state.thread.ctx_addr + ctx::x(2) as u64)
} else {
match nr {
gex_kernel::syscalls::nr::KILL | gex_kernel::syscalls::nr::TKILL => {
gm.read_u64(state.thread.ctx_addr + ctx::x(1) as u64)
}
gex_kernel::syscalls::nr::TGKILL => {
gm.read_u64(state.thread.ctx_addr + ctx::x(2) as u64)
}
_ => Err(gex_kernel::Errno::EINVAL),
}
_ => Err(gex_kernel::Errno::EINVAL),
};
if signo == Ok(6) {
let ctx_addr = state.thread.ctx_addr;
+9 -8
View File
@@ -110,6 +110,9 @@ pub(crate) fn stack(cx: &mut wasmtime::StoreContextMut<'_, HostState>) -> Result
let name = format!("[provider stack, thread {tid}]");
let base = if LIBS.get().map_or(true, |v| v.is_empty()) { alloc_top(cx.data_mut(), STACK, &name)? } else { alloc(cx.data_mut(), STACK, &name)? };
let top = base + STACK - 16;
if std::env::var_os("GEX_PROVIDER_STACKS").is_some() {
eprintln!("gex: provider stack for thread {tid}: [{base:#x}, {:#x})", base + STACK);
}
// Pointer-sized: i64 for a wasm64 provider beside a 64-bit guest memory.
let g = if gex_abi::mem64() {
wasmtime::Global::new(&mut *cx, wasmtime::GlobalType::new(wasmtime::ValType::I64, wasmtime::Mutability::Var), Val::I64(top as i64))?
@@ -118,6 +121,7 @@ pub(crate) fn stack(cx: &mut wasmtime::StoreContextMut<'_, HostState>) -> Result
};
if let Some(s) = cx.data_mut().jit.as_mut().and_then(|j| j.ftab.as_mut()) {
s.provider_sp = Some(g.clone());
s.provider_stack_lo = base;
}
Ok(g)
}
@@ -522,17 +526,14 @@ fn register64(linker: &mut Linker<HostState>) -> Result<()> {
Ok(())
})()) })?;
// This thread's provider C stack (`stack`): its lowest address; it is
// `STACK` bytes. musl reports it as the thread's stack.
// `STACK` bytes. musl reports it as the thread's stack. (Not derived from
// the stack pointer's current value, which is below the top by whatever
// the thread has on its stack when it asks.)
linker.func_wrap(IMPORT_MODULE, "provider_stack", |mut caller: Caller<'_, HostState>| -> wasmtime::Result<i64> { crate::host((|| -> anyhow::Result<i64> {
use wasmtime::AsContextMut;
let mut cx = caller.as_context_mut();
let g = stack(&mut cx)?;
let top = match g.get(&mut cx) {
Val::I64(v) => v as u64,
Val::I32(v) => v as u32 as u64,
_ => 0,
};
Ok((top + 16 - STACK) as i64)
stack(&mut cx)?;
Ok(cx.data().jit.as_ref().and_then(|j| j.ftab.as_ref()).map_or(0, |s| s.provider_stack_lo) as i64)
})()) })?;
// Constants of gex's ABI a provider calling guest code needs.
linker.func_wrap(IMPORT_MODULE, "provider_abi", |_caller: Caller<'_, HostState>, which: i32| -> i64 {
+153
View File
@@ -50,6 +50,104 @@ struct Inner {
/// The GPU compositor, when one could be started. Optional: a machine with
/// no graphics adapter still runs, compositing on the host instead.
gpu: Mutex<Option<gex_gpu::Compositor>>,
/// `GEX_WL_INPUT`: scripted input, timed from the first frame.
input: Mutex<Option<Input>>,
}
/// One scripted input event (`GEX_WL_INPUT`, [`Input::load`]).
#[derive(Debug, Clone, Copy)]
enum Ev {
Motion(i32, i32),
Button(u32, bool),
Key(u32, bool),
}
/// A script of input events at times (seconds) after the compositor's Nth
/// frame (`frames N`, default 1: a game's first frame is often a splash long
/// before its menu; 0: from the compositor's start), delivered to the clients by [`Wayland::pump`] as they fall due:
/// what an automated game test needs to get past menus. One event per line,
/// `#` comments:
///
/// ```text
/// frames 300 # time from the 300th frame
/// 30 click 100 328 # motion, then BTN_LEFT down and (0.1 s later) up
/// 31.5 move 400 300
/// 32 button 272 1 # a Linux BTN_* code, 1 down / 0 up
/// 40 tap enter # key down, and up 0.1 s later
/// 41 key z 1 # a key name or Linux KEY_* code, 1 down / 0 up
/// ```
///
/// Coordinates are the focused surface's pixels.
struct Input {
events: Vec<(f64, Ev, String)>,
next: usize,
start: Option<std::time::Instant>,
/// Frames to see before the clock starts, and how many have been.
after: u64,
seen: u64,
}
/// Linux `KEY_*` codes for the names a script may use (else a number).
fn key_code(name: &str) -> Option<u32> {
let k = match name.to_ascii_lowercase().as_str() {
"esc" | "escape" => 1,
"1" => 2, "2" => 3, "3" => 4, "4" => 5, "5" => 6, "6" => 7, "7" => 8, "8" => 9, "9" => 10, "0" => 11,
"backspace" => 14, "tab" => 15,
"q" => 16, "w" => 17, "e" => 18, "r" => 19, "t" => 20, "y" => 21, "u" => 22, "i" => 23, "o" => 24, "p" => 25,
"enter" | "return" => 28, "ctrl" | "leftctrl" => 29,
"a" => 30, "s" => 31, "d" => 32, "f" => 33, "g" => 34, "h" => 35, "j" => 36, "k" => 37, "l" => 38,
"shift" | "leftshift" => 42,
"z" => 44, "x" => 45, "c" => 46, "v" => 47, "b" => 48, "n" => 49, "m" => 50,
"alt" | "leftalt" => 56, "space" => 57,
"f1" => 59, "f2" => 60, "f3" => 61, "f4" => 62, "f5" => 63,
"up" => 103, "left" => 105, "right" => 106, "down" => 108,
n => return n.parse().ok(),
};
Some(k)
}
impl Input {
fn load(path: &str) -> Result<Input, String> {
let text = std::fs::read_to_string(path).map_err(|e| format!("{path}: {e}"))?;
let mut events = Vec::new();
let mut after = 1;
for (n, line) in text.lines().enumerate() {
let line = line.split('#').next().unwrap_or("").trim();
if line.is_empty() {
continue;
}
let w: Vec<&str> = line.split_whitespace().collect();
let bad = || format!("{path}:{}: cannot read `{line}`", n + 1);
if w[0] == "frames" {
after = w.get(1).and_then(|v| v.parse().ok()).ok_or_else(bad)?;
continue;
}
let t: f64 = w[0].parse().map_err(|_| bad())?;
let num = |i: usize| -> Result<i32, String> { w.get(i).and_then(|v| v.parse().ok()).ok_or_else(bad) };
let key = |i: usize| -> Result<u32, String> { w.get(i).and_then(|v| key_code(v)).ok_or_else(bad) };
let down = |i: usize| -> Result<bool, String> { Ok(num(i)? != 0) };
let l = line.to_string();
match w.get(1).copied() {
Some("move") => events.push((t, Ev::Motion(num(2)?, num(3)?), l)),
Some("click") => {
events.push((t, Ev::Motion(num(2)?, num(3)?), l.clone()));
events.push((t, Ev::Button(0x110, true), l.clone()));
events.push((t + 0.1, Ev::Button(0x110, false), l));
}
Some("button") => events.push((t, Ev::Button(num(2)? as u32, down(3)?), l)),
Some("key") => events.push((t, Ev::Key(key(2)?, down(3)?), l)),
Some("tap") => {
events.push((t, Ev::Key(key(2)?, true), l.clone()));
events.push((t + 0.1, Ev::Key(key(2)?, false), l));
}
_ => return Err(bad()),
}
}
events.sort_by(|a, b| a.0.total_cmp(&b.0));
// `frames 0`: from the compositor's start.
let start = if after == 0 { Some(std::time::Instant::now()) } else { None };
Ok(Input { events, next: 0, start, after, seen: 0 })
}
}
impl std::fmt::Debug for Wayland {
@@ -60,14 +158,53 @@ impl std::fmt::Debug for Wayland {
impl Wayland {
pub fn new(width: i32, height: i32) -> Wayland {
let input = match std::env::var("GEX_WL_INPUT") {
Ok(p) => match Input::load(&p) {
Ok(i) => Some(i),
Err(e) => {
eprintln!("[gex] GEX_WL_INPUT: {e}");
None
}
},
Err(_) => None,
};
Wayland {
inner: Arc::new(Inner {
size: Mutex::new((width, height)),
input: Mutex::new(input),
..Default::default()
}),
}
}
/// A frame was shown (a client's commit, or GL's own present): the
/// input script's clock starts at its `frames N`th.
fn note_frame(&self) {
if let Some(inp) = self.inner.input.lock().unwrap().as_mut() {
inp.seen += 1;
if inp.seen >= inp.after && inp.start.is_none() {
eprintln!("[gex] input: the script's clock starts at frame {}", inp.seen);
inp.start = Some(std::time::Instant::now());
}
}
}
/// The scripted events now due (`GEX_WL_INPUT`), with their time in ms.
fn due_input(&self) -> Vec<(Ev, u32)> {
let mut g = self.inner.input.lock().unwrap();
let Some(inp) = g.as_mut() else { return Vec::new() };
let Some(start) = inp.start else { return Vec::new() };
let now = start.elapsed().as_secs_f64();
let mut due = Vec::new();
while inp.next < inp.events.len() && inp.events[inp.next].0 <= now {
let (t, ev, line) = &inp.events[inp.next];
eprintln!("[gex] input at {now:.1} s: {ev:?} ({line})");
due.push((*ev, (t * 1000.0) as u32));
inp.next += 1;
}
due
}
/// Composite on the GPU rather than handing single frames to the host.
/// Returns the adapter, or the reason there is none; either way the
/// compositor keeps working.
@@ -159,6 +296,7 @@ impl Wayland {
/// Write a frame drawn somewhere other than a client buffer (a GL
/// client's host surface): RGBA rows, top first.
pub fn present_external(&self, width: i32, height: i32, rgba: &[u8]) {
self.note_frame();
let Some(prefix) = self.inner.out.lock().unwrap().clone() else {
return;
};
@@ -183,6 +321,9 @@ impl Wayland {
/// kernel locates a buffer's pixels through its shared mappings; guest
/// memory is where they are.
pub fn pump(&self, kernel: &Kernel, mem: &GuestMem<'_>) {
// Scripted input waits for a client that can take it (a seat and a surface).
let ready = self.inner.sessions.lock().unwrap().iter().any(|s| s.client.input_ready());
let due = if ready { self.due_input() } else { Vec::new() };
let mut sessions = self.inner.sessions.lock().unwrap();
let mut produced = Vec::new();
sessions.retain_mut(|s| {
@@ -212,6 +353,17 @@ impl Wayland {
break;
}
}
if !due.is_empty() {
eprintln!("[gex] input: {}", s.client.describe_input());
}
let take = s.client.input_ready();
for &(ev, ms) in due.iter().filter(|_| take) {
match ev {
Ev::Motion(x, y) => s.client.pointer_motion(x, y, ms),
Ev::Button(b, down) => s.client.pointer_button(b, down, ms),
Ev::Key(k, down) => s.client.keyboard_key(k, down, ms),
}
}
s.out.extend_from_slice(&s.client.take_outgoing());
// A `wl_keyboard.keymap` event owes a descriptor for its `h`
// argument: an in-memory file holding the keymap, staged on the
@@ -238,6 +390,7 @@ impl Wayland {
});
drop(sessions);
if !produced.is_empty() {
self.note_frame();
// One screen per batch of commits, not one per surface.
self.save(&produced);
self.inner.frames.lock().unwrap().extend(produced);
+19 -3
View File
@@ -8,7 +8,9 @@
# lib/System.Private.CoreLib.dll). The BCL directory (CoreLib plus .NET 10's
# managed shared framework) is made in RUNDIR. Env: MUCK (the Linux install,
# default ~/gex-sandbox/games-linux/Muck), GEXC, CORES (optional taskset
# list), DOTNET_FW (the .NET 10 shared framework directory).
# list), EXE (another Unity game's executable in MUCK, e.g. hollow_knight.x86_64),
# UNITY_RELAUNCH=1 (run a PIE rebuild of that executable: unity-launcher.c; with it,
# STEAM_APPID=N writes steam_appid.txt beside it), DOTNET_FW (the .NET 10 shared framework directory).
#
# Default: batch mode, no graphics. GFX=GLDIR: with graphics, GL through
# gex-gl (GLDIR holds guest/gl/build.sh's x86-64 build) and X from an
@@ -19,17 +21,31 @@ HERE=$(cd "$(dirname "$0")" && pwd)
OUT=$(realpath "$1"); RUN=$(realpath -m "$2"); SECS=${3:-1800}
shift 2; [ $# -gt 0 ] && shift
MUCK=${MUCK:-$HOME/gex-sandbox/games-linux/Muck}
EXE=${EXE:-Muck.x86_64}
GEXC=${GEXC:-$HERE/../../target/x86/release/gexc}
FW=${DOTNET_FW:-$(dirname "$(dotnet --list-runtimes | awk '/Microsoft.NETCore.App 10\./ {v=$2; p=$3} END {gsub(/[\[\]]/, "", p); print p "/" v "/x"}')")}
mkdir -p "$RUN/bcl" "$RUN/home"
cp "$FW"/*.dll "$RUN/bcl/"
cp "$OUT/lib/System.Private.CoreLib.dll" "$RUN/bcl/"
# The framework's x86-64 natives that have no wasm64 build in the provider: P/Invokes to them go to
# the guest (the provider's dl fallback), which loads them beside the BCL with the host's OpenSSL/zlib.
cp "$FW"/libSystem.Security.Cryptography.Native.OpenSsl.so "$FW"/libSystem.IO.Compression.Native.so "$RUN/bcl/"
if [ -n "${UNITY_RELAUNCH:-}" ]; then
# The game's executable rebuilt PIE (unity-launcher.c) among links to its files.
G="$RUN/game"; mkdir -p "$G"
for f in "$MUCK"/*; do [ "$(basename "$f")" = "$EXE" ] || ln -sfn "$f" "$G/"; done
cc -O1 -fPIE -pie -o "$G/$EXE" "$HERE/unity-launcher.c" -L"$MUCK" -l:UnityPlayer.so -Wl,-rpath,'$ORIGIN'
# STEAM_APPID: Steamworks' development file, so a game started outside Steam runs instead of
# asking Steam to restart it.
[ -n "${STEAM_APPID:-}" ] && echo "$STEAM_APPID" > "$G/steam_appid.txt"
MUCK=$G
fi
cd "$MUCK"
export GEX_WASM_DLLS=libmonobdwgc-2.0.so="$OUT/provider/mono.wasm"
if [ -z "${GFX:-}" ]; then
timeout -s KILL "$SECS" ${CORES:+taskset -c "$CORES"} \
"$GEXC" run --mem64 "$@" --env HOME="$RUN/home" --env GEXMONO_ASSEMBLIES="$RUN/bcl" \
./Muck.x86_64 -batchmode -nographics -logFile "$RUN/player.log" > "$RUN/out.txt" 2>&1 || echo "exit $?"
./$EXE -batchmode -nographics -logFile "$RUN/player.log" > "$RUN/out.txt" 2>&1 || echo "exit $?"
else
N=${XNUM:-71}
[ -e /tmp/.X$N-lock ] && { echo "display :$N is taken (XNUM)"; exit 1; }
@@ -37,7 +53,7 @@ else
"$GEXC" run --mem64 "$@" --gl --wayland "${XDG_RUNTIME_DIR:-/tmp}/muckwl-$$" --wayland-size 800x600 --wayland-out "$RUN/f" \
--env LD_LIBRARY_PATH="$(realpath "$GFX")" --env HOME="$RUN/home" --env GEXMONO_ASSEMBLIES="$RUN/bcl" \
/bin/sh "$HERE/xwayland-inguest.sh" $N "$RUN/xwayland.log" \
./Muck.x86_64 -logFile "$RUN/player.log" -screen-width 800 -screen-height 600 -screen-fullscreen 0 \
./$EXE -logFile "$RUN/player.log" -screen-width 800 -screen-height 600 -screen-fullscreen 0 \
> "$RUN/out.txt" 2>&1 || echo "exit $?"
# Xwayland's lock and socket node are host files; remove only this display's.
rm -f /tmp/.X$N-lock /tmp/.X11-unix/X$N
+13
View File
@@ -0,0 +1,13 @@
/* A Unity Linux player's executable is only `main` calling UnityPlayer.so's
* PlayerMain. Some (Unity 6's, e.g. Hollow Knight's) are linked at 0x200000,
* inside gex's reserved low region; this is the same program built PIE.
* muck-dotnet.sh UNITY_RELAUNCH=1 builds it under the game's executable name
* in a directory of links to the game's files, since the player finds its
* data from its own path (<name>_Data). */
int _Z10PlayerMainiPPc (int argc, char **argv); /* PlayerMain(int, char**) */
int
main (int argc, char **argv)
{
return _Z10PlayerMainiPPc (argc, argv);
}
+4 -3
View File
@@ -1,9 +1,9 @@
#!/bin/sh
# Runs inside gex: Xwayland on gex's compositor, then the program on it.
# usage: inguest.sh DISPLAYNUM LOG prog args...
# Xwayland gets no LD_LIBRARY_PATH: the program's (gex-gl's libGL) is not its.
# Xwayland gets no LD_LIBRARY_PATH or LD_PRELOAD: the program's (gex-gl's libGL, a diagnostic) are not its.
n=$1; log=$2; shift 2
env -u LD_LIBRARY_PATH Xwayland :$n -shm -ac -nolisten tcp > "$log" 2>&1 &
env -u LD_LIBRARY_PATH -u LD_PRELOAD Xwayland :$n -shm -ac -nolisten tcp > "$log" 2>&1 &
xw=$!
i=0
while [ ! -S /tmp/.X11-unix/X$n ]; do
@@ -12,4 +12,5 @@ while [ ! -S /tmp/.X11-unix/X$n ]; do
sleep 1; i=$((i+1))
done
echo "[inguest] X :$n up after $i s"
DISPLAY=:$n exec "$@"
# The program uses X: a Wayland-capable one (Unity 6, SDL) would otherwise pick gex's compositor directly.
DISPLAY=:$n exec env -u WAYLAND_DISPLAY "$@"
+11 -1
View File
@@ -22,12 +22,22 @@ cp "$FW/System.Runtime.dll" "$FW/System.Collections.dll" "$FW/System.Runtime.Int
cd "$T"
GEX_WASM_DLLS=libmonobdwgc-2.0.so="$OUT/provider/mono.wasm" timeout 300 "$GEXC" run --mem64 \
--env GEXMONO_ASSEMBLIES="$T/lib" ./embed ./libmonobdwgc-2.0.so "$T/asm/t.dll" | tee out.txt
if ! { grep -q "^Compute(50) = 50002$" out.txt && grep -q "^Hello = hello gex from t ptr=8$" out.txt && grep -q "^Guest = twice 42; concat guest strings; mix 323.25; scale 2,4,6; big 11,25,52,3; caught from the guest$" out.txt && grep -q "^Back = callback 806.50; unmanaged 1015$" out.txt && grep -q "^Work(200) = 24983091$" out.txt && grep -q "^held across collections: 64 of 64 intact$" out.txt && grep -q "^worker thread: 0 bad of 640$" out.txt && grep -q "^gc ok$" out.txt; }; then echo FAIL; exit 1; fi
PATTERNS=("Compute(50) = 50002" "Hello = hello gex from t ptr=8"
"Guest = twice 42; concat guest strings; mix 323.25; scale 2,4,6; big 11,25,52,3; caught from the guest"
"Back = callback 806.50; unmanaged 1015" "Work(200) = 24983091"
"held across collections: 64 of 64 intact" "worker thread: 0 bad of 640" "gc ok")
for p in "${PATTERNS[@]}"; do grep -a -q -x -F "$p" out.txt || { echo "FAIL: no '$p'"; exit 1; }; done
# The same under the function JIT, which must leave the replaced library's own code alone: it once
# followed the PLT into the stub ("x86 Mono stub ... called"), as it followed Muck's into Boehm's GC.
GEX_WASM_DLLS=libmonobdwgc-2.0.so="$OUT/provider/mono.wasm" timeout 300 "$GEXC" run --mem64 --fjit \
--env GEXMONO_ASSEMBLIES="$T/lib" ./embed ./libmonobdwgc-2.0.so "$T/asm/t.dll" > fjit.txt 2>&1 || true
if ! grep -q "^gc ok$" fjit.txt; then echo "FAIL (--fjit)"; tail -3 fjit.txt; exit 1; fi
# The fork's wasm JIT (GEXMONO_WASM_JIT=1) at threshold 1: every method it accepts runs as a
# wasm64 module compiled by the host, and the results must be the interpreter's.
GEX_WASM_DLLS=libmonobdwgc-2.0.so="$OUT/provider/mono.wasm" timeout 600 "$GEXC" run --mem64 \
--env GEXMONO_WASM_JIT=1 --env MONO_WASM_JIT_THRESHOLD=1 \
--env GEXMONO_ASSEMBLIES="$T/lib" ./embed ./libmonobdwgc-2.0.so "$T/asm/t.dll" > wasmjit.txt 2>&1 || true
for p in "${PATTERNS[@]}"; do grep -a -q -x -F "$p" wasmjit.txt || { echo "FAIL (wasm JIT): no '$p'"; exit 1; }; done
# The control: without scanning the guest's frames, objects held only there die.
GEX_WASM_DLLS=libmonobdwgc-2.0.so="$OUT/provider/mono.wasm" timeout 300 "$GEXC" run --mem64 --env GEXP_NO_GUEST_ROOTS=1 \
--env GEXMONO_ASSEMBLIES="$T/lib" ./embed ./libmonobdwgc-2.0.so "$T/asm/t.dll" > control.txt 2>&1 || true
+48
View File
@@ -0,0 +1,48 @@
/* An X window that prints the input it gets, one line per event, unbuffered:
* the check that scripted input (GEX_WL_INPUT) reaches an X client through the
* in-guest Xwayland.
* xinput-echo [SECONDS] exits after SECONDS (default 60) */
#include <X11/Xlib.h>
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
int
main (int argc, char **argv)
{
int secs = argc > 1 ? atoi (argv [1]) : 60;
setvbuf (stdout, NULL, _IONBF, 0);
printf ("xinput-echo: opening the display\n");
Display *d = XOpenDisplay (NULL);
if (!d) { printf ("no display\n"); return 1; }
Window w = XCreateSimpleWindow (d, DefaultRootWindow (d), 0, 0, 800, 600, 0, 0, 0x336699);
XSelectInput (d, w, KeyPressMask | KeyReleaseMask | ButtonPressMask | ButtonReleaseMask | PointerMotionMask | EnterWindowMask | ExposureMask | FocusChangeMask);
XMapWindow (d, w);
XFlush (d);
printf ("xinput-echo: window mapped\n");
time_t end = time (NULL) + secs;
setvbuf (stdout, NULL, _IONBF, 0);
int motions = 0;
time_t beat = time (NULL);
while (time (NULL) < end) {
if (time (NULL) >= beat + 5) { beat = time (NULL); printf ("alive, %d motions\n", motions); }
while (XPending (d)) {
XEvent e;
XNextEvent (d, &e);
switch (e.type) {
case MotionNotify: if (motions++ % 20 == 0) printf ("motion %d %d\n", e.xmotion.x, e.xmotion.y); break;
case ButtonPress: printf ("button press %u at %d %d\n", e.xbutton.button, e.xbutton.x, e.xbutton.y); break;
case ButtonRelease: printf ("button release %u\n", e.xbutton.button); break;
case KeyPress: printf ("key press %u\n", e.xkey.keycode); break;
case KeyRelease: printf ("key release %u\n", e.xkey.keycode); break;
case EnterNotify: printf ("enter %d %d\n", e.xcrossing.x, e.xcrossing.y); break;
case FocusIn: printf ("focus in\n"); break;
case Expose: if (e.xexpose.count == 0) { XClearWindow (d, w); XFlush (d); } break;
}
}
struct timespec ts = { 0, 20000000 };
nanosleep (&ts, NULL);
}
printf ("motions %d\n", motions);
return 0;
}
+73 -17
View File
@@ -138,40 +138,96 @@ dl_symbol (void *handle, const char *name, char **err, void *user_data)
* A thread attached to the runtime runs guest code in GC Safe (blocking)
* mode, so a collection does not wait for it to poll: gex stops it at its
* next safepoint instead (sgen-stw.c, gexp_stop_guest_threads), and its
* guest stack and registers are scanned conservatively (sgen-mono.c). Each
* crossing into guest code pushes whether it entered the region. */
* guest stack and registers are scanned conservatively (sgen-mono.c).
*
* Calling guest code enters the region unless the thread is already in it
* (the interpreter's P/Invoke transition), and its return leaves only what
* the call entered. Entering from guest code makes the thread Running for
* the call's duration whatever state it was in -- guest code a P/Invoke
* called, calling the embedding API back (Unity does: mono_runtime_invoke,
* mono_gc_get_used_size), is in the interpreter's GC-safe region -- and the
* return to guest code puts it back; a thread that was Running in guest code
* (its first entry attached it) is left GC-safe. */
#include <mono/utils/mono-threads.h>
#include <mono/utils/mono-threads-api.h>
#define GEXMONO_SAFE_MAX 256
static __thread gpointer safe_cookie [GEXMONO_SAFE_MAX];
static __thread int safe_depth;
static __thread gpointer entry_cookie [GEXMONO_SAFE_MAX];
static __thread int entry_depth;
void
gexp_hook_to_guest (void)
void *
gexp_hook_call_guest (void)
{
gpointer cookie = NULL;
MonoThreadInfo *info = mono_thread_info_current_unchecked ();
if (info && mono_thread_info_current_state (info) == STATE_RUNNING) {
gpointer dummy;
cookie = mono_threads_enter_gc_safe_region_unbalanced (&dummy);
return mono_threads_enter_gc_safe_region_unbalanced (&dummy);
}
return NULL;
}
void
gexp_hook_call_return (void *cookie)
{
if (cookie) {
gpointer dummy;
mono_threads_exit_gc_safe_region_unbalanced (cookie, &dummy);
}
if (safe_depth < GEXMONO_SAFE_MAX)
safe_cookie [safe_depth] = cookie;
safe_depth++;
}
void
gexp_hook_from_guest (void)
{
if (!safe_depth)
return; /* the thread's first entry */
safe_depth--;
gpointer cookie = safe_depth < GEXMONO_SAFE_MAX ? safe_cookie [safe_depth] : NULL;
if (cookie) {
gpointer cookie = NULL;
MonoThreadInfo *info = mono_thread_info_current_unchecked ();
if (info && mono_thread_info_current_state (info) != STATE_RUNNING) {
gpointer dummy;
mono_threads_exit_gc_safe_region_unbalanced (cookie, &dummy);
cookie = mono_threads_enter_gc_unsafe_region_unbalanced (&dummy);
}
if (entry_depth < GEXMONO_SAFE_MAX)
entry_cookie [entry_depth] = cookie;
entry_depth++;
}
void
gexp_hook_to_guest (void)
{
gpointer cookie = NULL;
if (entry_depth > 0) {
entry_depth--;
cookie = entry_depth < GEXMONO_SAFE_MAX ? entry_cookie [entry_depth] : NULL;
}
gpointer dummy;
if (cookie) {
mono_threads_exit_gc_unsafe_region_unbalanced (cookie, &dummy);
return;
}
MonoThreadInfo *info = mono_thread_info_current_unchecked ();
if (info && mono_thread_info_current_state (info) == STATE_RUNNING)
mono_threads_enter_gc_safe_region_unbalanced (&dummy);
}
/* A raise that abandoned guest frames (gexp_call_guest_catching): the
* entries inside them never return, and the thread (Running: it raised) goes
* back to the state it called the guest in -- GC-safe when the interpreter's
* P/Invoke had entered the region, which it will leave itself. */
int
gexp_hook_mark (void)
{
MonoThreadInfo *info = mono_thread_info_current_unchecked ();
int safe = info && mono_thread_info_current_state (info) != STATE_RUNNING;
return entry_depth * 2 + safe;
}
void
gexp_hook_unwind (int mark)
{
entry_depth = mark >> 1;
MonoThreadInfo *info = mono_thread_info_current_unchecked ();
if ((mark & 1) && info && mono_thread_info_current_state (info) == STATE_RUNNING) {
gpointer dummy;
mono_threads_enter_gc_safe_region_unbalanced (&dummy);
}
}
+60 -2
View File
@@ -12,6 +12,7 @@
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
#include "gexmono-adapters.h"
#include <mono/metadata/class-internals.h>
#include <mono/metadata/object-internals.h>
@@ -224,6 +225,54 @@ U(mono_thread_pop_appdomain_ref) { ENTER; RETV; }
U(mono_thread_has_sufficient_execution_stack) { ENTER; RET (1); }
U(mono_gchandle_is_in_domain) { ENTER; RET (1); }
U(mono_gchandle_is_in_domain_v2) { ENTER; RET (1); }
/* Unity 6 uses the v2 handle API with Unity's Mono encoding: a v2 handle is the GC's own handle value
* (slot << 3 | type + 1), which Unity's libmonobdwgc also returns from the v1 calls. Its scripting layer
* tests bit 0: set (normal and weak handles), a GC handle; clear, a pointer to an object reference; and its
* managed code takes the same values (GCHandle.FromIntPtr). This tree's runtime and CoreLib use that raw
* value as the handle (gc-internals.h, GCHandle.cs), so v1 and v2 calls are the same functions here. */
MonoGCHandle mono_gchandle_new_internal (MonoObject *obj, gboolean pinned);
MonoGCHandle mono_gchandle_new_weakref_internal (MonoObject *obj, gboolean track_resurrection);
MonoObject *mono_gchandle_get_target_internal (MonoGCHandle gchandle);
void mono_gchandle_free_internal (MonoGCHandle gchandle);
#define UNITY_H(h) ((uintptr_t)(h))
#define GC_H(u) ((MonoGCHandle)(uintptr_t)(u))
/* Unity reads handles after freeing them (Hollow Knight: get_target on a freed handle), which Unity's
* Boehm-backed Mono answers with NULL and SGen asserts on. So the handles given out are remembered, and a
* read or free of one that is not live is NULL / nothing. */
static GHashTable *live_handles;
static pthread_mutex_t live_lock = PTHREAD_MUTEX_INITIALIZER;
static uintptr_t
handle_out (MonoGCHandle h)
{
uintptr_t u = UNITY_H (h);
pthread_mutex_lock (&live_lock);
if (!live_handles)
live_handles = g_hash_table_new (NULL, NULL);
g_hash_table_add (live_handles, (gpointer)u);
pthread_mutex_unlock (&live_lock);
if (gexmono_trace_on)
fprintf (stderr, "[gexmono] -> gchandle %#llx\n", (unsigned long long)u);
return u;
}
static gboolean
handle_live (uintptr_t u, gboolean drop)
{
pthread_mutex_lock (&live_lock);
gboolean live = live_handles && (drop ? g_hash_table_remove (live_handles, (gpointer)u) : g_hash_table_contains (live_handles, (gpointer)u));
pthread_mutex_unlock (&live_lock);
if (!live)
fprintf (stderr, "[gexmono] gchandle %#llx is not live (%s)\n", (unsigned long long)u, drop ? "free" : "get_target");
return live;
}
U(mono_gchandle_new_v2) { ENTER; RET (handle_out (mono_gchandle_new_internal ((MonoObject *)P (0), (gboolean)A (1)))); }
U(mono_gchandle_new_weakref_v2) { ENTER; RET (handle_out (mono_gchandle_new_weakref_internal ((MonoObject *)P (0), (gboolean)A (1)))); }
U(mono_gchandle_get_target_v2) { ENTER; RET (handle_live (A (0), FALSE) ? (uintptr_t)mono_gchandle_get_target_internal (GC_H (A (0))) : 0); }
U(mono_gchandle_free_v2) { ENTER; if (handle_live (A (0), TRUE)) mono_gchandle_free_internal (GC_H (A (0))); RETV; }
/* The v1 calls too, in the same encoding: Unity mixes them (a handle from one, freed or read by the other). */
U(mono_gchandle_new) { ENTER; RET ((uint32_t)handle_out (mono_gchandle_new_internal ((MonoObject *)P (0), (gboolean)A (1)))); }
U(mono_gchandle_new_weakref) { ENTER; RET ((uint32_t)handle_out (mono_gchandle_new_weakref_internal ((MonoObject *)P (0), (gboolean)A (1)))); }
U(mono_gchandle_get_target) { ENTER; RET (handle_live ((uint32_t)A (0), FALSE) ? (uintptr_t)mono_gchandle_get_target_internal (GC_H ((uint32_t)A (0))) : 0); }
U(mono_gchandle_free) { ENTER; if (handle_live ((uint32_t)A (0), TRUE)) mono_gchandle_free_internal (GC_H ((uint32_t)A (0))); RETV; }
U(mono_pmip) { ENTER; RET (0); }
U(mono_debug_lookup_source_location_by_il) { ENTER; RET (0); }
@@ -340,6 +389,15 @@ set_assemblies_path (const char *unity_path)
g_string_free (s, TRUE);
}
/* Unity 6 maps DllImport names (mono_dllmap_insert), which .NET's Mono does not have: noted, and left
* to the find-plugin callback and dl fallback, which see the original name. */
U(mono_dllmap_insert)
{
ENTER;
fprintf (stderr, "[gexmono] dllmap %s%s%s -> %s%s%s (ignored)\n", P (1) ? (const char *)P (1) : "?", P (2) ? ":" : "",
P (2) ? (const char *)P (2) : "", P (3) ? (const char *)P (3) : "?", P (4) ? ":" : "", P (4) ? (const char *)P (4) : "");
RETV;
}
U(mono_set_dirs) { ENTER; set_assemblies_path (P (0)); RETV; }
U(mono_set_assemblies_path) { ENTER; set_assemblies_path (P (0)); RETV; }
U(mono_set_assemblies_path_null_separated)
@@ -395,7 +453,7 @@ __attribute__((used)) gexp_body_t const gexmono_unity_adapters [] = {
L(mono_runtime_unhandled_exception_policy_set) L(mono_threads_set_shutting_down)
L(mono_thread_suspend_all_other_threads) L(mono_thread_pool_cleanup) L(mono_thread_push_appdomain_ref)
L(mono_thread_pop_appdomain_ref) L(mono_thread_has_sufficient_execution_stack) L(mono_gchandle_is_in_domain)
L(mono_gchandle_is_in_domain_v2) L(mono_pmip) L(mono_debug_lookup_source_location_by_il)
L(mono_gchandle_is_in_domain_v2) L(mono_gchandle_new_v2) L(mono_gchandle_new_weakref_v2) L(mono_gchandle_get_target_v2) L(mono_gchandle_free_v2) L(mono_gchandle_new) L(mono_gchandle_new_weakref) L(mono_gchandle_get_target) L(mono_gchandle_free) L(mono_pmip) L(mono_debug_lookup_source_location_by_il)
L(mono_unity_gc_set_mode) L(mono_unity_gc_disable) L(mono_unity_gc_enable) L(mono_unity_gc_is_disabled)
L(mono_gc_is_incremental) L(mono_gc_set_incremental) L(mono_gc_collect_a_little) L(mono_gc_get_max_time_slice_ns)
L(mono_gc_set_max_time_slice_ns) L(mono_gc_start_incremental_collection) L(mono_gc_set_heap_validate_frequency)
@@ -413,7 +471,7 @@ __attribute__((used)) gexp_body_t const gexmono_unity_adapters [] = {
L(mono_profiler_create) L(mono_profiler_enable_coverage) L(mono_profiler_set_coverage_filter_callback)
L(mono_profiler_get_coverage_data) L(mono_profiler_get_all_coverage_data) L(mono_profiler_reset_coverage)
L(mono_profiler_reset_all_coverage) L(mono_assembly_foreach) L(mono_stack_walk) L(mono_stack_walk_no_il)
L(mono_trace_set_log_handler) L(mono_set_dirs) L(mono_set_assemblies_path) L(mono_set_assemblies_path_null_separated)
L(mono_trace_set_log_handler) L(mono_dllmap_insert) L(mono_set_dirs) L(mono_set_assemblies_path) L(mono_set_assemblies_path_null_separated)
L(mono_set_find_plugin_callback) L(mono_dl_fallback_register)
L(mono_class_get_field_from_name) L(mono_custom_attrs_from_field) L(unity_mono_method_is_generic) L(unity_mono_method_is_inflated) L(unity_mono_reflection_method_get_method)
0
@@ -1,3 +1,41 @@
diff --git a/src/libraries/System.Private.CoreLib/src/System/Runtime/InteropServices/GCHandle.cs b/src/libraries/System.Private.CoreLib/src/System/Runtime/InteropServices/GCHandle.cs
index 060143b50..eaed95671 100644
--- a/src/libraries/System.Private.CoreLib/src/System/Runtime/InteropServices/GCHandle.cs
+++ b/src/libraries/System.Private.CoreLib/src/System/Runtime/InteropServices/GCHandle.cs
@@ -46,11 +46,13 @@ private GCHandle(object? value, GCHandleType type)
IntPtr handle = InternalAlloc(value, type);
+#if !MONO
if (type == GCHandleType.Pinned)
{
// Record if the handle is pinned.
handle |= 1;
}
+#endif
_handle = handle;
}
@@ -180,10 +182,19 @@ public static GCHandle FromIntPtr(IntPtr value)
public static bool operator !=(GCHandle a, GCHandle b) => a._handle != b._handle;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
+#if MONO
+ // gex's Mono: handles are the GC's raw values, as Unity's native code passes them (gc-internals.h);
+ // the low three bits are the handle type + 1, so a pinned handle (type 3) ends in 4.
+ private static IntPtr GetHandleValue(IntPtr handle) => handle;
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static bool IsPinned(IntPtr handle) => (handle & 7) == 4;
+#else
private static IntPtr GetHandleValue(IntPtr handle) => new IntPtr(handle & ~1); // Remove Pin flag
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool IsPinned(IntPtr handle) => (handle & 1) != 0; // Check Pin flag
+#endif
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void ThrowIfInvalid(IntPtr handle)
diff --git a/src/mono/cmake/config.h.in b/src/mono/cmake/config.h.in
index 7d2b81fc9..56a529ae6 100644
--- a/src/mono/cmake/config.h.in
@@ -13,6 +51,24 @@ index 7d2b81fc9..56a529ae6 100644
+#if defined(HOST_BROWSER) || defined(HOST_GEX)
+#define WJ_RUNTIME 1
+#endif
diff --git a/src/mono/mono/metadata/gc-internals.h b/src/mono/mono/metadata/gc-internals.h
index f0141675b..632d3eb4f 100644
--- a/src/mono/mono/metadata/gc-internals.h
+++ b/src/mono/mono/metadata/gc-internals.h
@@ -30,8 +30,11 @@
* are used to convert between the old int32 representation to a netcore compatible pointer
* representation.
*/
-#define MONO_GC_HANDLE_TO_UINT(ptr) ((guint32)((size_t)(ptr) >> 1))
-#define MONO_GC_HANDLE_FROM_UINT(i) ((MonoGCHandle)((size_t)(i) << 1))
+/* gex: a handle is the GC's own value, unshifted, as in Unity's Mono, whose native and managed code (UnityPlayer
+ * and UnityEngine's GCHandle.FromIntPtr) pass the same raw values both ways. CoreLib's GCHandle is patched to
+ * match: no pin bit, a pinned handle is told by its type bits (the GC's type + 1 in the low three bits). */
+#define MONO_GC_HANDLE_TO_UINT(ptr) ((guint32)(size_t)(ptr))
+#define MONO_GC_HANDLE_FROM_UINT(i) ((MonoGCHandle)(size_t)(i))
/*
* Return a GC descriptor for an array containing N pointers to memory allocated
* by mono_gc_alloc_fixed ().
diff --git a/src/mono/mono/metadata/sgen-mono.c b/src/mono/mono/metadata/sgen-mono.c
index ed1afb5a7..45839cad6 100644
--- a/src/mono/mono/metadata/sgen-mono.c
@@ -0,0 +1,53 @@
diff --git a/src/mono/mono/metadata/sgen-stw.c b/src/mono/mono/metadata/sgen-stw.c
index b7f15460e..2eebcbb12 100644
--- a/src/mono/mono/metadata/sgen-stw.c
+++ b/src/mono/mono/metadata/sgen-stw.c
@@ -390,6 +390,18 @@ unified_suspend_stop_world (MonoThreadInfoFlags flags, unified_suspend_thread_st
continue;
}
+#ifdef HOST_GEX
+ /* gex: a stop-the-world that keeps restarting a thread still in a critical location: say why, once. */
+ if (sleep_duration == 20000) {
+ MonoThreadUnwindState *st = mono_thread_info_get_suspend_state (info);
+ g_warning ("gex STW: thread %p still critical: platform %d inside %d gc %d domain %p sp %p ip %p stack [%p, %p) state %d",
+ mono_thread_info_get_tid (info), mono_threads_platform_in_critical_region (info),
+ info->client_info.info.inside_critical_region, info->client_info.in_critical_region,
+ st->unwind_data [MONO_UNWIND_DATA_DOMAIN], MONO_CONTEXT_GET_SP (&st->ctx),
+ MONO_CONTEXT_GET_IP (&st->ctx), info->client_info.info.stack_start_limit, info->client_info.info.stack_end,
+ mono_thread_info_current_state (info));
+ }
+#endif
suspend_count = mono_thread_info_suspend_count (info);
if (!(suspend_count == 1))
g_error ("[%p] suspend_count = %d, but should be 1", mono_thread_info_get_tid (info), suspend_count);
diff --git a/src/mono/mono/utils/mono-threads-gex.c b/src/mono/mono/utils/mono-threads-gex.c
index 0036d0da9..8d5dfb7de 100644
--- a/src/mono/mono/utils/mono-threads-gex.c
+++ b/src/mono/mono/utils/mono-threads-gex.c
@@ -28,10 +28,25 @@ mono_threads_suspend_init (void)
* of its guest stack. */
void gexp_thread_guest_info (guint64 *ctx, guint64 *stack_end);
+#include <stdlib.h>
+#include <stdio.h>
+#include <unistd.h>
+#include <sys/syscall.h>
+
void
mono_threads_suspend_register (MonoThreadInfo *info)
{
gexp_thread_guest_info (&info->gex_ctx, &info->gex_stack_end);
+ /* GEXMONO_TRACE_THREADS=1: each thread as it registers, with the C stack Mono will check its saved
+ * states against. */
+ static int trace = -1;
+ if (trace < 0)
+ trace = getenv ("GEXMONO_TRACE_THREADS") != NULL;
+ if (trace) {
+ int here;
+ fprintf (stderr, "[gexmono] thread %ld (%p) registers: stack [%p, %p), sp %p\n", (long)syscall (SYS_gettid),
+ (void *)mono_thread_info_get_tid (info), info->stack_start_limit, info->stack_end, (void *)&here);
+ }
}
void
+58 -7
View File
@@ -18,28 +18,45 @@ __attribute__((import_module("gex"), import_name("provider_stop_guest"))) void g
__attribute__((import_module("gex"), import_name("provider_start_guest"))) void gexp_host_start_guest(void);
__attribute__((import_module("gex"), import_name("provider_guest_stack_end"))) uint64_t gexp_host_guest_stack_end(uint64_t sp);
/* A runtime's hooks at the crossing (a GC-safe region while in guest code). */
/* A runtime's hooks at the crossing (a GC-safe region while in guest code).
* Entering from guest code and returning to it (an adapter: gexp_enter,
* gexp_return) are one pair; calling guest code and its return
* (gexp_call_guest) the other, whose cookie the caller keeps. A raise that
* abandons guest frames resets the entry pairs to a mark. */
static void no_hook(void) {}
static void *no_call_hook(void) { return 0; }
static void no_call_return(void *cookie) {}
static int no_mark(void) { return 0; }
static void no_unwind(int mark) {}
__attribute__((weak, alias("no_hook"))) void gexp_hook_to_guest(void);
__attribute__((weak, alias("no_hook"))) void gexp_hook_from_guest(void);
__attribute__((weak, alias("no_call_hook"))) void *gexp_hook_call_guest(void);
__attribute__((weak, alias("no_call_return"))) void gexp_hook_call_return(void *cookie);
__attribute__((weak, alias("no_mark"))) int gexp_hook_mark(void);
__attribute__((weak, alias("no_unwind"))) void gexp_hook_unwind(int mark);
void gexp_attach_thread(void)
{
gexp_host_thread_register(&gexp_in_guest, &gexp_stop);
}
static void in_guest(int v)
{
__atomic_store_n(&gexp_in_guest, v, __ATOMIC_SEQ_CST);
if (v && __atomic_load_n(&gexp_stop, __ATOMIC_SEQ_CST))
gexp_host_park();
}
void gexp_from_guest(void)
{
__atomic_store_n(&gexp_in_guest, 0, __ATOMIC_SEQ_CST);
in_guest(0);
gexp_hook_from_guest();
}
void gexp_to_guest(void)
{
gexp_hook_to_guest();
__atomic_store_n(&gexp_in_guest, 1, __ATOMIC_SEQ_CST);
if (__atomic_load_n(&gexp_stop, __ATOMIC_SEQ_CST))
gexp_host_park();
in_guest(1);
}
void gexp_stop_guest_threads(void)
@@ -119,7 +136,8 @@ void gexp_call_guest(uint64_t fn, gexp_call *c)
for (unsigned i = 0; i < c->nxmm && i < 8; i++)
*(volatile uint64_t *)((uintptr_t)(uint32_t)ctx + GEXP_CTX_V + 16 * i) = c->xmm[i];
GEXP_X(ctx, 4) = sp;
gexp_to_guest();
void *cookie = gexp_hook_call_guest();
in_guest(1);
/* Run the guest until it returns to `ra`. A function body returns 1 when
* its return went to the address its caller predicted; otherwise 0, with
* PC where the guest continues (a longjmp inside the callee, a signal, a
@@ -139,7 +157,8 @@ void gexp_call_guest(uint64_t fn, gexp_call *c)
gexp_ctx = ctx;
gexp_depth = depth;
__gex_set_ctx((uint32_t)ctx);
gexp_from_guest();
in_guest(0);
gexp_hook_call_return(cookie);
GEXP_X(ctx, 4) = rsp0;
c->rax = GEXP_X(ctx, 0);
c->rdx = GEXP_X(ctx, 2);
@@ -177,6 +196,7 @@ int gexp_catching(void)
uint64_t gexp_call_guest_catching(uint64_t fn, gexp_call *c)
{
catching k = { catch_top, fn, c, 0, gexp_ctx, gexp_depth };
int mark = gexp_hook_mark();
for (int i = 0; i < 16; i++)
k.saved[i] = GEXP_X(k.ctx, i);
catch_top = &k;
@@ -191,6 +211,8 @@ uint64_t gexp_call_guest_catching(uint64_t fn, gexp_call *c)
gexp_ctx = k.ctx;
gexp_depth = k.depth;
__gex_set_ctx((uint32_t)k.ctx);
in_guest(0);
gexp_hook_unwind(mark);
if (code != GEXP_RAISE_CODE)
gexp_host_throw(code); /* someone else's: keep going */
return k.payload ? k.payload : 1;
@@ -274,3 +296,32 @@ int gexp_reverse_return(int ctx, uint64_t ret, const gexp_call *c)
*(volatile uint64_t *)((uintptr_t)(uint32_t)ctx + GEXP_CTX_V + 16) = c->xmm1;
return gexp_return(ctx, ret);
}
/* ---- signals at a provider's syscalls ----
* A signal handled at a syscall's return (musl wasm64's syscall stub): gex-kernel has left an x86-64
* rt_sigframe on the guest stack, RSP at it (its first word the restorer's return address), the handler's
* arguments in rdi/rsi/rdx and PC at the handler, which is guest code. Run it by the guest ABI, then do
* what the restorer does (pop the return address, rt_sigreturn), which puts back the interrupted context,
* the syscall's result and PC included; the loop ends when PC is back where the stub was. Overrides musl's
* default, which stops. */
#define X86_64_RT_SIGRETURN 15
__attribute__((import_module("gex"), import_name("syscall"))) void gexp_host_syscall_ctx(int ctx, long long pc);
void __gex_signal_frames(volatile unsigned long long *x, unsigned long long pc)
{
/* A thread of the provider's own (not entered from guest code) has no gexp_ctx yet; this is its context. */
if (!gexp_ctx)
gexp_ctx = (int)(uintptr_t)x;
while (x[32] != pc) {
uint64_t handler = x[32], frame = x[4];
gexp_call c = { 0 };
c.ireg[0] = x[7]; /* signal */
c.ireg[1] = x[6]; /* &info */
c.ireg[2] = x[2]; /* &uc */
c.nireg = 3;
gexp_call_guest(handler, &c);
x[4] = frame + 8;
x[0] = X86_64_RT_SIGRETURN;
gexp_host_syscall_ctx((int)(uintptr_t)x, 0);
}
}