diff --git a/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.cs b/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.cs index cf7df717..cb089ed4 100644 --- a/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.cs +++ b/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.cs @@ -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) || diff --git a/src/SharpEmu.HLE/ExportedFunction.cs b/src/SharpEmu.HLE/ExportedFunction.cs index 4e9eec3e..d11e7bf1 100644 --- a/src/SharpEmu.HLE/ExportedFunction.cs +++ b/src/SharpEmu.HLE/ExportedFunction.cs @@ -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; } + + /// + /// A loaded guest export is authoritative for this registration. The HLE function + /// remains available as an explicit fallback when no usable guest target exists. + /// + public bool PreferLle { get; } } diff --git a/src/SharpEmu.HLE/SysAbiExportAttribute.cs b/src/SharpEmu.HLE/SysAbiExportAttribute.cs index 6a412205..72c3f15c 100644 --- a/src/SharpEmu.HLE/SysAbiExportAttribute.cs +++ b/src/SharpEmu.HLE/SysAbiExportAttribute.cs @@ -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; + + /// + /// 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. + /// + public bool PreferLle { get; set; } } diff --git a/src/SharpEmu.SourceGenerators/SysAbiExportAnalyzer.cs b/src/SharpEmu.SourceGenerators/SysAbiExportAnalyzer.cs index c2ef1f10..4fefc434 100644 --- a/src/SharpEmu.SourceGenerators/SysAbiExportAnalyzer.cs +++ b/src/SharpEmu.SourceGenerators/SysAbiExportAnalyzer.cs @@ -102,17 +102,16 @@ public sealed class SysAbiExportAnalyzer : DiagnosticAnalyzer ConcurrentDictionary exportsByNid) { var method = (IMethodSymbol)context.Symbol; - AttributeData? exportAttribute = null; + var exportAttributes = ImmutableArray.CreateBuilder(); 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? catalogNames, + ConcurrentDictionary 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, diff --git a/src/SharpEmu.SourceGenerators/SysAbiExportGenerator.cs b/src/SharpEmu.SourceGenerators/SysAbiExportGenerator.cs index 6e8b8e9c..ac2f9793 100644 --- a/src/SharpEmu.SourceGenerators/SysAbiExportGenerator.cs +++ b/src/SharpEmu.SourceGenerators/SysAbiExportGenerator.cs @@ -27,7 +27,16 @@ public sealed class SysAbiExportGenerator : IIncrementalGenerator private sealed class ExportModel : IEquatable { - 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 CreateModels(GeneratorAttributeSyntaxContext context) { if (context.TargetSymbol is not IMethodSymbol method || !SysAbiExportShape.IsAccessibleFromGeneratedCode(method)) { - return null; + return ImmutableArray.Empty; } var shape = SysAbiExportShape.Classify(method, out var typedParameterKinds); if (shape == SysAbiExportShape.HandlerShape.Invalid) { - return null; + return ImmutableArray.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(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 exports, + ImmutableArray> 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 CreateExports("); builder.AppendLine(" global::SharpEmu.HLE.Generation registrationGeneration)"); builder.AppendLine(" {"); - builder.AppendLine($" var exports = new global::System.Collections.Generic.List({exports.Length});"); + builder.AppendLine($" var exports = new global::System.Collections.Generic.List({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("}"); diff --git a/src/SharpEmu.SourceGenerators/SysAbiExportShape.cs b/src/SharpEmu.SourceGenerators/SysAbiExportShape.cs index 00d0dbc5..865a590d 100644 --- a/src/SharpEmu.SourceGenerators/SysAbiExportShape.cs +++ b/src/SharpEmu.SourceGenerators/SysAbiExportShape.cs @@ -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; } } /// @@ -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); } } diff --git a/tests/SharpEmu.Libs.Tests/Cpu/DirectExecutionBackendLlePreferenceTests.cs b/tests/SharpEmu.Libs.Tests/Cpu/DirectExecutionBackendLlePreferenceTests.cs new file mode 100644 index 00000000..fd168531 --- /dev/null +++ b/tests/SharpEmu.Libs.Tests/Cpu/DirectExecutionBackendLlePreferenceTests.cs @@ -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); +} diff --git a/tests/SharpEmu.SourceGenerators.Tests/SysAbiExportAnalyzerTests.cs b/tests/SharpEmu.SourceGenerators.Tests/SysAbiExportAnalyzerTests.cs index 68dca82c..d35a91bd 100644 --- a/tests/SharpEmu.SourceGenerators.Tests/SysAbiExportAnalyzerTests.cs +++ b/tests/SharpEmu.SourceGenerators.Tests/SysAbiExportAnalyzerTests.cs @@ -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() { diff --git a/tests/SharpEmu.SourceGenerators.Tests/SysAbiExportGeneratorTests.cs b/tests/SharpEmu.SourceGenerators.Tests/SysAbiExportGeneratorTests.cs index 9e8f16be..a0752000 100644 --- a/tests/SharpEmu.SourceGenerators.Tests/SysAbiExportGeneratorTests.cs +++ b/tests/SharpEmu.SourceGenerators.Tests/SysAbiExportGeneratorTests.cs @@ -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() {