mirror of
https://github.com/par274/sharpemu.git
synced 2026-07-22 19:06:15 +08:00
f23161be9a
* [Host] Introduce host platform abstraction with IHostMemory Add SharpEmu.HLE/Host with IHostPlatform/IHostMemory interfaces, neutral page-protection/region enums, and a HostPlatform.Current factory that resolves the Windows backend (or throws PlatformNotSupportedException on other OSes, matching today's de-facto behavior). WindowsHostMemory wraps the exact VirtualAlloc/VirtualFree/VirtualProtect/VirtualQuery calls used across the engine today, with identical MEM_*/PAGE_* constants. Migrate StubManager as the first consumer: its private kernel32 P/Invokes and enums are replaced by IHostMemory calls that issue the same two native operations (RWX commit+reserve of the PLT arena, release on Dispose). No behavior change. This is the first step toward supporting non-Windows hosts; subsequent commits move the remaining direct P/Invokes in Core and Libs behind the same seam. * [Host] Route PhysicalVirtualMemory through IHostMemory Replace the class's private VirtualAlloc/VirtualFree/VirtualProtect/ VirtualQuery P/Invokes with IHostMemory calls. Every site maps 1:1 onto the exact native call it issued before: MEM_COMMIT|MEM_RESERVE -> Allocate, MEM_RESERVE -> Reserve, fault-path commits -> Commit, and MEM_RELEASE -> Free, with identical protection values produced by the Windows backend. IHostMemory gains ProtectRaw so the save/restore protection sequences in TryWriteExclusive and TryTemporarilyProtectForRead round-trip the raw OS protection word (including modifier bits the neutral enum cannot represent) exactly as before. Raw PAGE_* constants remain only for the internal region-classification helpers, which only ever see values this class itself assigned. The exact-address free-on-mismatch, lazy reserve-only threshold, prime loop, and all trace strings are unchanged. * [Host] Add IGuestAddressSpace and retire the reflection-based allocator lookup Introduce IGuestAddressSpace in SharpEmu.HLE (fixed-address AllocateAt / TryAllocateAtOrAbove and guest mprotect via TryProtect) with signatures copied from PhysicalVirtualMemory, which now implements it. TryProtect reproduces the read/write/execute decomposition that KernelMemoryCompatExports.ResolveHostProtection performs, yielding the same PAGE_* values through the Windows backend. KernelVirtualRangeAllocator previously located AllocateAt via cached MethodInfo reflection (because SharpEmu.Libs cannot see Core types) and walked wrapper memories through an untyped 'Inner' property. Both are now typed: ICpuMemoryWrapper exposes the decorated memory (implemented by TrackedCpuMemory, whose Inner property already existed) and the allocator type-tests for IGuestAddressSpace with the same bounded unwrap depth. Failure paths keep the exact [LOADER][TRACE] strings. * [Host] Move Kernel HLE memory exports off direct kernel32 P/Invokes KernelMemoryCompatExports loses its private VirtualQuery/VirtualProtect/ VirtualAlloc/VirtualFree declarations and MemoryBasicInformation struct: - Guest mprotect (sceKernelMprotect/sceKernelMtypeprotect) now routes through IGuestAddressSpace.TryProtect resolved from ctx.Memory. The orbis read/write/execute decomposition moves into a GuestPageProtection conversion whose mapping is value-identical to the removed ResolveHostProtection. - The guarded libc heap and host-page accessibility checks go through IHostMemory (same commit+reserve/protect/free sequence; guard-page and protection-mask checks compare HostRegionInfo.RawProtection against the same PAGE_* literals as before). - HostMemory is exposed as a property so merely loading the type never resolves the platform backend on non-Windows hosts. KernelRuntimeCompatExports' RDTSC stub allocates its 16-byte RWX page via IHostMemory.Allocate; the OperatingSystem.IsWindows() gate returning null is unchanged. * [Host] Abstract thread, TLS, and symbol primitives in the execution backend Add IHostThreading (native TLS slots, current-thread id, affinity, raw thread create/join, diagnostic register capture) and IHostSymbolResolver (enum-keyed host function addresses baked into emitted stubs), with Windows implementations wrapping the exact kernel32 calls the backend made directly before. DirectExecutionBackend takes an optional IHostPlatform (defaulting to HostPlatform.Current) and routes every TlsAlloc/TlsFree/TlsSet/GetValue, GetCurrentThreadId, SetThreadAffinityMask, GetModuleHandle/GetProcAddress and the suspend+GetThreadContext diagnostic snapshot through it. The snapshot moves wholesale into WindowsHostThreading (including the Win64 CONTEXT size/flags/offsets, which are Windows-specific by nature) and returns a neutral HostCapturedRegisters. NativeGuestExecutor resolves WaitForSingleObject/SetEvent/ExitThread via the symbol resolver — the same addresses end up in the emitted run loop, so stub bytes are unchanged — and creates/joins its raw worker thread through IHostThreading with the same stack-reservation semantics. The run-loop emitter itself does not move. Marshal.GetLastWin32Error() in the affinity-failure log still observes SetThreadAffinityMask's error because the wrapper makes no intervening SetLastError call. * [Host] Move fault handling and remaining backend memory ops behind the seam Add IHostFaultHandling (handler-thunk creation, first-chance handler install/remove, unhandled-filter set) with WindowsFaultHandling in a new Cpu/Native/Windows/ folder. The exception-handler trampoline emitter moves there whole — same pre-filtered NTSTATUS codes, same TEB gs:[8]/ gs:[0x10] stack-limit reads, same host-RSP TLS switch — parameterized only by (managed callback, TLS slot, TlsGetValue address), which is exactly what SetupExceptionHandler passed it before. Handler installation order, the AddVectoredExceptionHandler(first=1) flag, the SHARPEMU_DISABLE_RAW_HANDLER gate, and all install/teardown log strings are unchanged. Every remaining VirtualAlloc/VirtualProtect/VirtualFree/VirtualQuery/ FlushInstructionCache in the backend partials routes through IHostMemory with 1:1 call mapping (RWX emit -> RX downgrade -> flush for stub emission, reserve/commit for the PRT aperture and lazy-commit fault path, raw-protection round-trips via ProtectRaw). HostRegionInfo gains RawState/RawAllocationProtection so the lazy-commit trace lines and protection-mask checks keep printing and comparing the exact native values. Windows semantics leaked as bare literals become named constants with identical values: NTSTATUS codes (WindowsFaultCodes) and Win64 CONTEXT byte offsets (Win64ContextOffsets, with the existing CTX_* constants aliased to it and handler-local numeric offsets replaced by the names). * [Host] Resolve the host platform explicitly at the composition root SharpEmuRuntime.CreateDefault() now resolves HostPlatform.Current once and passes it explicitly to PhysicalVirtualMemory and (via a new optional CpuDispatcher parameter) to DirectExecutionBackend, replacing the implicit default-argument fallbacks. On unsupported OSes boot now fails at the root with PlatformNotSupportedException and a clear message instead of on the first native call. A future Linux/macOS backend plugs in by returning a different IHostPlatform here. * [Host] Convert the platform backends to source-generated P/Invokes Replace [DllImport] with [LibraryImport] in the four Windows backend files added by this branch (WindowsHostMemory, WindowsHostThreading, WindowsHostSymbolResolver, WindowsFaultHandling). Marshalling stubs are now generated at compile time instead of JIT-emitted at runtime, which fits the pre-JIT-everything boot model and keeps the backends NativeAOT/trimming ready. Interop stays zero-copy: all signatures are blittable, GetModuleHandleW now pins the managed string via Utf16 marshalling instead of copying, and GetProcAddress names marshal through a stack-allocated Utf8 buffer. Implicit contracts become explicit where LibraryImport requires it: TlsFree/TlsSetValue gain [MarshalAs(UnmanagedType.Bool)] (the 4-byte Win32 BOOL DllImport assumed silently), and GetModuleHandle targets the W entry point directly since LibraryImport never probes suffixes. The CONTEXT snapshot buffer stays a NativeMemory allocation rather than stackalloc: CONTEXT requires 16-byte alignment, now documented at the call site. Native call sequences are unchanged. * [Host] Address Copilot review: harden failure paths, honor injected platform - Free the handler thunk page when the RX protection downgrade fails (the leak predates this branch, but the failure path is boot-fatal so releasing the page is unobservable). - TraceThreadMode and the static diagnostics helpers now resolve host primitives through the backend bound to the current thread, falling back to HostPlatform.Current only when no run is active (identical on supported configs, honors injection everywhere a backend exists). - HostPlatform.Create additionally requires an x64 process so native Windows ARM64 fails with the promised PlatformNotSupportedException instead of emitting x86-64 stubs into an ARM64 process.
563 lines
18 KiB
C#
563 lines
18 KiB
C#
// Copyright (C) 2026 SharpEmu Emulator Project
|
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Runtime.InteropServices;
|
|
using System.Threading;
|
|
using SharpEmu.HLE;
|
|
using SharpEmu.HLE.Host;
|
|
|
|
namespace SharpEmu.Core.Cpu.Native;
|
|
|
|
public sealed partial class DirectExecutionBackend
|
|
{
|
|
// Guest entry stubs must not run above CLR-managed frames on a CLR-created thread:
|
|
// the suspension/stack-walk machinery then has to traverse a frame chain that is
|
|
// interleaved with guest stubs carrying no CLR unwind info, and any window in which
|
|
// the thread-mode bookkeeping disagrees with the actual stack (import dispatch, VEH
|
|
// redirection) fail-fasts the runtime — the audio_output_thread ~7th-cycle
|
|
// "attempted to call a UnmanagedCallersOnly method from managed code" crash.
|
|
//
|
|
// Guest execution therefore runs on dedicated raw OS threads whose run loop is
|
|
// emitted native code. While guest code executes there is not a single managed
|
|
// frame on the thread and it sits in preemptive mode, so the GC ignores it
|
|
// entirely. The only managed activity on the thread is the reverse-P/Invoke import
|
|
// dispatch and the per-run prologue/epilogue, which the CLR supports on foreign
|
|
// threads (lazy attach, preemptive between calls). The managed orchestrator
|
|
// (RunGuestThread / the main execution thread) parks in a preemptive wait for the
|
|
// duration of the run, so its own frame chain is never walked across guest frames.
|
|
private static readonly bool NativeGuestWorkersDisabled =
|
|
string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_DISABLE_NATIVE_GUEST_WORKERS"), "1", StringComparison.Ordinal);
|
|
|
|
private readonly object _nativeWorkerGate = new();
|
|
private readonly List<NativeGuestExecutor> _allNativeWorkers = new();
|
|
private readonly Stack<NativeGuestExecutor> _idleNativeWorkers = new();
|
|
private bool _nativeWorkersDisposed;
|
|
private int _nativeWorkerCreationFailedLogged;
|
|
|
|
// Runs an emitted guest entry stub. Preferred path is a pooled native worker
|
|
// thread; falls back to the historical inline calli (guest frames above this
|
|
// thread's managed frames) when workers are disabled or unavailable.
|
|
//
|
|
// Callers set the Active* thread-statics before emitting the stub and read the
|
|
// yield/forced-exit flags right after this returns, so the worker outcome is
|
|
// copied back into this thread's statics before returning.
|
|
private unsafe int RunGuestEntryStub(void* entryStub, ulong hostRspSlot)
|
|
{
|
|
var worker = RentNativeGuestExecutor();
|
|
if (worker is null)
|
|
{
|
|
_hostThreading.SetTlsValue(_hostRspSlotTlsIndex, (nint)hostRspSlot);
|
|
return CallNativeEntry(entryStub);
|
|
}
|
|
try
|
|
{
|
|
var state = _activeGuestThreadState;
|
|
var nativeReturn = worker.Run(
|
|
_activeCpuContext!,
|
|
state,
|
|
GuestThreadExecution.CurrentGuestThreadHandle,
|
|
_activeEntryReturnSentinelRip,
|
|
_activeGuestReturnSlotAddress,
|
|
(nint)hostRspSlot,
|
|
(nint)entryStub,
|
|
state?.AffinityMask ?? 0,
|
|
out var yieldRequested,
|
|
out var yieldReason,
|
|
out var forcedExit);
|
|
_activeGuestThreadYieldRequested = yieldRequested;
|
|
_activeGuestThreadYieldReason = yieldReason;
|
|
_activeForcedGuestExit = forcedExit;
|
|
return nativeReturn;
|
|
}
|
|
finally
|
|
{
|
|
ReturnNativeGuestExecutor(worker);
|
|
}
|
|
}
|
|
|
|
private NativeGuestExecutor? RentNativeGuestExecutor()
|
|
{
|
|
if (NativeGuestWorkersDisabled)
|
|
{
|
|
return null;
|
|
}
|
|
lock (_nativeWorkerGate)
|
|
{
|
|
if (_nativeWorkersDisposed)
|
|
{
|
|
return null;
|
|
}
|
|
if (_idleNativeWorkers.Count > 0)
|
|
{
|
|
return _idleNativeWorkers.Pop();
|
|
}
|
|
}
|
|
var worker = NativeGuestExecutor.TryCreate(this);
|
|
if (worker is null)
|
|
{
|
|
if (Interlocked.Exchange(ref _nativeWorkerCreationFailedLogged, 1) == 0)
|
|
{
|
|
Console.Error.WriteLine(
|
|
"[LOADER][WARN] Failed to create a native guest worker thread; falling back to inline guest execution.");
|
|
}
|
|
return null;
|
|
}
|
|
lock (_nativeWorkerGate)
|
|
{
|
|
if (_nativeWorkersDisposed)
|
|
{
|
|
worker.Dispose();
|
|
return null;
|
|
}
|
|
_allNativeWorkers.Add(worker);
|
|
}
|
|
return worker;
|
|
}
|
|
|
|
private void ReturnNativeGuestExecutor(NativeGuestExecutor worker)
|
|
{
|
|
lock (_nativeWorkerGate)
|
|
{
|
|
if (!_nativeWorkersDisposed)
|
|
{
|
|
_idleNativeWorkers.Push(worker);
|
|
return;
|
|
}
|
|
}
|
|
worker.Dispose();
|
|
}
|
|
|
|
private void DisposeNativeGuestExecutors()
|
|
{
|
|
NativeGuestExecutor[] workers;
|
|
lock (_nativeWorkerGate)
|
|
{
|
|
if (_nativeWorkersDisposed)
|
|
{
|
|
return;
|
|
}
|
|
_nativeWorkersDisposed = true;
|
|
workers = _allNativeWorkers.ToArray();
|
|
_allNativeWorkers.Clear();
|
|
_idleNativeWorkers.Clear();
|
|
}
|
|
foreach (var worker in workers)
|
|
{
|
|
worker.Dispose();
|
|
}
|
|
}
|
|
|
|
// A pooled raw OS thread that executes guest entry stubs. The run loop is emitted
|
|
// native code (no CLR unwind info required — nothing ever unwinds through it):
|
|
//
|
|
// loop: WaitForSingleObject(work);
|
|
// if (*stopFlag) { SetEvent(done); ExitThread(0); }
|
|
// rax = RunPrologue(self); // managed: binds per-run ambient, returns stub
|
|
// if (rax != 0) eax = rax(); // guest entry stub — zero managed frames below
|
|
// RunEpilogue(self, eax); // managed: captures outcome, restores ambient
|
|
// SetEvent(done); goto loop;
|
|
//
|
|
// Workers carry no per-guest identity of their own: the prologue rebinds guest TLS,
|
|
// the host-RSP slot, the Active* thread-statics and the GuestThreadExecution ambient
|
|
// on every run, so a worker can be reused for any guest thread.
|
|
private sealed unsafe class NativeGuestExecutor : IDisposable
|
|
{
|
|
private const uint LoopStubSize = 512u;
|
|
private const uint WorkerStackReservation = 4u * 1024u * 1024u;
|
|
|
|
private static nint _waitForSingleObjectAddress;
|
|
private static nint _setEventAddress;
|
|
private static nint _exitThreadAddress;
|
|
|
|
private readonly DirectExecutionBackend _backend;
|
|
private readonly AutoResetEvent _workAvailable = new(false);
|
|
private readonly AutoResetEvent _workCompleted = new(false);
|
|
private GCHandle _selfHandle;
|
|
private void* _controlBlock;
|
|
private void* _loopStub;
|
|
private nint _threadHandle;
|
|
private uint _nativeThreadId;
|
|
|
|
// Single in-flight run; publication is ordered by the work/done event pair.
|
|
private CpuContext? _runContext;
|
|
private GuestThreadState? _runState;
|
|
private ulong _runGuestThreadHandle;
|
|
private ulong _runSentinelRip;
|
|
private ulong _runReturnSlotAddress;
|
|
private nint _runHostRspSlot;
|
|
private nint _runEntryStub;
|
|
private ulong _runAffinityMask;
|
|
private int _runNativeResult;
|
|
private bool _runYieldRequested;
|
|
private string? _runYieldReason;
|
|
private bool _runForcedExit;
|
|
private bool _runPrologueFailed;
|
|
|
|
// Prologue -> epilogue carry, only touched on the worker thread.
|
|
private DirectExecutionBackend? _prevBackend;
|
|
private CpuContext? _prevContext;
|
|
private ulong _prevSentinel;
|
|
private ulong _prevReturnSlot;
|
|
private bool _prevForcedExit;
|
|
private bool _prevYieldRequested;
|
|
private string? _prevYieldReason;
|
|
private GuestThreadState? _prevState;
|
|
private ulong _prevGuestThreadHandle;
|
|
private nint _prevHostRspSlot;
|
|
private int _prevHostThreadId;
|
|
private bool _entered;
|
|
|
|
private NativeGuestExecutor(DirectExecutionBackend backend)
|
|
{
|
|
_backend = backend;
|
|
}
|
|
|
|
public static NativeGuestExecutor? TryCreate(DirectExecutionBackend backend)
|
|
{
|
|
if (!EnsureHostRuntimeExports(backend._hostSymbols))
|
|
{
|
|
return null;
|
|
}
|
|
var executor = new NativeGuestExecutor(backend);
|
|
if (!executor.Initialize())
|
|
{
|
|
executor.Dispose();
|
|
return null;
|
|
}
|
|
return executor;
|
|
}
|
|
|
|
private static bool EnsureHostRuntimeExports(IHostSymbolResolver symbols)
|
|
{
|
|
if (_exitThreadAddress != 0)
|
|
{
|
|
return _waitForSingleObjectAddress != 0 && _setEventAddress != 0;
|
|
}
|
|
_waitForSingleObjectAddress = symbols.GetAddress(HostRuntimeFunction.WaitForSingleObject);
|
|
_setEventAddress = symbols.GetAddress(HostRuntimeFunction.SetEvent);
|
|
_exitThreadAddress = symbols.GetAddress(HostRuntimeFunction.ExitThread);
|
|
return _waitForSingleObjectAddress != 0 && _setEventAddress != 0 && _exitThreadAddress != 0;
|
|
}
|
|
|
|
private bool Initialize()
|
|
{
|
|
_selfHandle = GCHandle.Alloc(this);
|
|
_controlBlock = (void*)_backend._hostMemory.Allocate(0, 4096u, HostPageProtection.ReadWrite);
|
|
if (_controlBlock == null)
|
|
{
|
|
return false;
|
|
}
|
|
_loopStub = (void*)_backend._hostMemory.Allocate(0, LoopStubSize, HostPageProtection.ReadWriteExecute);
|
|
if (_loopStub == null)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var prologuePtr = (nint)(delegate* unmanaged<nint, nint>)&RunPrologue;
|
|
var epiloguePtr = (nint)(delegate* unmanaged<nint, int, void>)&RunEpilogue;
|
|
var executorHandle = GCHandle.ToIntPtr(_selfHandle);
|
|
var workHandle = _workAvailable.SafeWaitHandle.DangerousGetHandle();
|
|
var doneHandle = _workCompleted.SafeWaitHandle.DangerousGetHandle();
|
|
|
|
byte* code = (byte*)_loopStub;
|
|
int offset = 0;
|
|
void Emit(byte value) => code[offset++] = value;
|
|
void EmitMovRcxImm64(ulong value)
|
|
{
|
|
Emit(0x48); Emit(0xB9);
|
|
*(ulong*)(code + offset) = value;
|
|
offset += sizeof(ulong);
|
|
}
|
|
void EmitMovRaxImm64(ulong value)
|
|
{
|
|
Emit(0x48); Emit(0xB8);
|
|
*(ulong*)(code + offset) = value;
|
|
offset += sizeof(ulong);
|
|
}
|
|
void EmitCallRax()
|
|
{
|
|
Emit(0xFF); Emit(0xD0);
|
|
}
|
|
void EmitSetDoneEvent()
|
|
{
|
|
EmitMovRcxImm64((ulong)doneHandle);
|
|
EmitMovRaxImm64((ulong)_setEventAddress);
|
|
EmitCallRax();
|
|
}
|
|
|
|
// Thread entry leaves RSP ≡ 8 (mod 16); after this every call below happens
|
|
// at RSP ≡ 0 with shadow space available.
|
|
Emit(0x48); Emit(0x83); Emit(0xEC); Emit(0x28); // sub rsp, 0x28
|
|
int loopStart = offset;
|
|
EmitMovRcxImm64((ulong)workHandle);
|
|
Emit(0xBA); // mov edx, INFINITE
|
|
*(uint*)(code + offset) = 0xFFFFFFFFu;
|
|
offset += sizeof(uint);
|
|
EmitMovRaxImm64((ulong)_waitForSingleObjectAddress);
|
|
EmitCallRax();
|
|
EmitMovRaxImm64((ulong)_controlBlock);
|
|
Emit(0x83); Emit(0x38); Emit(0x00); // cmp dword [rax], 0
|
|
Emit(0x0F); Emit(0x85); // jne stop
|
|
int stopJump = offset;
|
|
offset += sizeof(int);
|
|
EmitMovRcxImm64((ulong)executorHandle);
|
|
EmitMovRaxImm64((ulong)prologuePtr);
|
|
EmitCallRax(); // rax = entry stub, or 0 on failure
|
|
Emit(0x48); Emit(0x85); Emit(0xC0); // test rax, rax
|
|
Emit(0x0F); Emit(0x84); // je skipEntry
|
|
int skipJump = offset;
|
|
offset += sizeof(int);
|
|
EmitCallRax(); // guest entry stub -> eax
|
|
int skipEntryOffset = offset;
|
|
Emit(0x89); Emit(0xC2); // mov edx, eax
|
|
EmitMovRcxImm64((ulong)executorHandle);
|
|
EmitMovRaxImm64((ulong)epiloguePtr);
|
|
EmitCallRax();
|
|
EmitSetDoneEvent();
|
|
Emit(0xE9); // jmp loop
|
|
*(int*)(code + offset) = loopStart - (offset + sizeof(int));
|
|
offset += sizeof(int);
|
|
int stopOffset = offset;
|
|
// Signal done so a Run() racing teardown cannot park forever; a stopped
|
|
// worker is never re-rented, so the stale signal is unobservable.
|
|
EmitSetDoneEvent();
|
|
Emit(0x31); Emit(0xC9); // xor ecx, ecx
|
|
EmitMovRaxImm64((ulong)_exitThreadAddress);
|
|
EmitCallRax();
|
|
Emit(0xCC); // int3 (ExitThread never returns)
|
|
*(int*)(code + stopJump) = stopOffset - (stopJump + sizeof(int));
|
|
*(int*)(code + skipJump) = skipEntryOffset - (skipJump + sizeof(int));
|
|
|
|
uint oldProtect = 0;
|
|
if (!_backend._hostMemory.Protect((ulong)_loopStub, LoopStubSize, HostPageProtection.ReadExecute, out oldProtect))
|
|
{
|
|
return false;
|
|
}
|
|
_backend._hostMemory.FlushInstructionCache((ulong)_loopStub, LoopStubSize);
|
|
_threadHandle = _backend._hostThreading.CreateNativeThread(
|
|
(nint)_loopStub,
|
|
0,
|
|
WorkerStackReservation,
|
|
out _nativeThreadId);
|
|
if (_threadHandle == 0)
|
|
{
|
|
return false;
|
|
}
|
|
if (LogThreadMode)
|
|
{
|
|
TraceThreadMode($"worker_created native_tid={_nativeThreadId} loop=0x{(ulong)_loopStub:X16}");
|
|
}
|
|
return true;
|
|
}
|
|
|
|
public int Run(
|
|
CpuContext context,
|
|
GuestThreadState? state,
|
|
ulong guestThreadHandle,
|
|
ulong sentinelRip,
|
|
ulong returnSlotAddress,
|
|
nint hostRspSlot,
|
|
nint entryStub,
|
|
ulong affinityMask,
|
|
out bool yieldRequested,
|
|
out string? yieldReason,
|
|
out bool forcedExit)
|
|
{
|
|
_runContext = context;
|
|
_runState = state;
|
|
_runGuestThreadHandle = guestThreadHandle;
|
|
_runSentinelRip = sentinelRip;
|
|
_runReturnSlotAddress = returnSlotAddress;
|
|
_runHostRspSlot = hostRspSlot;
|
|
_runEntryStub = entryStub;
|
|
_runAffinityMask = affinityMask;
|
|
_runPrologueFailed = true;
|
|
_runYieldRequested = false;
|
|
_runYieldReason = null;
|
|
_runForcedExit = false;
|
|
_workAvailable.Set();
|
|
_workCompleted.WaitOne();
|
|
_runContext = null;
|
|
_runState = null;
|
|
yieldRequested = _runYieldRequested;
|
|
yieldReason = _runYieldReason;
|
|
forcedExit = _runForcedExit;
|
|
if (_runPrologueFailed)
|
|
{
|
|
throw new InvalidOperationException("Native guest worker failed to bind the run ambient (prologue fault)");
|
|
}
|
|
return _runNativeResult;
|
|
}
|
|
|
|
[UnmanagedCallersOnly]
|
|
private static nint RunPrologue(nint executorHandle)
|
|
{
|
|
try
|
|
{
|
|
var executor = (NativeGuestExecutor)GCHandle.FromIntPtr(executorHandle).Target!;
|
|
return executor.EnterRun();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
try
|
|
{
|
|
Console.Error.WriteLine(
|
|
$"[LOADER][ERROR] Native guest worker prologue failed: {ex.GetType().Name}: {ex.Message}");
|
|
}
|
|
catch
|
|
{
|
|
}
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
[UnmanagedCallersOnly]
|
|
private static void RunEpilogue(nint executorHandle, int nativeResult)
|
|
{
|
|
try
|
|
{
|
|
var executor = (NativeGuestExecutor)GCHandle.FromIntPtr(executorHandle).Target!;
|
|
executor.ExitRun(nativeResult);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
try
|
|
{
|
|
Console.Error.WriteLine(
|
|
$"[LOADER][ERROR] Native guest worker epilogue failed: {ex.GetType().Name}: {ex.Message}");
|
|
}
|
|
catch
|
|
{
|
|
}
|
|
}
|
|
}
|
|
|
|
private nint EnterRun()
|
|
{
|
|
var backend = _backend;
|
|
_prevBackend = _activeExecutionBackend;
|
|
_prevContext = _activeCpuContext;
|
|
_prevSentinel = _activeEntryReturnSentinelRip;
|
|
_prevReturnSlot = _activeGuestReturnSlotAddress;
|
|
_prevForcedExit = _activeForcedGuestExit;
|
|
_prevYieldRequested = _activeGuestThreadYieldRequested;
|
|
_prevYieldReason = _activeGuestThreadYieldReason;
|
|
_prevState = _activeGuestThreadState;
|
|
_prevHostRspSlot = backend._hostThreading.GetTlsValue(backend._hostRspSlotTlsIndex);
|
|
_prevGuestThreadHandle = GuestThreadExecution.EnterGuestThread(_runGuestThreadHandle);
|
|
_entered = true;
|
|
_activeExecutionBackend = backend;
|
|
_activeCpuContext = _runContext;
|
|
_activeEntryReturnSentinelRip = _runSentinelRip;
|
|
_activeGuestReturnSlotAddress = _runReturnSlotAddress;
|
|
_activeForcedGuestExit = false;
|
|
_activeGuestThreadYieldRequested = false;
|
|
_activeGuestThreadYieldReason = null;
|
|
_activeGuestThreadState = _runState;
|
|
backend.BindTlsBase(_runContext!);
|
|
backend._hostThreading.SetTlsValue(backend._hostRspSlotTlsIndex, _runHostRspSlot);
|
|
if (_runState is { } state)
|
|
{
|
|
_prevHostThreadId = Volatile.Read(ref state.HostThreadId);
|
|
Volatile.Write(ref state.HostThreadId, unchecked((int)backend._hostThreading.CurrentThreadId));
|
|
}
|
|
if (_runAffinityMask != 0)
|
|
{
|
|
backend.ApplyGuestThreadAffinity(_runAffinityMask);
|
|
}
|
|
_runPrologueFailed = false;
|
|
if (LogThreadMode)
|
|
{
|
|
TraceThreadMode(
|
|
$"worker_enter guest=0x{_runGuestThreadHandle:X16} stub=0x{(ulong)_runEntryStub:X16}");
|
|
}
|
|
return _runEntryStub;
|
|
}
|
|
|
|
private void ExitRun(int nativeResult)
|
|
{
|
|
_runNativeResult = nativeResult;
|
|
_runYieldRequested = _activeGuestThreadYieldRequested;
|
|
_runYieldReason = _activeGuestThreadYieldReason;
|
|
_runForcedExit = _activeForcedGuestExit;
|
|
if (!_entered)
|
|
{
|
|
return;
|
|
}
|
|
_entered = false;
|
|
if (_runState is { } state)
|
|
{
|
|
Volatile.Write(ref state.HostThreadId, _prevHostThreadId);
|
|
}
|
|
_backend._hostThreading.SetTlsValue(_backend._hostRspSlotTlsIndex, _prevHostRspSlot);
|
|
GuestThreadExecution.RestoreGuestThread(_prevGuestThreadHandle);
|
|
_activeExecutionBackend = _prevBackend;
|
|
_activeCpuContext = _prevContext;
|
|
_activeEntryReturnSentinelRip = _prevSentinel;
|
|
_activeGuestReturnSlotAddress = _prevReturnSlot;
|
|
_activeForcedGuestExit = _prevForcedExit;
|
|
_activeGuestThreadYieldRequested = _prevYieldRequested;
|
|
_activeGuestThreadYieldReason = _prevYieldReason;
|
|
_activeGuestThreadState = _prevState;
|
|
_prevBackend = null;
|
|
_prevContext = null;
|
|
_prevState = null;
|
|
_prevYieldReason = null;
|
|
if (LogThreadMode)
|
|
{
|
|
TraceThreadMode(
|
|
$"worker_exit guest=0x{_runGuestThreadHandle:X16} result=0x{nativeResult:X8} yield={_runYieldRequested}");
|
|
}
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
if (_controlBlock != null)
|
|
{
|
|
*(int*)_controlBlock = 1;
|
|
}
|
|
try
|
|
{
|
|
_workAvailable.Set();
|
|
}
|
|
catch (ObjectDisposedException)
|
|
{
|
|
}
|
|
var exited = _threadHandle == 0;
|
|
if (_threadHandle != 0)
|
|
{
|
|
exited = _backend._hostThreading.WaitForThreadExit(_threadHandle, 1000u);
|
|
_backend._hostThreading.CloseThreadHandle(_threadHandle);
|
|
_threadHandle = 0;
|
|
}
|
|
if (!exited)
|
|
{
|
|
// The worker is still parked in guest code (teardown should have unwound
|
|
// it first). Leak the stub, control block, events and GC handle rather
|
|
// than have the thread execute freed memory.
|
|
Console.Error.WriteLine(
|
|
$"[LOADER][WARN] Native guest worker tid={_nativeThreadId} did not stop; leaking its run loop.");
|
|
return;
|
|
}
|
|
if (_loopStub != null)
|
|
{
|
|
_backend._hostMemory.Free((ulong)_loopStub);
|
|
_loopStub = null;
|
|
}
|
|
if (_controlBlock != null)
|
|
{
|
|
_backend._hostMemory.Free((ulong)_controlBlock);
|
|
_controlBlock = null;
|
|
}
|
|
if (_selfHandle.IsAllocated)
|
|
{
|
|
_selfHandle.Free();
|
|
}
|
|
_workAvailable.Dispose();
|
|
_workCompleted.Dispose();
|
|
}
|
|
}
|
|
}
|