mirror of
https://github.com/par274/sharpemu.git
synced 2026-08-30 13:24:19 +08:00
b226dca3df
Co-authored-by: Acelogic <miguelc4600@gmail.com>
187 lines
8.2 KiB
C#
187 lines
8.2 KiB
C#
// Copyright (C) 2026 SharpEmu Emulator Project
|
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
|
|
using Xunit;
|
|
|
|
namespace SharpEmu.SourceGenerators.Tests;
|
|
|
|
public sealed class SysAbiExportGeneratorTests
|
|
{
|
|
private const string HandlerSource = """
|
|
using SharpEmu.HLE;
|
|
|
|
namespace TestExports;
|
|
|
|
public static class SampleExports
|
|
{
|
|
[SysAbiExport(Nid = "Zxa0VhQVTsk", ExportName = "sceKernelWaitSema", Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libKernel")]
|
|
public static int WaitSema(CpuContext ctx) => 0;
|
|
|
|
// NID omitted on purpose: the generator must derive it from the name.
|
|
[SysAbiExport(ExportName = "sceKernelSignalSema", Target = Generation.Gen5)]
|
|
public static int SignalSema(CpuContext ctx) => 0;
|
|
|
|
// Parameterless handler shape: must be wrapped to the SysAbiFunction contract.
|
|
[SysAbiExport(Nid = "ekNvsT22rsY", ExportName = "sceAudioOutOpen")]
|
|
public static int Open() => 0;
|
|
|
|
// Typed handler shape: the generator emits the SysV register thunk.
|
|
[SysAbiExport(Nid = "12wOHk8ywb0", ExportName = "sceKernelPollSema")]
|
|
public static int PollSema(CpuContext ctx, uint handle, int needCount) => 0;
|
|
|
|
// All four integer kinds across all six argument registers.
|
|
[SysAbiExport(Nid = "4DM06U2BNEY", ExportName = "sceKernelCancelSema")]
|
|
public static int CancelSema(CpuContext ctx, uint a, int b, ulong c, long d, uint e, int f) => 0;
|
|
|
|
// Guest string marshalling: the thunk reads the pointer before the handler.
|
|
[SysAbiExport(Nid = "1G3lF1Gg1k8", ExportName = "sceKernelOpen")]
|
|
public static int KernelOpen(CpuContext ctx, [GuestCString(4096)] string path, int flags) => 0;
|
|
|
|
// A single fail-closed handler may back a catalog of LLE-preferred exports.
|
|
[SysAbiExport(Nid = "5fbPUzoA2fM", ExportName = "sceLleFirst", Target = Generation.Gen5, LibraryName = "libSceLle", PreferLle = true)]
|
|
[SysAbiExport(Nid = "L9NfM+f4f1Y", ExportName = "sceLleSecond", Target = Generation.Gen5, LibraryName = "libSceLle", PreferLle = true)]
|
|
public static int LleFallback(CpuContext ctx) => -1;
|
|
}
|
|
""";
|
|
|
|
[Fact]
|
|
public void GeneratedRegistryCompilesAgainstRealHleTypes()
|
|
{
|
|
var compilation = RoslynTestHost.Compile(HandlerSource);
|
|
var (updated, generated) = RoslynTestHost.RunGenerator(compilation);
|
|
|
|
Assert.NotEqual(string.Empty, generated);
|
|
RoslynTestHost.AssertCompiles(updated);
|
|
}
|
|
|
|
[Fact]
|
|
public void RegistryContainsDeclaredDerivedAndWrappedExports()
|
|
{
|
|
var (_, generated) = RoslynTestHost.RunGenerator(RoslynTestHost.Compile(HandlerSource));
|
|
|
|
// Declared NID passes through verbatim.
|
|
Assert.Contains("\"Zxa0VhQVTsk\"", generated, StringComparison.Ordinal);
|
|
Assert.Contains("global::TestExports.SampleExports.WaitSema", generated, StringComparison.Ordinal);
|
|
|
|
// Omitted NID is derived from the export name at compile time.
|
|
Assert.Contains("\"4czppHBiriw\"", generated, StringComparison.Ordinal);
|
|
|
|
// Parameterless handlers are adapted to the SysAbiFunction shape.
|
|
Assert.Contains("static _ => global::TestExports.SampleExports.Open()", generated, StringComparison.Ordinal);
|
|
}
|
|
|
|
[Fact]
|
|
public void TypedHandlersGetSysVRegisterThunks()
|
|
{
|
|
var (_, generated) = RoslynTestHost.RunGenerator(RoslynTestHost.Compile(HandlerSource));
|
|
|
|
// Parameters map positionally to RDI/RSI/... with the same unchecked-cast idiom
|
|
// hand-written handlers use; ulong reads the register raw.
|
|
Assert.Contains(
|
|
"static ctx => global::TestExports.SampleExports.PollSema(ctx, " +
|
|
"unchecked((uint)ctx[global::SharpEmu.HLE.CpuRegister.Rdi]), " +
|
|
"unchecked((int)ctx[global::SharpEmu.HLE.CpuRegister.Rsi]))",
|
|
generated,
|
|
StringComparison.Ordinal);
|
|
Assert.Contains(
|
|
"static ctx => global::TestExports.SampleExports.CancelSema(ctx, " +
|
|
"unchecked((uint)ctx[global::SharpEmu.HLE.CpuRegister.Rdi]), " +
|
|
"unchecked((int)ctx[global::SharpEmu.HLE.CpuRegister.Rsi]), " +
|
|
"ctx[global::SharpEmu.HLE.CpuRegister.Rdx], " +
|
|
"unchecked((long)ctx[global::SharpEmu.HLE.CpuRegister.Rcx]), " +
|
|
"unchecked((uint)ctx[global::SharpEmu.HLE.CpuRegister.R8]), " +
|
|
"unchecked((int)ctx[global::SharpEmu.HLE.CpuRegister.R9]))",
|
|
generated,
|
|
StringComparison.Ordinal);
|
|
}
|
|
|
|
[Fact]
|
|
public void GuestCStringParametersAreMarshalledWithAFaultPath()
|
|
{
|
|
var (_, generated) = RoslynTestHost.RunGenerator(RoslynTestHost.Compile(HandlerSource));
|
|
|
|
// The string is read from the pointer in RDI before the handler runs, and a
|
|
// failed read returns MEMORY_FAULT to the guest without invoking the handler.
|
|
Assert.Contains(
|
|
"if (!ctx.TryReadNullTerminatedUtf8(ctx[global::SharpEmu.HLE.CpuRegister.Rdi], 4096, out var guestString0))",
|
|
generated,
|
|
StringComparison.Ordinal);
|
|
Assert.Contains(
|
|
"return ctx.SetReturn(global::SharpEmu.HLE.OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);",
|
|
generated,
|
|
StringComparison.Ordinal);
|
|
Assert.Contains(
|
|
"return global::TestExports.SampleExports.KernelOpen(ctx, guestString0, " +
|
|
"unchecked((int)ctx[global::SharpEmu.HLE.CpuRegister.Rsi]));",
|
|
generated,
|
|
StringComparison.Ordinal);
|
|
}
|
|
|
|
[Fact]
|
|
public void GenerationFilteringMatchesTheReflectionScanSemantics()
|
|
{
|
|
var (_, generated) = RoslynTestHost.RunGenerator(RoslynTestHost.Compile(HandlerSource));
|
|
|
|
// The Add helper reproduces ResolveExportInfo: None inherits the registration
|
|
// generation, and exports outside the registration generation are skipped.
|
|
Assert.Contains("attributeTarget == global::SharpEmu.HLE.Generation.None ? registrationGeneration : attributeTarget", generated, StringComparison.Ordinal);
|
|
Assert.Contains("(target & registrationGeneration) == 0", generated, StringComparison.Ordinal);
|
|
}
|
|
|
|
[Fact]
|
|
public void MultipleLlePreferredAttributesShareOneFailClosedHandler()
|
|
{
|
|
var (_, generated) = RoslynTestHost.RunGenerator(RoslynTestHost.Compile(HandlerSource));
|
|
|
|
Assert.Contains("\"5fbPUzoA2fM\"", generated, StringComparison.Ordinal);
|
|
Assert.Contains("\"L9NfM+f4f1Y\"", generated, StringComparison.Ordinal);
|
|
Assert.Equal(
|
|
2,
|
|
generated.Split("global::TestExports.SampleExports.LleFallback", StringSplitOptions.None).Length - 1);
|
|
Assert.Contains(
|
|
", true, global::TestExports.SampleExports.LleFallback",
|
|
generated,
|
|
StringComparison.Ordinal);
|
|
}
|
|
|
|
[Fact]
|
|
public void AssemblyWithoutExportsEmitsNoRegistry()
|
|
{
|
|
// Referencing the analyzer must not mint a colliding
|
|
// SharpEmu.Generated.SysAbiExportRegistry type in export-free assemblies.
|
|
const string noExports = """
|
|
public static class PlainCode
|
|
{
|
|
public static int Nothing() => 0;
|
|
}
|
|
""";
|
|
var (_, generated) = RoslynTestHost.RunGenerator(RoslynTestHost.Compile(noExports));
|
|
|
|
Assert.Equal(string.Empty, generated);
|
|
}
|
|
|
|
[Fact]
|
|
public void InvalidHandlersAreSkippedNotEmitted()
|
|
{
|
|
const string invalid = """
|
|
using SharpEmu.HLE;
|
|
|
|
namespace TestExports;
|
|
|
|
public static class BrokenExports
|
|
{
|
|
[SysAbiExport(ExportName = "sceKernelUsleep")]
|
|
public static long WrongReturn(CpuContext ctx) => 0;
|
|
|
|
[SysAbiExport(ExportName = "sceKernelGettimeofday")]
|
|
private static int Inaccessible(CpuContext ctx) => 0;
|
|
}
|
|
""";
|
|
var (updated, generated) = RoslynTestHost.RunGenerator(RoslynTestHost.Compile(invalid));
|
|
|
|
Assert.DoesNotContain("WrongReturn", generated, StringComparison.Ordinal);
|
|
Assert.DoesNotContain("Inaccessible", generated, StringComparison.Ordinal);
|
|
RoslynTestHost.AssertCompiles(updated);
|
|
}
|
|
}
|