From 2764aaab3fb01f06c0ee3e14a935202df88af47d Mon Sep 17 00:00:00 2001 From: Kurt Himebauch <136133082+xXJSONDeruloXx@users.noreply.github.com> Date: Thu, 23 Jul 2026 08:35:59 -0400 Subject: [PATCH] feat: implement cosf, time, ctype tables, tracked heap access, IL2CPP lookup ABI paths (#542) * fix: add Messenger CRT and AGC compatibility shims * fix: keep Messenger IL2CPP bootstrap on HLE shims * Fix Messenger compatibility ABI handling * Make IL2CPP ABI regression tests portable --- .../Native/DirectExecutionBackend.Imports.cs | 40 +++-- .../Cpu/Native/Il2CppApiLookupAbi.cs | 18 ++ src/SharpEmu.Libs/Agc/AgcExports.cs | 32 ++++ src/SharpEmu.Libs/CxxAbiExports.cs | 166 ++++++++++++++++++ .../Kernel/KernelMemoryCompatExports.cs | 55 +++++- src/SharpEmu.Libs/LibcStdioExports.cs | 78 ++++++++ src/SharpEmu.Libs/MessengerCompatExports.cs | 82 +++++++++ .../MessengerCompatExportsTests.cs | 119 +++++++++++++ 8 files changed, 570 insertions(+), 20 deletions(-) create mode 100644 src/SharpEmu.Core/Cpu/Native/Il2CppApiLookupAbi.cs create mode 100644 src/SharpEmu.Libs/MessengerCompatExports.cs create mode 100644 tests/SharpEmu.Libs.Tests/MessengerCompatExportsTests.cs diff --git a/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.Imports.cs b/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.Imports.cs index 92e53ad5..e6797ee6 100644 --- a/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.Imports.cs +++ b/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.Imports.cs @@ -2168,25 +2168,18 @@ public sealed partial class DirectExecutionBackend } var symbolNameAddress = cpuContext[CpuRegister.Rdi]; - var outputAddress = cpuContext[CpuRegister.Rsi]; if (!TryReadAsciiZ(symbolNameAddress, 512, out var symbolName) || - outputAddress == 0 || - !TryResolveIl2CppApiAddress(symbolName, out var resolvedAddress) || - !TryWriteUInt64Compat(outputAddress, resolvedAddress)) + !TryResolveIl2CppApiAddress(symbolName, out var resolvedAddress)) { Console.Error.WriteLine( - $"[LOADER][WARN] il2cpp_api_lookup_symbol failed: name='{symbolName}' out=0x{outputAddress:X16}"); - if (outputAddress != 0) - { - _ = TryWriteUInt64Compat(outputAddress, 0); - } - - cpuContext[CpuRegister.Rax] = ulong.MaxValue; - return OrbisGen2Result.ORBIS_GEN2_OK; + $"[LOADER][WARN] il2cpp_api_lookup_symbol failed: name='{symbolName}'"); + // il2cpp_api_lookup_symbol is a normal one-argument pointer-returning + // function. In particular, RSI is not an output pointer; the title's + // caller leaves it live from an earlier call. Do not write through it. + return Il2CppApiLookupAbi.SetResult(cpuContext, resolved: false, address: 0); } - cpuContext[CpuRegister.Rax] = 0; - return OrbisGen2Result.ORBIS_GEN2_OK; + return Il2CppApiLookupAbi.SetResult(cpuContext, resolved: true, resolvedAddress); } private bool TryResolveIl2CppApiAddress(string symbolName, out ulong address) @@ -2196,8 +2189,23 @@ public sealed partial class DirectExecutionBackend return true; } - return Aerolib.Instance.TryGetByExportName(symbolName, out var symbol) && - TryResolveRuntimeSymbolAddress(symbol.Nid, out address); + if (Aerolib.Instance.TryGetByExportName(symbolName, out var symbol) && + TryResolveRuntimeSymbolAddress(symbol.Nid, out address)) + { + return true; + } + + // Unity's IL2CPP API table is populated through this resolver, then the + // returned pointers are called directly by the title. Use the callable + // zero-return stub for missing APIs rather than a non-callable sentinel. + if (symbolName.StartsWith("il2cpp_", StringComparison.Ordinal) && + _unresolvedReturnStub != 0) + { + address = (ulong)_unresolvedReturnStub; + return true; + } + + return false; } private OrbisGen2Result DispatchBootstrapBridge() diff --git a/src/SharpEmu.Core/Cpu/Native/Il2CppApiLookupAbi.cs b/src/SharpEmu.Core/Cpu/Native/Il2CppApiLookupAbi.cs new file mode 100644 index 00000000..683946e3 --- /dev/null +++ b/src/SharpEmu.Core/Cpu/Native/Il2CppApiLookupAbi.cs @@ -0,0 +1,18 @@ +// Copyright (C) 2026 SharpEmu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +using SharpEmu.Core.Cpu; +using SharpEmu.HLE; + +namespace SharpEmu.Core.Cpu.Native; + +internal static class Il2CppApiLookupAbi +{ + internal static OrbisGen2Result SetResult(CpuContext context, bool resolved, ulong address) + { + // il2cpp_api_lookup_symbol is a normal pointer-returning function: + // the result is in RAX and RSI remains caller-owned state. + context[CpuRegister.Rax] = resolved ? address : 0; + return OrbisGen2Result.ORBIS_GEN2_OK; + } +} diff --git a/src/SharpEmu.Libs/Agc/AgcExports.cs b/src/SharpEmu.Libs/Agc/AgcExports.cs index 62257b6c..b6e6b141 100644 --- a/src/SharpEmu.Libs/Agc/AgcExports.cs +++ b/src/SharpEmu.Libs/Agc/AgcExports.cs @@ -2965,6 +2965,38 @@ public static partial class AgcExports $"arg1=0x{ctx[CpuRegister.Rsi]:X16} arg2=0x{ctx[CpuRegister.Rdx]:X16}"); return ReturnPointer(ctx, commandAddress); } + + // Synthetic labels for uncatalogued AGC helper NIDs; the NID is authoritative. + #pragma warning disable SHEM006 + [SysAbiExport( + Nid = "zlqfTyrQSPk", + ExportName = "sceAgcUnknownZlqfTyrQSPk", + Target = Generation.Gen5, + LibraryName = "libSceAgc")] + public static int UnknownZlqfTyrQSPk(CpuContext ctx) + { + TraceAgc( + $"agc.unknown_zlqf rdi=0x{ctx[CpuRegister.Rdi]:X16} " + + $"rsi=0x{ctx[CpuRegister.Rsi]:X16} rdx=0x{ctx[CpuRegister.Rdx]:X16} " + + $"rcx=0x{ctx[CpuRegister.Rcx]:X16} r8=0x{ctx[CpuRegister.R8]:X16} r9=0x{ctx[CpuRegister.R9]:X16}"); + ctx[CpuRegister.Rax] = 0; + return (int)OrbisGen2Result.ORBIS_GEN2_OK; + } + + [SysAbiExport( + Nid = "dZGYu5wObJs", + ExportName = "sceAgcUnknownDZGYu5wObJs", + Target = Generation.Gen5, + LibraryName = "libSceAgc")] + public static int UnknownDZGYu5wObJs(CpuContext ctx) + { + TraceAgc( + $"agc.unknown_dzgy rdi=0x{ctx[CpuRegister.Rdi]:X16} " + + $"rsi=0x{ctx[CpuRegister.Rsi]:X16} rdx=0x{ctx[CpuRegister.Rdx]:X16} " + + $"rcx=0x{ctx[CpuRegister.Rcx]:X16} r8=0x{ctx[CpuRegister.R8]:X16} r9=0x{ctx[CpuRegister.R9]:X16}"); + ctx[CpuRegister.Rax] = 0; + return (int)OrbisGen2Result.ORBIS_GEN2_OK; + } #pragma warning restore SHEM006 private static void EnqueueSubmittedDcb( diff --git a/src/SharpEmu.Libs/CxxAbiExports.cs b/src/SharpEmu.Libs/CxxAbiExports.cs index 1d174dc0..805c8c96 100644 --- a/src/SharpEmu.Libs/CxxAbiExports.cs +++ b/src/SharpEmu.Libs/CxxAbiExports.cs @@ -21,6 +21,11 @@ public static class CxaGuardExports } private static readonly ConcurrentDictionary _inProgress = new(); + private static readonly ConcurrentDictionary _onceGates = new(); + + private const int OnceUninitialized = 0; + private const int OnceInProgress = 1; + private const int OnceComplete = 2; [SysAbiExport( Nid = "3GPpjQdAMTw", @@ -171,6 +176,167 @@ public static class CxaGuardExports return (int)OrbisGen2Result.ORBIS_GEN2_OK; } + [SysAbiExport( + Nid = "DiGVep5yB5w", + ExportName = "_ZSt13_Execute_onceRSt9once_flagPFiPvS1_PS1_ES1_", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libc")] + public static int ExecuteOnce(CpuContext ctx) + { + var onceAddress = ctx[CpuRegister.Rdi]; + var callbackAddress = ctx[CpuRegister.Rsi]; + var parameter = ctx[CpuRegister.Rdx]; + if (onceAddress == 0 || callbackAddress == 0) + { + ctx[CpuRegister.Rax] = 0; + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT; + } + + if (!ctx.TryReadInt32(onceAddress, out var onceValue)) + { + ctx[CpuRegister.Rax] = 0; + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; + } + + if (onceValue == OnceComplete) + { + ctx[CpuRegister.Rax] = 0; + return (int)OrbisGen2Result.ORBIS_GEN2_OK; + } + + var gate = _onceGates.GetOrAdd(onceAddress, static _ => new object()); + lock (gate) + { + if (!ctx.TryReadInt32(onceAddress, out onceValue)) + { + ctx[CpuRegister.Rax] = 0; + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; + } + + while (onceValue == OnceInProgress) + { + Monitor.Wait(gate, TimeSpan.FromMilliseconds(1)); + if (!ctx.TryReadInt32(onceAddress, out onceValue)) + { + ctx[CpuRegister.Rax] = 0; + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; + } + } + + if (onceValue == OnceComplete) + { + ctx[CpuRegister.Rax] = 0; + return (int)OrbisGen2Result.ORBIS_GEN2_OK; + } + + if (!ctx.TryWriteInt32(onceAddress, OnceInProgress)) + { + ctx[CpuRegister.Rax] = 0; + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; + } + } + + var scheduler = GuestThreadExecution.Scheduler; + var callbackSucceeded = false; + string? callbackError = null; + var allocator = ctx.Memory as IGuestMemoryAllocator; + var hasScratchContext = false; + ulong scratchContextAddress = 0; + try + { + if (allocator is not null && allocator.TryAllocateGuestMemory(0x10, 0x10, out scratchContextAddress)) + { + hasScratchContext = true; + _ = ctx.TryWriteUInt64(scratchContextAddress, 0); + _ = ctx.TryWriteUInt64(scratchContextAddress + 8, 0); + } + + if (scheduler is null) + { + callbackError = "guest scheduler unavailable"; + } + else if (scheduler.TryCallGuestFunction( + ctx, + callbackAddress, + onceAddress, + parameter, + scratchContextAddress, + 0, + 0, + "std::_Execute_once", + out var returnValue, + out callbackError)) + { + callbackSucceeded = returnValue != 0; + } + } + finally + { + if (hasScratchContext && allocator is not null) + { + _ = allocator.TryFreeGuestMemory(scratchContextAddress); + } + } + + lock (gate) + { + if (!callbackSucceeded) + { + _ = ctx.TryWriteInt32(onceAddress, OnceUninitialized); + Monitor.PulseAll(gate); + if (!string.IsNullOrWhiteSpace(callbackError)) + { + Console.Error.WriteLine( + $"[LOADER][WARN] std::_Execute_once callback failed: {callbackError}"); + } + + ctx[CpuRegister.Rax] = unchecked((ulong)(int)OrbisGen2Result.ORBIS_GEN2_ERROR_TRY_AGAIN); + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_TRY_AGAIN; + } + + if (!ctx.TryWriteInt32(onceAddress, OnceComplete)) + { + _ = ctx.TryWriteInt32(onceAddress, OnceUninitialized); + Monitor.PulseAll(gate); + ctx[CpuRegister.Rax] = unchecked((ulong)(int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; + } + + Monitor.PulseAll(gate); + // A completed once flag no longer needs a host-side gate. Waiters + // already holding this gate will observe OnceComplete after the + // pulse; future callers take the fast path before looking it up. + _ = _onceGates.TryRemove(onceAddress, out _); + } + + ctx[CpuRegister.Rax] = 0; + return (int)OrbisGen2Result.ORBIS_GEN2_OK; + } + + [SysAbiExport( + Nid = "PsrRUg671K0", + ExportName = "__cxa_increment_exception_refcount", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libc")] + public static int CxaIncrementExceptionRefcount(CpuContext ctx) + { + _ = ctx[CpuRegister.Rdi]; + ctx[CpuRegister.Rax] = 0; + return (int)OrbisGen2Result.ORBIS_GEN2_OK; + } + + [SysAbiExport( + Nid = "MQFPAqQPt1s", + ExportName = "__cxa_decrement_exception_refcount", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libc")] + public static int CxaDecrementExceptionRefcount(CpuContext ctx) + { + _ = ctx[CpuRegister.Rdi]; + ctx[CpuRegister.Rax] = 0; + return (int)OrbisGen2Result.ORBIS_GEN2_OK; + } + private static bool TryReadGuardState(CpuContext ctx, ulong guardPtr, out ulong word, out bool initialized, out bool inProgress) { word = 0; diff --git a/src/SharpEmu.Libs/Kernel/KernelMemoryCompatExports.cs b/src/SharpEmu.Libs/Kernel/KernelMemoryCompatExports.cs index c9dd8c4a..e8b116aa 100644 --- a/src/SharpEmu.Libs/Kernel/KernelMemoryCompatExports.cs +++ b/src/SharpEmu.Libs/Kernel/KernelMemoryCompatExports.cs @@ -5855,7 +5855,7 @@ public static partial class KernelMemoryCompatExports return true; } - if (!TryReadHostMemory(address, destination)) + if (!TryReadTrackedLibcHeap(address, destination) && !TryReadHostMemory(address, destination)) { return false; } @@ -5921,7 +5921,7 @@ public static partial class KernelMemoryCompatExports return true; } - if (!TryWriteHostMemory(address, source)) + if (!TryWriteTrackedLibcHeap(address, source) && !TryWriteHostMemory(address, source)) { return false; } @@ -6663,7 +6663,7 @@ public static partial class KernelMemoryCompatExports } } - internal static bool TryReadTrackedLibcHeap( + internal static unsafe bool TryReadTrackedLibcHeap( ulong address, Span destination) { @@ -6687,7 +6687,54 @@ public static partial class KernelMemoryCompatExports continue; } - return TryReadHostMemory(address, destination); + try + { + // Marshal.AllocHGlobal allocations are not present in the + // emulator's POSIX HostMemory map, so TryReadHostMemory + // would reject this already-bounds-checked range. + new ReadOnlySpan((void*)address, destination.Length).CopyTo(destination); + return true; + } + catch + { + return false; + } + } + } + + return false; + } + + private static unsafe bool TryWriteTrackedLibcHeap(ulong address, ReadOnlySpan source) + { + if (source.IsEmpty) + { + return true; + } + + var length = (ulong)source.Length; + lock (_libcAllocGate) + { + foreach (var (allocationAddress, allocation) in _libcAllocations) + { + var allocationSize = (ulong)allocation.Size; + var offset = address >= allocationAddress + ? address - allocationAddress + : ulong.MaxValue; + if (offset > allocationSize || length > allocationSize - offset) + { + continue; + } + + try + { + source.CopyTo(new Span((void*)address, source.Length)); + return true; + } + catch + { + return false; + } } } diff --git a/src/SharpEmu.Libs/LibcStdioExports.cs b/src/SharpEmu.Libs/LibcStdioExports.cs index b9a17af4..15d4421f 100644 --- a/src/SharpEmu.Libs/LibcStdioExports.cs +++ b/src/SharpEmu.Libs/LibcStdioExports.cs @@ -45,7 +45,11 @@ public static class LibcStdioExports private const ushort CtypeBlank = 0x400; // _XB ' ' and '\t' private static readonly object _ctypeTableGate = new(); + private static readonly object _ctypeLowerTableGate = new(); + private static readonly object _ctypeUpperTableGate = new(); private static nint _ctypeTableBase; + private static nint _ctypeLowerTableBase; + private static nint _ctypeUpperTableBase; [SysAbiExport( Nid = "xeYO4u7uyJ0", @@ -716,6 +720,28 @@ public static class LibcStdioExports return (int)OrbisGen2Result.ORBIS_GEN2_OK; } + [SysAbiExport( + Nid = "1uJgoVq3bQU", + ExportName = "_Getptolower", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libc")] + public static int GetPtolower(CpuContext ctx) + { + ctx[CpuRegister.Rax] = unchecked((ulong)EnsureCtypeLowerTable()); + return (int)OrbisGen2Result.ORBIS_GEN2_OK; + } + + [SysAbiExport( + Nid = "rcQCUr0EaRU", + ExportName = "_Getptoupper", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libc")] + public static int GetPtoupper(CpuContext ctx) + { + ctx[CpuRegister.Rax] = unchecked((ulong)EnsureCtypeUpperTable()); + return (int)OrbisGen2Result.ORBIS_GEN2_OK; + } + private static unsafe nint EnsureCtypeTable() { lock (_ctypeTableGate) @@ -740,6 +766,58 @@ public static class LibcStdioExports } } + private static unsafe nint EnsureCtypeLowerTable() + { + lock (_ctypeLowerTableGate) + { + if (_ctypeLowerTableBase != 0) + { + return _ctypeLowerTableBase; + } + + var storage = Marshal.AllocHGlobal(CtypeTableEntryCount * sizeof(ushort)); + var entries = new Span((void*)storage, CtypeTableEntryCount); + for (var i = 0; i < CtypeTableEntryCount; i++) + { + var c = i + CtypeTableLowerBound; + entries[i] = c == -1 + ? ushort.MaxValue + : c is >= 0 and <= 0x7F + ? (ushort)char.ToLowerInvariant((char)c) + : (ushort)(c & 0xFF); + } + + _ctypeLowerTableBase = storage - (CtypeTableLowerBound * sizeof(ushort)); + return _ctypeLowerTableBase; + } + } + + private static unsafe nint EnsureCtypeUpperTable() + { + lock (_ctypeUpperTableGate) + { + if (_ctypeUpperTableBase != 0) + { + return _ctypeUpperTableBase; + } + + var storage = Marshal.AllocHGlobal(CtypeTableEntryCount * sizeof(ushort)); + var entries = new Span((void*)storage, CtypeTableEntryCount); + for (var i = 0; i < CtypeTableEntryCount; i++) + { + var c = i + CtypeTableLowerBound; + entries[i] = c == -1 + ? ushort.MaxValue + : c is >= 0 and <= 0x7F + ? (ushort)char.ToUpperInvariant((char)c) + : (ushort)(c & 0xFF); + } + + _ctypeUpperTableBase = storage - (CtypeTableLowerBound * sizeof(ushort)); + return _ctypeUpperTableBase; + } + } + private static ushort ComputeCtypeFlags(int c) { var isUpper = c is >= 'A' and <= 'Z'; diff --git a/src/SharpEmu.Libs/MessengerCompatExports.cs b/src/SharpEmu.Libs/MessengerCompatExports.cs new file mode 100644 index 00000000..cbdeaa04 --- /dev/null +++ b/src/SharpEmu.Libs/MessengerCompatExports.cs @@ -0,0 +1,82 @@ +// Copyright (C) 2026 SharpEmu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +using System; +using SharpEmu.HLE; + +namespace SharpEmu.Libs.Messenger; + +/// Small Unity/libc compatibility shims exercised by The Messenger. +public static class MessengerCompatExports +{ + [SysAbiExport(Nid = "wLlFkwG9UcQ", ExportName = "time", Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libc")] + public static int Time(CpuContext ctx) + { + var seconds = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); + var output = ctx[CpuRegister.Rdi]; + if (output != 0 && !ctx.TryWriteUInt64(output, unchecked((ulong)seconds))) + { + ctx[CpuRegister.Rax] = 0; + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; + } + + ctx[CpuRegister.Rax] = unchecked((ulong)seconds); + return (int)OrbisGen2Result.ORBIS_GEN2_OK; + } + + [SysAbiExport(Nid = "M4YYbSFfJ8g", ExportName = "setenv", Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libc")] + public static int Setenv(CpuContext ctx) + { + ctx[CpuRegister.Rax] = 0; + return (int)OrbisGen2Result.ORBIS_GEN2_OK; + } + + [SysAbiExport(Nid = "-P6FNMzk2Kc", ExportName = "cosf", Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libc")] + public static int Cosf(CpuContext ctx) + { + // AMD64 passes a scalar float in XMM0 and returns it in XMM0. RDI is + // unrelated caller state and must not be interpreted as the argument. + ctx.GetXmmRegister(0, out var low, out var high); + var value = BitConverter.Int32BitsToSingle(unchecked((int)low)); + var resultBits = unchecked((uint)BitConverter.SingleToInt32Bits(MathF.Cos(value))); + ctx.SetXmmRegister(0, (low & 0xFFFFFFFF00000000UL) | resultBits, high); + return (int)OrbisGen2Result.ORBIS_GEN2_OK; + } + + [SysAbiExport(Nid = "YQ0navp+YIc", ExportName = "puts", Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libc")] + public static int Puts(CpuContext ctx) + { + ctx[CpuRegister.Rax] = 0; + return (int)OrbisGen2Result.ORBIS_GEN2_OK; + } + + [SysAbiExport(Nid = "KuOuD58hqn4", ExportName = "malloc_stats_fast", Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libc")] + public static int MallocStatsFast(CpuContext ctx) + { + ctx[CpuRegister.Rax] = 0; + return (int)OrbisGen2Result.ORBIS_GEN2_OK; + } + + [SysAbiExport(Nid = "-pnj3-7a6QA", ExportName = "unity_mono_set_user_malloc_mutex", Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libunity")] + public static int UnityMonoSetMallocMutex(CpuContext ctx) + { + ctx[CpuRegister.Rax] = 0; + return (int)OrbisGen2Result.ORBIS_GEN2_OK; + } + + [SysAbiExport(Nid = "35NoyMOtYpE", ExportName = "SetDataFolder", Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libunity")] + public static int SetDataFolder(CpuContext ctx) + { + ctx[CpuRegister.Rax] = 0; + return (int)OrbisGen2Result.ORBIS_GEN2_OK; + } + +#pragma warning disable SHEM006 + [SysAbiExport(Nid = "cJ2Y4E-t258", ExportName = "il2cpp_api_register_symbol", Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libil2cpp")] + public static int Il2CppRegisterSymbol(CpuContext ctx) + { + ctx[CpuRegister.Rax] = 0; + return (int)OrbisGen2Result.ORBIS_GEN2_OK; + } +} +#pragma warning restore SHEM006 diff --git a/tests/SharpEmu.Libs.Tests/MessengerCompatExportsTests.cs b/tests/SharpEmu.Libs.Tests/MessengerCompatExportsTests.cs new file mode 100644 index 00000000..0ca9f498 --- /dev/null +++ b/tests/SharpEmu.Libs.Tests/MessengerCompatExportsTests.cs @@ -0,0 +1,119 @@ +// Copyright (C) 2026 SharpEmu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +using System.Runtime.InteropServices; +using SharpEmu.Core.Cpu.Native; +using SharpEmu.HLE; +using SharpEmu.Libs.Kernel; +using SharpEmu.Libs.LibcStdio; +using SharpEmu.Libs.Messenger; +using Xunit; + +namespace SharpEmu.Libs.Tests; + +public sealed class MessengerCompatExportsTests +{ + [Fact] + public void Cosf_UsesScalarXmmArgumentAndReturn() + { + const ulong memoryBase = 0x1_0000_0000; + var context = new CpuContext(new FakeCpuMemory(memoryBase, 0x1000), Generation.Gen5); + var input = 0.5f; + var inputBits = unchecked((uint)BitConverter.SingleToInt32Bits(input)); + context[CpuRegister.Rdi] = 0xDEAD_BEEF; // Must not be used as the argument. + context.SetXmmRegister(0, 0xAABB_CCDD_0000_0000UL | inputBits, 0x1122_3344_5566_7788UL); + + Assert.Equal((int)OrbisGen2Result.ORBIS_GEN2_OK, MessengerCompatExports.Cosf(context)); + + context.GetXmmRegister(0, out var low, out var high); + var expectedBits = unchecked((uint)BitConverter.SingleToInt32Bits(MathF.Cos(input))); + Assert.Equal(expectedBits, unchecked((uint)low)); + Assert.Equal(0xAABB_CCDDUL, low >> 32); + Assert.Equal(0x1122_3344_5566_7788UL, high); + } + + [Fact] + public void CtypeCaseTables_MapAsciiCharacters() + { + var context = new CpuContext(new FakeCpuMemory(0x1_0000_0000, 0x1000), Generation.Gen5); + + Assert.Equal((int)OrbisGen2Result.ORBIS_GEN2_OK, LibcStdioExports.GetPtolower(context)); + var lower = unchecked((nint)(long)context[CpuRegister.Rax]); + Assert.Equal((short)'a', Marshal.ReadInt16(lower + ('A' * sizeof(short)))); + Assert.Equal((short)'z', Marshal.ReadInt16(lower + ('z' * sizeof(short)))); + Assert.Equal((short)-1, Marshal.ReadInt16(lower - sizeof(short))); + + Assert.Equal((int)OrbisGen2Result.ORBIS_GEN2_OK, LibcStdioExports.GetPtoupper(context)); + var upper = unchecked((nint)(long)context[CpuRegister.Rax]); + Assert.Equal((short)'A', Marshal.ReadInt16(upper + ('a' * sizeof(short)))); + Assert.Equal((short)'Z', Marshal.ReadInt16(upper + ('Z' * sizeof(short)))); + Assert.Equal((short)-1, Marshal.ReadInt16(upper - sizeof(short))); + } + + [Fact] + public void Il2CppLookup_ReturnsPointerInRaxWithoutWritingRsi() + { + const ulong memoryBase = 0x1_0000_0000; + const ulong nameAddress = memoryBase + 0x100; + const ulong outputAddress = memoryBase + 0x200; + const ulong resolvedAddress = 0x2_0000_0000; + var memory = new FakeCpuMemory(memoryBase, 0x1000); + memory.WriteCString(nameAddress, "il2cpp_test"); + var context = new CpuContext(memory, Generation.Gen5); + context[CpuRegister.Rdi] = nameAddress; + context[CpuRegister.Rsi] = outputAddress; // Live caller state, not an output pointer. + Assert.True(context.TryWriteUInt64(outputAddress, 0xCAFE_BABE)); + + var result = Il2CppApiLookupAbi.SetResult(context, resolved: true, resolvedAddress); + + Assert.Equal(OrbisGen2Result.ORBIS_GEN2_OK, result); + Assert.Equal(resolvedAddress, context[CpuRegister.Rax]); + Assert.True(context.TryReadUInt64(outputAddress, out var output)); + Assert.Equal(0xCAFE_BABEUL, output); + } + + [Fact] + public void Il2CppLookup_MissingApiReturnsNullWithoutWritingRsi() + { + const ulong memoryBase = 0x1_0000_0000; + const ulong nameAddress = memoryBase + 0x100; + const ulong outputAddress = memoryBase + 0x200; + var memory = new FakeCpuMemory(memoryBase, 0x1000); + memory.WriteCString(nameAddress, "il2cpp_missing"); + var context = new CpuContext(memory, Generation.Gen5); + context[CpuRegister.Rdi] = nameAddress; + context[CpuRegister.Rsi] = outputAddress; + Assert.True(context.TryWriteUInt64(outputAddress, 0xCAFE_BABE)); + + var result = Il2CppApiLookupAbi.SetResult(context, resolved: false, address: 0); + + Assert.Equal(OrbisGen2Result.ORBIS_GEN2_OK, result); + Assert.Equal(0UL, context[CpuRegister.Rax]); + Assert.True(context.TryReadUInt64(outputAddress, out var output)); + Assert.Equal(0xCAFE_BABEUL, output); + } + + [Fact] + public void TrackedLibcHeapFallback_ReadsAndWritesHostAllocation() + { + var context = new CpuContext(new FakeCpuMemory(0x1_0000_0000, 0x1000), Generation.Gen5); + context[CpuRegister.Rdi] = 16; + Assert.Equal((int)OrbisGen2Result.ORBIS_GEN2_OK, KernelMemoryCompatExports.Malloc(context)); + var address = context[CpuRegister.Rax]; + Assert.NotEqual(0UL, address); + + try + { + Assert.True(KernelMemoryCompatExports.TryWriteUInt64Compat(context, address, 0x1234_5678_9ABC_DEF0UL)); + Span bytes = stackalloc byte[8]; + Assert.True(KernelMemoryCompatExports.TryReadTrackedLibcHeap(address, bytes)); + Assert.True(KernelMemoryCompatExports.TryReadUInt64Compat(context, address, out var value)); + Assert.Equal(0x1234_5678_9ABC_DEF0UL, value); + } + finally + { + context[CpuRegister.Rdi] = address; + _ = KernelMemoryCompatExports.Free(context); + } + } +}