// Copyright (C) 2026 SharpEmu Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later using System.Collections; using System.Reflection; using SharpEmu.Core.Cpu.Native; using SharpEmu.HLE; using Xunit; namespace SharpEmu.Libs.Tests.Cpu; public sealed class GuestExceptionExecutorHandoffTests { [NativeX64Theory] [InlineData(false)] [InlineData(true)] public void BlockedThreadTakesPendingExceptionWhenExecutorReleasesIt(bool signalArrivesBeforeYield) { var moduleManager = new ModuleManager(); moduleManager.Freeze(); using var backend = new DirectExecutionBackend(moduleManager); var backendType = typeof(DirectExecutionBackend); var threadType = backendType.GetNestedType( "GuestThreadState", BindingFlags.NonPublic); var runStateType = backendType.GetNestedType( "GuestThreadRunState", BindingFlags.NonPublic); var pendingType = backendType.GetNestedType( "PendingGuestException", BindingFlags.NonPublic); Assert.NotNull(threadType); Assert.NotNull(runStateType); Assert.NotNull(pendingType); const ulong threadHandle = 0x1234; const ulong handler = 0x5678; const int exceptionType = 0x1E; const ulong exceptionStack = 0x9000; var thread = Activator.CreateInstance(threadType); Assert.NotNull(thread); threadType.GetProperty("ThreadHandle")!.SetValue(thread, threadHandle); threadType.GetProperty("State")!.SetValue( thread, Enum.Parse(runStateType, "Blocked")); threadType.GetProperty("ExecutorActive")!.SetValue(thread, true); threadType.GetProperty("ExceptionDeliveryActive")!.SetValue(thread, false); var pending = Activator.CreateInstance( pendingType, handler, exceptionType, exceptionStack); Assert.NotNull(pending); var pendingDictionary = Assert.IsAssignableFrom( backendType.GetField( "_pendingGuestExceptions", BindingFlags.Instance | BindingFlags.NonPublic)!.GetValue(backend)); backendType.GetMethod( "QueuePendingGuestExceptionLocked", BindingFlags.Instance | BindingFlags.NonPublic)!.Invoke(backend, [threadHandle, pending]); if (signalArrivesBeforeYield) { // The import requested a wait that the executor has not saved. // Keep the signal callback separate from this wait. var context = new CpuContext(new FakeCpuMemory(0x10000, 0x1000), Generation.Gen5); var previousThread = GuestThreadExecution.EnterGuestThread(threadHandle); var previousFrame = GuestThreadExecution.EnterImportCallFrame(0x10000, 0x20000, 0x30000); try { Assert.True(GuestThreadExecution.RequestCurrentThreadBlock( context, "job_wait", "event:3", blockDeadlineTimestamp: 123)); backendType.GetMethod( "DeliverPendingGuestExceptionAtSafePoint", BindingFlags.Instance | BindingFlags.NonPublic)!.Invoke( backend, [context, default(GuestCpuContinuation)]); Assert.True(pendingDictionary.Contains(threadHandle)); Assert.True(GuestThreadExecution.TryConsumeCurrentThreadBlock( out var reason, out var continuation, out var hasContinuation, out var wakeKey, out _, out var deadline)); Assert.Equal("job_wait", reason); Assert.True(hasContinuation); Assert.Equal(0x10000UL, continuation.Rip); Assert.Equal(0x20000UL, continuation.Rsp); Assert.Equal("event:3", wakeKey); Assert.Equal(123, deadline); } finally { GuestThreadExecution.RestoreImportCallFrame(previousFrame); GuestThreadExecution.RestoreGuestThread(previousThread); } } var handoff = backendType.GetMethod( "TryReleaseGuestThreadExecutorLocked", BindingFlags.Instance | BindingFlags.NonPublic); Assert.NotNull(handoff); object?[] arguments = [thread, null]; Assert.True(Assert.IsType(handoff.Invoke(backend, arguments))); Assert.False(Assert.IsType( threadType.GetProperty("ExecutorActive")!.GetValue(thread))); Assert.Empty(pendingDictionary); Assert.NotNull(arguments[1]); Assert.Equal(handler, pendingType.GetProperty("Handler")!.GetValue(arguments[1])); Assert.Equal(exceptionType, pendingType.GetProperty("ExceptionType")!.GetValue(arguments[1])); Assert.Equal(exceptionStack, pendingType.GetProperty("ExceptionStackBase")!.GetValue(arguments[1])); } }