// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Runtime.InteropServices;
namespace SharpEmu.HLE.Host.Posix;
///
/// POSIX replacements for the kernel32 helpers the native backend embeds in
/// emitted x86-64 code. Every stub exposed here follows the Win64 calling
/// convention the emitted call sites were written for (first argument in
/// ECX, result in RAX, Win64 non-volatile registers preserved), so the
/// emission code stays identical across platforms.
///
internal static unsafe class PosixHostStubs
{
private static readonly object Gate = new();
private static bool _initialized;
private static nint _tlsGetValueStub;
private static nint _queryPerformanceCounterStub;
private static nint _switchToThreadStub;
private static nint _sleepStub;
private static nint _waitForSingleObjectStub;
private static nint _setEventStub;
private static nint _exitThreadStub;
public static nint TlsGetValueStubAddress
{
get { EnsureInitialized(); return _tlsGetValueStub; }
}
public static nint QueryPerformanceCounterStubAddress
{
get { EnsureInitialized(); return _queryPerformanceCounterStub; }
}
public static nint SwitchToThreadStubAddress
{
get { EnsureInitialized(); return _switchToThreadStub; }
}
public static nint SleepStubAddress
{
get { EnsureInitialized(); return _sleepStub; }
}
///
/// Win64-convention replacements for the kernel32 event/thread helpers the
/// native guest worker loop embeds. The "handle" they take is a worker
/// event created by : a dispatch semaphore
/// on macOS, an unnamed POSIX semaphore on Linux. The wait stub always
/// waits forever (the worker loop passes INFINITE) and retries EINTR.
///
public static nint WaitForSingleObjectStubAddress
{
get { EnsureInitialized(); return _waitForSingleObjectStub; }
}
public static nint SetEventStubAddress
{
get { EnsureInitialized(); return _setEventStub; }
}
public static nint ExitThreadStubAddress
{
get { EnsureInitialized(); return _exitThreadStub; }
}
///
/// Creates a binary-semaphore worker event signalable/waitable both from
/// managed code and from emitted native code (via the stub addresses
/// above). Returns 0 on failure.
///
public static nint CreateWorkerEvent()
{
if (OperatingSystem.IsMacOS())
{
return dispatch_semaphore_create(0);
}
var semaphore = Marshal.AllocHGlobal(64);
if (sem_init(semaphore, 0, 0) != 0)
{
Marshal.FreeHGlobal(semaphore);
return 0;
}
return semaphore;
}
public static bool SignalWorkerEvent(nint handle)
{
if (OperatingSystem.IsMacOS())
{
_ = dispatch_semaphore_signal(handle);
return true;
}
return sem_post(handle) == 0;
}
/// Waits for a worker event; a negative timeout waits forever.
public static bool WaitWorkerEvent(nint handle, int timeoutMilliseconds)
{
if (OperatingSystem.IsMacOS())
{
if (timeoutMilliseconds < 0)
{
return dispatch_semaphore_wait(handle, ulong.MaxValue) == 0;
}
var deadline = dispatch_time(0, timeoutMilliseconds * 1_000_000L);
return dispatch_semaphore_wait(handle, deadline) == 0;
}
if (timeoutMilliseconds < 0)
{
while (sem_wait(handle) != 0)
{
// EINTR: retry.
}
return true;
}
var deadlineTicks = Environment.TickCount64 + timeoutMilliseconds;
while (sem_trywait(handle) != 0)
{
if (Environment.TickCount64 >= deadlineTicks)
{
return false;
}
System.Threading.Thread.Sleep(1);
}
return true;
}
public static void DestroyWorkerEvent(nint handle)
{
if (handle == 0)
{
return;
}
if (OperatingSystem.IsMacOS())
{
dispatch_release(handle);
return;
}
_ = sem_destroy(handle);
Marshal.FreeHGlobal(handle);
}
///
/// Starts a raw pthread at a native entry point (pthread entries take their
/// argument in RDI; the worker loop stub ignores it). Returns an opaque
/// handle for /,
/// or 0 on failure.
///
public static nint CreateWorkerThread(nint entry, nint parameter, nuint stackReserveBytes, out uint threadId)
{
threadId = 0;
byte* attr = stackalloc byte[512];
if (pthread_attr_init(attr) != 0)
{
return 0;
}
try
{
if (stackReserveBytes != 0)
{
_ = pthread_attr_setstacksize(attr, nuint.Max(stackReserveBytes, 512 * 1024));
}
nint thread;
if (pthread_create(&thread, attr, entry, parameter) != 0)
{
return 0;
}
if (OperatingSystem.IsMacOS())
{
ulong numericId;
if (pthread_threadid_np(thread, &numericId) == 0)
{
threadId = unchecked((uint)numericId);
}
}
else
{
threadId = unchecked((uint)thread);
}
var holder = (nint*)Marshal.AllocHGlobal(sizeof(nint) * 2);
holder[0] = thread;
holder[1] = 0; // joined flag
return (nint)holder;
}
finally
{
_ = pthread_attr_destroy(attr);
}
}
///
/// Waits for a worker thread to exit. Liveness is probed with
/// pthread_kill(thread, 0) (ESRCH once the thread has terminated) because
/// neither platform offers a portable timed join; the exited thread is then
/// joined so its resources are reclaimed.
///
public static bool WaitForWorkerThreadExit(nint threadHandle, uint timeoutMilliseconds)
{
var holder = (nint*)threadHandle;
if (holder == null)
{
return false;
}
if (holder[1] != 0)
{
return true;
}
var thread = holder[0];
var deadline = Environment.TickCount64 + timeoutMilliseconds;
while (pthread_kill(thread, 0) == 0)
{
if (Environment.TickCount64 >= deadline)
{
return false;
}
System.Threading.Thread.Sleep(1);
}
_ = pthread_join(thread, null);
holder[1] = 1;
return true;
}
public static void CloseWorkerThreadHandle(nint threadHandle)
{
var holder = (nint*)threadHandle;
if (holder == null)
{
return;
}
if (holder[1] == 0)
{
// Never observed exiting: detach so the thread does not leak a
// zombie join target when it eventually terminates.
_ = pthread_detach(holder[0]);
}
Marshal.FreeHGlobal(threadHandle);
}
/// Allocates a pthread TLS key, mirroring kernel32!TlsAlloc.
public static uint TlsAlloc()
{
if (OperatingSystem.IsMacOS())
{
nuint key;
return pthread_key_create_mac(&key, 0) == 0 ? (uint)key : uint.MaxValue;
}
uint key32;
return pthread_key_create_linux(&key32, 0) == 0 ? key32 : uint.MaxValue;
}
public static bool TlsFree(uint key)
{
return OperatingSystem.IsMacOS()
? pthread_key_delete_mac((nuint)key) == 0
: pthread_key_delete_linux(key) == 0;
}
public static bool TlsSetValue(uint key, nint value)
{
return OperatingSystem.IsMacOS()
? pthread_setspecific_mac((nuint)key, value) == 0
: pthread_setspecific_linux(key, value) == 0;
}
public static nint TlsGetValue(uint key)
{
return OperatingSystem.IsMacOS()
? pthread_getspecific_mac((nuint)key)
: pthread_getspecific_linux(key);
}
/// Stable numeric id of the calling thread (kernel32!GetCurrentThreadId).
public static uint GetCurrentThreadId()
{
if (OperatingSystem.IsMacOS())
{
ulong tid;
return pthread_threadid_np(0, &tid) == 0 ? unchecked((uint)tid) : 0u;
}
return unchecked((uint)gettid());
}
///
/// Wraps a managed callback (compiled for the SysV ABI on POSIX .NET) in a
/// thunk that accepts up to four integer arguments in the Win64 ABI the
/// emitted x86-64 call sites use. Win64 passes args in rcx/rdx/r8/r9 and
/// treats rdi/rsi as non-volatile; SysV expects rdi/rsi/rdx/rcx and
/// clobbers them, so the thunk saves rdi/rsi, shuffles the registers, keeps
/// the stack 16-byte aligned for the call, and forwards the rax result.
///
public static nint CreateWin64ToSysVThunk(nint sysvTarget)
{
var memory = HostPlatform.Current.Memory;
var page = (byte*)memory.Allocate(
0,
4096,
HostPageProtection.ReadWriteExecute);
if (page == null)
{
throw new OutOfMemoryException("Failed to allocate Win64->SysV thunk page");
}
var offset = 0;
Emit(page, ref offset, 0x57); // push rdi
Emit(page, ref offset, 0x56); // push rsi
Emit(page, ref offset, 0x48, 0x89, 0xCF); // mov rdi, rcx
Emit(page, ref offset, 0x48, 0x89, 0xD6); // mov rsi, rdx
Emit(page, ref offset, 0x4C, 0x89, 0xC2); // mov rdx, r8
Emit(page, ref offset, 0x4C, 0x89, 0xC9); // mov rcx, r9
Emit(page, ref offset, 0x48, 0x83, 0xEC, 0x08); // sub rsp, 8 (realign to 16)
EmitMovRaxImm64(page, ref offset, sysvTarget); // mov rax, target
Emit(page, ref offset, 0xFF, 0xD0); // call rax
Emit(page, ref offset, 0x48, 0x83, 0xC4, 0x08); // add rsp, 8
Emit(page, ref offset, 0x5E); // pop rsi
Emit(page, ref offset, 0x5F); // pop rdi
Emit(page, ref offset, 0xC3); // ret
if (!memory.Protect((ulong)page, 4096, HostPageProtection.ReadExecute, out _))
{
throw new InvalidOperationException("Failed to protect Win64->SysV thunk page");
}
memory.FlushInstructionCache((ulong)page, (ulong)offset);
return (nint)page;
}
private static void EnsureInitialized()
{
if (_initialized)
{
return;
}
lock (Gate)
{
if (_initialized)
{
return;
}
BuildStubs();
_initialized = true;
}
}
private static void BuildStubs()
{
var memory = HostPlatform.Current.Memory;
var page = (byte*)memory.Allocate(
0,
4096,
HostPageProtection.ReadWriteExecute);
if (page == null)
{
throw new OutOfMemoryException("Failed to allocate POSIX host helper stub page");
}
var offset = 0;
_tlsGetValueStub = EmitTlsGetValue(page, ref offset);
_queryPerformanceCounterStub = EmitQueryPerformanceCounter(page, ref offset);
_switchToThreadStub = EmitSwitchToThread(page, ref offset);
_sleepStub = EmitSleep(page, ref offset);
_waitForSingleObjectStub = EmitWaitForSingleObject(page, ref offset);
_setEventStub = EmitSetEvent(page, ref offset);
_exitThreadStub = EmitExitThread(page, ref offset);
if (!memory.Protect((ulong)page, 4096, HostPageProtection.ReadExecute, out _))
{
throw new InvalidOperationException("Failed to protect POSIX host helper stub page");
}
memory.FlushInstructionCache((ulong)page, (ulong)offset);
}
private static nint EmitTlsGetValue(byte* page, ref int offset)
{
var start = (nint)(page + offset);
if (OperatingSystem.IsMacOS())
{
// On macOS x86-64 pthread keys index the gs-based thread specific
// data array directly, so TlsGetValue(index in ecx) collapses to a
// single load that clobbers nothing but RAX.
Emit(page, ref offset, 0x89, 0xC8); // mov eax, ecx
Emit(page, ref offset, 0x65, 0x48, 0x8B, 0x04, 0xC5, 0, 0, 0, 0); // mov rax, gs:[rax*8]
Emit(page, ref offset, 0xC3); // ret
return start;
}
// Linux: call pthread_getspecific, preserving the registers that are
// volatile in SysV but non-volatile in Win64 (rsi, rdi).
var pthreadGetSpecific = ResolveLibcExport("pthread_getspecific");
Emit(page, ref offset, 0x56); // push rsi
Emit(page, ref offset, 0x57); // push rdi
Emit(page, ref offset, 0x48, 0x83, 0xEC, 0x08); // sub rsp, 8
Emit(page, ref offset, 0x89, 0xCF); // mov edi, ecx
EmitMovRaxImm64(page, ref offset, pthreadGetSpecific); // mov rax, imm64
Emit(page, ref offset, 0xFF, 0xD0); // call rax
Emit(page, ref offset, 0x48, 0x83, 0xC4, 0x08); // add rsp, 8
Emit(page, ref offset, 0x5F); // pop rdi
Emit(page, ref offset, 0x5E); // pop rsi
Emit(page, ref offset, 0xC3); // ret
return start;
}
private static nint EmitQueryPerformanceCounter(byte* page, ref int offset)
{
// BOOL QueryPerformanceCounter(LARGE_INTEGER* out in rcx): the emitted
// consumers only need a monotonically increasing counter, which rdtsc
// provides without leaving Win64-safe registers.
var start = (nint)(page + offset);
Emit(page, ref offset, 0x0F, 0x31); // rdtsc
Emit(page, ref offset, 0x48, 0xC1, 0xE2, 0x20); // shl rdx, 32
Emit(page, ref offset, 0x48, 0x09, 0xD0); // or rax, rdx
Emit(page, ref offset, 0x48, 0x89, 0x01); // mov [rcx], rax
Emit(page, ref offset, 0xB8, 0x01, 0x00, 0x00, 0x00); // mov eax, 1
Emit(page, ref offset, 0xC3); // ret
return start;
}
private static nint EmitSwitchToThread(byte* page, ref int offset)
{
var schedYield = ResolveLibcExport("sched_yield");
var start = (nint)(page + offset);
Emit(page, ref offset, 0x56); // push rsi
Emit(page, ref offset, 0x57); // push rdi
Emit(page, ref offset, 0x48, 0x83, 0xEC, 0x08); // sub rsp, 8
EmitMovRaxImm64(page, ref offset, schedYield); // mov rax, imm64
Emit(page, ref offset, 0xFF, 0xD0); // call rax
Emit(page, ref offset, 0x48, 0x83, 0xC4, 0x08); // add rsp, 8
Emit(page, ref offset, 0x5F); // pop rdi
Emit(page, ref offset, 0x5E); // pop rsi
Emit(page, ref offset, 0xB8, 0x01, 0x00, 0x00, 0x00); // mov eax, 1
Emit(page, ref offset, 0xC3); // ret
return start;
}
private static nint EmitSleep(byte* page, ref int offset)
{
// void Sleep(DWORD milliseconds in ecx) -> usleep(microseconds in edi).
var usleep = ResolveLibcExport("usleep");
var start = (nint)(page + offset);
Emit(page, ref offset, 0x56); // push rsi
Emit(page, ref offset, 0x57); // push rdi
Emit(page, ref offset, 0x48, 0x83, 0xEC, 0x08); // sub rsp, 8
Emit(page, ref offset, 0x89, 0xCF); // mov edi, ecx
Emit(page, ref offset, 0x81, 0xFF, 0xFF, 0x0F, 0x00, 0x00); // cmp edi, 0xFFF
Emit(page, ref offset, 0x76, 0x05); // jbe +5
Emit(page, ref offset, 0xBF, 0xFF, 0x0F, 0x00, 0x00); // mov edi, 0xFFF (cap at ~4s)
Emit(page, ref offset, 0x69, 0xFF, 0xE8, 0x03, 0x00, 0x00); // imul edi, edi, 1000
EmitMovRaxImm64(page, ref offset, usleep); // mov rax, imm64
Emit(page, ref offset, 0xFF, 0xD0); // call rax
Emit(page, ref offset, 0x48, 0x83, 0xC4, 0x08); // add rsp, 8
Emit(page, ref offset, 0x5F); // pop rdi
Emit(page, ref offset, 0x5E); // pop rsi
Emit(page, ref offset, 0xC3); // ret
return start;
}
private static nint EmitWaitForSingleObject(byte* page, ref int offset)
{
// DWORD WaitForSingleObject(worker event in rcx, timeout in edx): the
// worker loop only ever waits forever, so the timeout is ignored.
// macOS waits on a dispatch semaphore (needs DISPATCH_TIME_FOREVER in
// rsi), Linux on a sem_t; both retry until the wait succeeds (EINTR).
var wait = ResolveLibcExport(
OperatingSystem.IsMacOS() ? "dispatch_semaphore_wait" : "sem_wait");
var start = (nint)(page + offset);
Emit(page, ref offset, 0x56); // push rsi
Emit(page, ref offset, 0x57); // push rdi
Emit(page, ref offset, 0x53); // push rbx
Emit(page, ref offset, 0x48, 0x89, 0xCB); // mov rbx, rcx
var retry = offset;
Emit(page, ref offset, 0x48, 0x89, 0xDF); // mov rdi, rbx
if (OperatingSystem.IsMacOS())
{
Emit(page, ref offset, 0x48, 0xC7, 0xC6, 0xFF, 0xFF, 0xFF, 0xFF); // mov rsi, DISPATCH_TIME_FOREVER
}
EmitMovRaxImm64(page, ref offset, wait); // mov rax, imm64
Emit(page, ref offset, 0xFF, 0xD0); // call rax
Emit(page, ref offset, 0x85, 0xC0); // test eax, eax
Emit(page, ref offset, 0x75, unchecked((byte)(retry - (offset + 2)))); // jnz retry
Emit(page, ref offset, 0x31, 0xC0); // xor eax, eax (WAIT_OBJECT_0)
Emit(page, ref offset, 0x5B); // pop rbx
Emit(page, ref offset, 0x5F); // pop rdi
Emit(page, ref offset, 0x5E); // pop rsi
Emit(page, ref offset, 0xC3); // ret
return start;
}
private static nint EmitSetEvent(byte* page, ref int offset)
{
// BOOL SetEvent(worker event in rcx) -> dispatch_semaphore_signal /
// sem_post.
var signal = ResolveLibcExport(
OperatingSystem.IsMacOS() ? "dispatch_semaphore_signal" : "sem_post");
var start = (nint)(page + offset);
Emit(page, ref offset, 0x56); // push rsi
Emit(page, ref offset, 0x57); // push rdi
Emit(page, ref offset, 0x48, 0x83, 0xEC, 0x08); // sub rsp, 8
Emit(page, ref offset, 0x48, 0x89, 0xCF); // mov rdi, rcx
EmitMovRaxImm64(page, ref offset, signal); // mov rax, imm64
Emit(page, ref offset, 0xFF, 0xD0); // call rax
Emit(page, ref offset, 0x48, 0x83, 0xC4, 0x08); // add rsp, 8
Emit(page, ref offset, 0x5F); // pop rdi
Emit(page, ref offset, 0x5E); // pop rsi
Emit(page, ref offset, 0xB8, 0x01, 0x00, 0x00, 0x00); // mov eax, 1
Emit(page, ref offset, 0xC3); // ret
return start;
}
private static nint EmitExitThread(byte* page, ref int offset)
{
// void ExitThread(code in ecx) -> pthread_exit(NULL); never returns,
// so no registers need preserving. pthread_exit runs the thread's TSD
// destructors, which detaches the CLR if the thread lazily attached.
var pthreadExit = ResolveLibcExport("pthread_exit");
var start = (nint)(page + offset);
Emit(page, ref offset, 0x48, 0x83, 0xEC, 0x08); // sub rsp, 8
Emit(page, ref offset, 0x31, 0xFF); // xor edi, edi
EmitMovRaxImm64(page, ref offset, pthreadExit); // mov rax, imm64
Emit(page, ref offset, 0xFF, 0xD0); // call rax
Emit(page, ref offset, 0xCC); // int3 (never returns)
return start;
}
private static nint ResolveLibcExport(string name)
{
var libc = NativeLibrary.Load(OperatingSystem.IsMacOS() ? "libSystem.dylib" : "libc.so.6");
return NativeLibrary.GetExport(libc, name);
}
private static void Emit(byte* page, ref int offset, params byte[] bytes)
{
foreach (var value in bytes)
{
page[offset++] = value;
}
}
private static void EmitMovRaxImm64(byte* page, ref int offset, nint value)
{
Emit(page, ref offset, 0x48, 0xB8);
*(long*)(page + offset) = value;
offset += sizeof(long);
}
[DllImport("libc", EntryPoint = "pthread_key_create", SetLastError = true)]
private static extern int pthread_key_create_mac(nuint* key, nint destructor);
[DllImport("libc", EntryPoint = "pthread_key_create", SetLastError = true)]
private static extern int pthread_key_create_linux(uint* key, nint destructor);
[DllImport("libc", EntryPoint = "pthread_key_delete")]
private static extern int pthread_key_delete_mac(nuint key);
[DllImport("libc", EntryPoint = "pthread_key_delete")]
private static extern int pthread_key_delete_linux(uint key);
[DllImport("libc", EntryPoint = "pthread_setspecific")]
private static extern int pthread_setspecific_mac(nuint key, nint value);
[DllImport("libc", EntryPoint = "pthread_setspecific")]
private static extern int pthread_setspecific_linux(uint key, nint value);
[DllImport("libc", EntryPoint = "pthread_getspecific")]
private static extern nint pthread_getspecific_mac(nuint key);
[DllImport("libc", EntryPoint = "pthread_getspecific")]
private static extern nint pthread_getspecific_linux(uint key);
[DllImport("libc")]
private static extern int pthread_threadid_np(nint thread, ulong* threadId);
[DllImport("libc")]
private static extern int gettid();
[DllImport("libc")]
private static extern int pthread_attr_init(byte* attr);
[DllImport("libc")]
private static extern int pthread_attr_destroy(byte* attr);
[DllImport("libc")]
private static extern int pthread_attr_setstacksize(byte* attr, nuint stackSize);
[DllImport("libc")]
private static extern int pthread_create(nint* thread, byte* attr, nint startRoutine, nint arg);
[DllImport("libc")]
private static extern int pthread_join(nint thread, nint* returnValue);
[DllImport("libc")]
private static extern int pthread_detach(nint thread);
[DllImport("libc")]
private static extern int pthread_kill(nint thread, int signal);
// macOS: dispatch semaphores back the worker events (unnamed sem_init is
// unsupported on Darwin). libSystem reexports libdispatch, so "libc"
// resolves these like the pthread imports above.
[DllImport("libc")]
private static extern nint dispatch_semaphore_create(long value);
[DllImport("libc")]
private static extern nint dispatch_semaphore_signal(nint semaphore);
[DllImport("libc")]
private static extern nint dispatch_semaphore_wait(nint semaphore, ulong timeout);
[DllImport("libc")]
private static extern ulong dispatch_time(ulong when, long deltaNanoseconds);
[DllImport("libc")]
private static extern void dispatch_release(nint handle);
// Linux: unnamed POSIX semaphores.
[DllImport("libc")]
private static extern int sem_init(nint semaphore, int shared, uint value);
[DllImport("libc")]
private static extern int sem_post(nint semaphore);
[DllImport("libc")]
private static extern int sem_wait(nint semaphore);
[DllImport("libc")]
private static extern int sem_trywait(nint semaphore);
[DllImport("libc")]
private static extern int sem_destroy(nint semaphore);
}