feat(hle): prefer loaded guest exports for selected registrations (#844)

Co-authored-by: Acelogic <miguelc4600@gmail.com>
This commit is contained in:
frangametv
2026-08-25 22:32:57 +02:00
committed by GitHub
parent 51e5480049
commit b226dca3df
9 changed files with 262 additions and 72 deletions
@@ -1639,18 +1639,15 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
if (_moduleManager.TryGetExport(nid, out ExportedFunction export))
{
if (IsKernelLibrary(export.LibraryName))
var preferLleForLibc = IsLibcLibrary(export.LibraryName) && PreferLleForLibcExport(export.Name);
if (!ShouldResolveRegisteredExportViaLle(export, preferLleForLibc))
{
if (_logAllImports)
if (_logAllImports && IsKernelLibrary(export.LibraryName))
{
Console.Error.WriteLine($"[LOADER][DEBUG] TryResolveDirectImportTarget: {nid} ({export.LibraryName}:{export.Name}) -> HLE (kernel library)");
}
return false;
}
if (!IsLibcLibrary(export.LibraryName) || !PreferLleForLibcExport(export.Name))
{
return false;
}
if (TryResolveRuntimeSymbolAddress(nid, out var value2) && IsDirectImportTargetUsable(value2))
{
targetAddress = value2;
@@ -1704,6 +1701,14 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
return false;
}
internal static bool ShouldResolveRegisteredExportViaLle(
ExportedFunction export,
bool preferLleForLibc)
{
ArgumentNullException.ThrowIfNull(export);
return !IsKernelLibrary(export.LibraryName) && (export.PreferLle || preferLleForLibc);
}
private static bool IsHlePreferredNid(string nid)
{
return string.Equals(nid, "QrZZdJ8XsX0", StringComparison.Ordinal) ||
+14 -1
View File
@@ -5,7 +5,13 @@ namespace SharpEmu.HLE;
public sealed class ExportedFunction
{
public ExportedFunction(string libraryName, string nid, string name, Generation target, SysAbiFunction function)
public ExportedFunction(
string libraryName,
string nid,
string name,
Generation target,
SysAbiFunction function,
bool preferLle = false)
{
ArgumentException.ThrowIfNullOrWhiteSpace(libraryName);
ArgumentException.ThrowIfNullOrWhiteSpace(nid);
@@ -17,6 +23,7 @@ public sealed class ExportedFunction
Name = name;
Target = target;
Function = function;
PreferLle = preferLle;
}
public string LibraryName { get; }
@@ -28,4 +35,10 @@ public sealed class ExportedFunction
public Generation Target { get; }
public SysAbiFunction Function { get; }
/// <summary>
/// A loaded guest export is authoritative for this registration. The HLE function
/// remains available as an explicit fallback when no usable guest target exists.
/// </summary>
public bool PreferLle { get; }
}
+8 -1
View File
@@ -3,7 +3,7 @@
namespace SharpEmu.HLE;
[AttributeUsage(AttributeTargets.Method, Inherited = false, AllowMultiple = false)]
[AttributeUsage(AttributeTargets.Method, Inherited = false, AllowMultiple = true)]
public sealed class SysAbiExportAttribute : Attribute
{
public string LibraryName { get; set; } = "libKernel";
@@ -13,4 +13,11 @@ public sealed class SysAbiExportAttribute : Attribute
public string ExportName { get; set; } = string.Empty;
public Generation Target { get; set; } = Generation.None;
/// <summary>
/// Prefer a matching export from a loaded guest module and use this handler only
/// as the explicit fallback when that LLE provider is unavailable. Individual
/// handlers define whether that fallback is fail-closed or compatibility behavior.
/// </summary>
public bool PreferLle { get; set; }
}
@@ -102,17 +102,16 @@ public sealed class SysAbiExportAnalyzer : DiagnosticAnalyzer
ConcurrentDictionary<string, IMethodSymbol> exportsByNid)
{
var method = (IMethodSymbol)context.Symbol;
AttributeData? exportAttribute = null;
var exportAttributes = ImmutableArray.CreateBuilder<AttributeData>();
foreach (var attribute in method.GetAttributes())
{
if (SysAbiExportShape.IsSysAbiExportAttribute(attribute.AttributeClass))
{
exportAttribute = attribute;
break;
exportAttributes.Add(attribute);
}
}
if (exportAttribute is null)
if (exportAttributes.Count == 0)
{
return;
}
@@ -135,6 +134,28 @@ public sealed class SysAbiExportAnalyzer : DiagnosticAnalyzer
SysAbiDiagnostics.HandlerNotAccessible, location, methodDisplay));
}
foreach (var exportAttribute in exportAttributes)
{
AnalyzeExportAttribute(
context,
catalogNames,
exportsByNid,
method,
exportAttribute,
location,
methodDisplay);
}
}
private static void AnalyzeExportAttribute(
SymbolAnalysisContext context,
HashSet<string>? catalogNames,
ConcurrentDictionary<string, IMethodSymbol> exportsByNid,
IMethodSymbol method,
AttributeData exportAttribute,
Location location,
string methodDisplay)
{
var arguments = SysAbiExportShape.ReadArguments(exportAttribute);
var hasNid = !string.IsNullOrWhiteSpace(arguments.Nid);
var hasName = !string.IsNullOrWhiteSpace(arguments.ExportName);
@@ -188,9 +209,9 @@ public sealed class SysAbiExportAnalyzer : DiagnosticAnalyzer
}
}
var existing = exportsByNid.GetOrAdd(effectiveNid, method);
if (!SymbolEqualityComparer.Default.Equals(existing, method))
if (!exportsByNid.TryAdd(effectiveNid, method))
{
var existing = exportsByNid[effectiveNid];
context.ReportDiagnostic(Diagnostic.Create(
SysAbiDiagnostics.DuplicateNid,
location,
@@ -27,7 +27,16 @@ public sealed class SysAbiExportGenerator : IIncrementalGenerator
private sealed class ExportModel : IEquatable<ExportModel>
{
public ExportModel(string containingType, string methodName, SysAbiExportShape.HandlerShape shape, string typedParameterKinds, string libraryName, string nid, string exportName, int target)
public ExportModel(
string containingType,
string methodName,
SysAbiExportShape.HandlerShape shape,
string typedParameterKinds,
string libraryName,
string nid,
string exportName,
int target,
bool preferLle)
{
ContainingType = containingType;
MethodName = methodName;
@@ -37,6 +46,7 @@ public sealed class SysAbiExportGenerator : IIncrementalGenerator
Nid = nid;
ExportName = exportName;
Target = target;
PreferLle = preferLle;
}
public string ContainingType { get; }
@@ -51,6 +61,7 @@ public sealed class SysAbiExportGenerator : IIncrementalGenerator
public string Nid { get; }
public string ExportName { get; }
public int Target { get; }
public bool PreferLle { get; }
public bool Equals(ExportModel? other) =>
other is not null &&
@@ -61,7 +72,8 @@ public sealed class SysAbiExportGenerator : IIncrementalGenerator
LibraryName == other.LibraryName &&
Nid == other.Nid &&
ExportName == other.ExportName &&
Target == other.Target;
Target == other.Target &&
PreferLle == other.PreferLle;
public override bool Equals(object? obj) => Equals(obj as ExportModel);
@@ -73,6 +85,7 @@ public sealed class SysAbiExportGenerator : IIncrementalGenerator
hash = (hash * 31) + ContainingType.GetHashCode();
hash = (hash * 31) + MethodName.GetHashCode();
hash = (hash * 31) + Nid.GetHashCode();
hash = (hash * 31) + PreferLle.GetHashCode();
return hash;
}
}
@@ -80,80 +93,90 @@ public sealed class SysAbiExportGenerator : IIncrementalGenerator
public void Initialize(IncrementalGeneratorInitializationContext context)
{
var exports = context.SyntaxProvider
var exportGroups = context.SyntaxProvider
.ForAttributeWithMetadataName(
AttributeMetadataName,
static (node, _) => node is MethodDeclarationSyntax,
static (attributeContext, _) => CreateModel(attributeContext))
.Where(static model => model is not null)
static (attributeContext, _) => CreateModels(attributeContext))
.Where(static models => !models.IsDefaultOrEmpty)
.Collect();
var assemblyName = context.CompilationProvider
.Select(static (compilation, _) => compilation.AssemblyName ?? "Assembly");
context.RegisterSourceOutput(
exports.Combine(assemblyName),
exportGroups.Combine(assemblyName),
static (productionContext, source) => Emit(productionContext, source.Left!, source.Right));
}
private static ExportModel? CreateModel(GeneratorAttributeSyntaxContext context)
private static ImmutableArray<ExportModel> CreateModels(GeneratorAttributeSyntaxContext context)
{
if (context.TargetSymbol is not IMethodSymbol method ||
!SysAbiExportShape.IsAccessibleFromGeneratedCode(method))
{
return null;
return ImmutableArray<ExportModel>.Empty;
}
var shape = SysAbiExportShape.Classify(method, out var typedParameterKinds);
if (shape == SysAbiExportShape.HandlerShape.Invalid)
{
return null;
return ImmutableArray<ExportModel>.Empty;
}
var attribute = context.Attributes[0];
var arguments = SysAbiExportShape.ReadArguments(attribute);
var nid = arguments.Nid;
var exportName = arguments.ExportName;
// Mirror ModuleManager.ResolveExportInfo: a missing NID resolves from the export
// name (algorithmically — equivalent to the runtime catalog lookup, which was
// built with the same computation); a missing name falls back to the method name.
if (string.IsNullOrWhiteSpace(nid) && !string.IsNullOrWhiteSpace(exportName))
var models = ImmutableArray.CreateBuilder<ExportModel>(context.Attributes.Length);
foreach (var attribute in context.Attributes)
{
nid = Ps5Nid.Compute(exportName);
var arguments = SysAbiExportShape.ReadArguments(attribute);
var nid = arguments.Nid;
var exportName = arguments.ExportName;
// Mirror ModuleManager.ResolveExportInfo: a missing NID resolves from the
// export name. A missing name falls back to the method name.
if (string.IsNullOrWhiteSpace(nid) && !string.IsNullOrWhiteSpace(exportName))
{
nid = Ps5Nid.Compute(exportName);
}
if (string.IsNullOrWhiteSpace(nid))
{
continue;
}
if (string.IsNullOrWhiteSpace(exportName))
{
exportName = method.Name;
}
var libraryName = string.IsNullOrWhiteSpace(arguments.LibraryName) ? "libKernel" : arguments.LibraryName;
models.Add(new ExportModel(
method.ContainingType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat),
method.Name,
shape,
typedParameterKinds,
libraryName,
nid!,
exportName!,
arguments.Target,
arguments.PreferLle));
}
if (string.IsNullOrWhiteSpace(nid))
{
return null;
}
if (string.IsNullOrWhiteSpace(exportName))
{
exportName = method.Name;
}
var libraryName = string.IsNullOrWhiteSpace(arguments.LibraryName) ? "libKernel" : arguments.LibraryName;
return new ExportModel(
method.ContainingType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat),
method.Name,
shape,
typedParameterKinds,
libraryName,
nid!,
exportName!,
arguments.Target);
return models.ToImmutable();
}
private static void Emit(
SourceProductionContext context,
ImmutableArray<ExportModel?> exports,
ImmutableArray<ImmutableArray<ExportModel>> exportGroups,
string assemblyName)
{
// No exports, no registry: an assembly that merely references the analyzer
// (e.g. SharpEmu.HLE itself) must not mint a colliding
// SharpEmu.Generated.SysAbiExportRegistry type.
if (exports.IsDefaultOrEmpty)
var exportCount = 0;
foreach (var group in exportGroups)
{
exportCount += group.Length;
}
if (exportCount == 0)
{
return;
}
@@ -175,24 +198,23 @@ public sealed class SysAbiExportGenerator : IIncrementalGenerator
builder.AppendLine(" public static global::System.Collections.Generic.IReadOnlyList<global::SharpEmu.HLE.ExportedFunction> CreateExports(");
builder.AppendLine(" global::SharpEmu.HLE.Generation registrationGeneration)");
builder.AppendLine(" {");
builder.AppendLine($" var exports = new global::System.Collections.Generic.List<global::SharpEmu.HLE.ExportedFunction>({exports.Length});");
builder.AppendLine($" var exports = new global::System.Collections.Generic.List<global::SharpEmu.HLE.ExportedFunction>({exportCount});");
foreach (var export in exports)
foreach (var group in exportGroups)
{
if (export is null)
foreach (var export in group)
{
continue;
var function = export.Shape switch
{
SysAbiExportShape.HandlerShape.ContextOnly => $"{export.ContainingType}.{export.MethodName}",
SysAbiExportShape.HandlerShape.Parameterless => $"static _ => {export.ContainingType}.{export.MethodName}()",
_ => TypedThunk(export),
};
builder.AppendLine(
$" Add(exports, registrationGeneration, {Literal(export.LibraryName)}, {Literal(export.Nid)}, " +
$"{Literal(export.ExportName)}, (global::SharpEmu.HLE.Generation){export.Target}, " +
$"{(export.PreferLle ? "true" : "false")}, {function});");
}
var function = export.Shape switch
{
SysAbiExportShape.HandlerShape.ContextOnly => $"{export.ContainingType}.{export.MethodName}",
SysAbiExportShape.HandlerShape.Parameterless => $"static _ => {export.ContainingType}.{export.MethodName}()",
_ => TypedThunk(export),
};
builder.AppendLine(
$" Add(exports, registrationGeneration, {Literal(export.LibraryName)}, {Literal(export.Nid)}, " +
$"{Literal(export.ExportName)}, (global::SharpEmu.HLE.Generation){export.Target}, {function});");
}
builder.AppendLine(" return exports;");
@@ -205,6 +227,7 @@ public sealed class SysAbiExportGenerator : IIncrementalGenerator
builder.AppendLine(" string nid,");
builder.AppendLine(" string exportName,");
builder.AppendLine(" global::SharpEmu.HLE.Generation attributeTarget,");
builder.AppendLine(" bool preferLle,");
builder.AppendLine(" global::SharpEmu.HLE.SysAbiFunction function)");
builder.AppendLine(" {");
builder.AppendLine(" var target = attributeTarget == global::SharpEmu.HLE.Generation.None ? registrationGeneration : attributeTarget;");
@@ -213,7 +236,7 @@ public sealed class SysAbiExportGenerator : IIncrementalGenerator
builder.AppendLine(" return;");
builder.AppendLine(" }");
builder.AppendLine();
builder.AppendLine(" exports.Add(new global::SharpEmu.HLE.ExportedFunction(libraryName, nid, exportName, target, function));");
builder.AppendLine(" exports.Add(new global::SharpEmu.HLE.ExportedFunction(libraryName, nid, exportName, target, function, preferLle));");
builder.AppendLine(" }");
builder.AppendLine("}");
@@ -18,18 +18,20 @@ public static class SysAbiExportShape
public readonly struct Arguments
{
public Arguments(string libraryName, string nid, string exportName, int target)
public Arguments(string libraryName, string nid, string exportName, int target, bool preferLle)
{
LibraryName = libraryName;
Nid = nid;
ExportName = exportName;
Target = target;
PreferLle = preferLle;
}
public string LibraryName { get; }
public string Nid { get; }
public string ExportName { get; }
public int Target { get; }
public bool PreferLle { get; }
}
/// <summary>
@@ -205,6 +207,7 @@ public static class SysAbiExportShape
var nid = string.Empty;
var exportName = string.Empty;
var target = 0;
var preferLle = false;
foreach (var argument in attribute.NamedArguments)
{
switch (argument.Key)
@@ -221,9 +224,12 @@ public static class SysAbiExportShape
case "Target":
target = argument.Value.Value is int value ? value : 0;
break;
case "PreferLle":
preferLle = argument.Value.Value is bool boolValue && boolValue;
break;
}
}
return new Arguments(libraryName, nid, exportName, target);
return new Arguments(libraryName, nid, exportName, target, preferLle);
}
}
@@ -0,0 +1,60 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.Core.Cpu.Native;
using SharpEmu.HLE;
using Xunit;
namespace SharpEmu.Libs.Tests.Cpu;
public sealed class DirectExecutionBackendLlePreferenceTests
{
[Fact]
public void ExplicitLlePreference_AllowsNonKernelRegisteredExport()
{
var export = Export("libSceNpCppWebApi", preferLle: true);
Assert.True(DirectExecutionBackend.ShouldResolveRegisteredExportViaLle(
export,
preferLleForLibc: false));
}
[Fact]
public void ExplicitLlePreference_CannotOverrideKernelHleBoundary()
{
var export = Export("libKernel", preferLle: true);
Assert.False(DirectExecutionBackend.ShouldResolveRegisteredExportViaLle(
export,
preferLleForLibc: true));
}
[Fact]
public void RegisteredExportWithoutLlePreference_RemainsHle()
{
var export = Export("libSceNpCppWebApi", preferLle: false);
Assert.False(DirectExecutionBackend.ShouldResolveRegisteredExportViaLle(
export,
preferLleForLibc: false));
}
[Fact]
public void ExistingLibcPolicy_CanStillSelectRegisteredFirmwareExport()
{
var export = Export("libSceLibcInternal", preferLle: false);
Assert.True(DirectExecutionBackend.ShouldResolveRegisteredExportViaLle(
export,
preferLleForLibc: true));
}
private static ExportedFunction Export(string libraryName, bool preferLle) =>
new(
libraryName,
"Zxa0VhQVTsk",
"sceKernelWaitSema",
Generation.Gen5,
static _ => 0,
preferLle);
}
@@ -53,6 +53,40 @@ public sealed class SysAbiExportAnalyzerTests
AssertSingle(diagnostics, "SHEM001");
}
[Fact]
public void DuplicateNidOnTheSameMultiAttributeHandlerIsReported()
{
var diagnostics = Analyze("""
using SharpEmu.HLE;
public static class Exports
{
[SysAbiExport(Nid = "Zxa0VhQVTsk", ExportName = "sceKernelWaitSema")]
[SysAbiExport(Nid = "Zxa0VhQVTsk", ExportName = "sceKernelWaitSema")]
public static int Shared(CpuContext ctx) => 0;
}
""");
AssertSingle(diagnostics, "SHEM001");
}
[Fact]
public void EveryAttributeOnAMultiAttributeHandlerIsAnalyzed()
{
var diagnostics = Analyze("""
using SharpEmu.HLE;
public static class Exports
{
[SysAbiExport(Nid = "Zxa0VhQVTsk", ExportName = "sceKernelWaitSema")]
[SysAbiExport(Nid = "not_a_nid")]
public static int Shared(CpuContext ctx) => 0;
}
""");
AssertSingle(diagnostics, "SHEM002");
}
[Fact]
public void MalformedNidIsReported()
{
@@ -36,6 +36,11 @@ public sealed class SysAbiExportGeneratorTests
// 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;
}
""";
@@ -123,6 +128,22 @@ public sealed class SysAbiExportGeneratorTests
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()
{