// Copyright (C) 2026 SharpEmu Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later using System.Collections.Immutable; using System.Text; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Text; namespace SharpEmu.SourceGenerators; /// /// Emits the SysAbi export registry at compile time: one static class per assembly whose /// CreateExports(Generation) lists every [SysAbiExport] handler — generation filtering, /// name fallback, and library default preserved from the retired reflection scan. NIDs /// omitted from attributes are derived from the export name with the PS NID algorithm /// (the same computation the runtime symbol catalog was built from). Handlers written /// with typed signatures get a SysV register-unmarshalling thunk emitted here. /// /// Invalid declarations are skipped here and rejected by SysAbiExportAnalyzer as build /// errors, so nothing can be silently dropped. /// [Generator] public sealed class SysAbiExportGenerator : IIncrementalGenerator { private const string AttributeMetadataName = SysAbiExportShape.SysAbiExportAttributeName; private sealed class ExportModel : IEquatable { public ExportModel(string containingType, string methodName, SysAbiExportShape.HandlerShape shape, string typedParameterKinds, string libraryName, string nid, string exportName, int target) { ContainingType = containingType; MethodName = methodName; Shape = shape; TypedParameterKinds = typedParameterKinds; LibraryName = libraryName; Nid = nid; ExportName = exportName; Target = target; } public string ContainingType { get; } public string MethodName { get; } public SysAbiExportShape.HandlerShape Shape { get; } // Deliberately a comma-joined string ("uint,int,cstring:4096") rather than an // array: the model must be equatable for incremental-generator caching, and a // string gets that for free where an array would need a custom comparer. public string TypedParameterKinds { get; } public string LibraryName { get; } public string Nid { get; } public string ExportName { get; } public int Target { get; } public bool Equals(ExportModel? other) => other is not null && ContainingType == other.ContainingType && MethodName == other.MethodName && Shape == other.Shape && TypedParameterKinds == other.TypedParameterKinds && LibraryName == other.LibraryName && Nid == other.Nid && ExportName == other.ExportName && Target == other.Target; public override bool Equals(object? obj) => Equals(obj as ExportModel); public override int GetHashCode() { unchecked { var hash = 17; hash = (hash * 31) + ContainingType.GetHashCode(); hash = (hash * 31) + MethodName.GetHashCode(); hash = (hash * 31) + Nid.GetHashCode(); return hash; } } } public void Initialize(IncrementalGeneratorInitializationContext context) { var exports = context.SyntaxProvider .ForAttributeWithMetadataName( AttributeMetadataName, static (node, _) => node is MethodDeclarationSyntax, static (attributeContext, _) => CreateModel(attributeContext)) .Where(static model => model is not null) .Collect(); var assemblyName = context.CompilationProvider .Select(static (compilation, _) => compilation.AssemblyName ?? "Assembly"); context.RegisterSourceOutput( exports.Combine(assemblyName), static (productionContext, source) => Emit(productionContext, source.Left!, source.Right)); } private static ExportModel? CreateModel(GeneratorAttributeSyntaxContext context) { if (context.TargetSymbol is not IMethodSymbol method || !SysAbiExportShape.IsAccessibleFromGeneratedCode(method)) { return null; } var shape = SysAbiExportShape.Classify(method, out var typedParameterKinds); if (shape == SysAbiExportShape.HandlerShape.Invalid) { return null; } 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)) { nid = Ps5Nid.Compute(exportName); } 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); } private static void Emit( SourceProductionContext context, ImmutableArray exports, 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) { return; } var builder = new StringBuilder(); builder.AppendLine("// "); builder.AppendLine("#nullable enable"); builder.AppendLine(); builder.AppendLine("namespace SharpEmu.Generated;"); builder.AppendLine(); builder.AppendLine("/// Compile-time SysAbi export registry for " + assemblyName + "."); builder.AppendLine("public static class SysAbiExportRegistry"); builder.AppendLine("{"); builder.AppendLine(" /// "); builder.AppendLine(" /// Exports effective for the given registration generation, with the same"); builder.AppendLine(" /// semantics as the reflection scan: an attribute Target of None inherits the"); builder.AppendLine(" /// registration generation, and exports outside it are skipped."); builder.AppendLine(" /// "); 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});"); foreach (var export in exports) { if (export is null) { 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}, {function});"); } builder.AppendLine(" return exports;"); builder.AppendLine(" }"); builder.AppendLine(); builder.AppendLine(" private static void Add("); builder.AppendLine(" global::System.Collections.Generic.List exports,"); builder.AppendLine(" global::SharpEmu.HLE.Generation registrationGeneration,"); builder.AppendLine(" string libraryName,"); builder.AppendLine(" string nid,"); builder.AppendLine(" string exportName,"); builder.AppendLine(" global::SharpEmu.HLE.Generation attributeTarget,"); builder.AppendLine(" global::SharpEmu.HLE.SysAbiFunction function)"); builder.AppendLine(" {"); builder.AppendLine(" var target = attributeTarget == global::SharpEmu.HLE.Generation.None ? registrationGeneration : attributeTarget;"); builder.AppendLine(" if ((target & registrationGeneration) == 0)"); builder.AppendLine(" {"); builder.AppendLine(" return;"); builder.AppendLine(" }"); builder.AppendLine(); builder.AppendLine(" exports.Add(new global::SharpEmu.HLE.ExportedFunction(libraryName, nid, exportName, target, function));"); builder.AppendLine(" }"); builder.AppendLine("}"); context.AddSource("SysAbiExportRegistry.g.cs", SourceText.From(builder.ToString(), Encoding.UTF8)); } /// /// SysV integer-register unmarshalling: parameter i reads argument register i as a /// raw ulong and reinterprets it with an unchecked cast, exactly the idiom /// hand-written handlers use today. [GuestCString] parameters read the register as /// a guest pointer and marshal the null-terminated UTF-8 string up front, failing /// the call with ORBIS_GEN2_ERROR_MEMORY_FAULT before the handler runs. /// private static string TypedThunk(ExportModel export) { var kinds = export.TypedParameterKinds.Split(','); var arguments = new string[kinds.Length]; var reads = new StringBuilder(); for (var index = 0; index < kinds.Length; index++) { var register = "ctx[global::SharpEmu.HLE.CpuRegister." + SysAbiExportShape.ArgumentRegisters[index] + "]"; if (kinds[index].StartsWith("cstring:", StringComparison.Ordinal)) { var maxLength = kinds[index].Substring("cstring:".Length); var variable = "guestString" + index; reads.AppendLine($" if (!ctx.TryReadNullTerminatedUtf8({register}, {maxLength}, out var {variable}))"); reads.AppendLine(" {"); reads.AppendLine(" return ctx.SetReturn(global::SharpEmu.HLE.OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);"); reads.AppendLine(" }"); reads.AppendLine(); arguments[index] = variable; continue; } arguments[index] = kinds[index] == "ulong" ? register : "unchecked((" + kinds[index] + ")" + register + ")"; } var invocation = export.ContainingType + "." + export.MethodName + "(ctx, " + string.Join(", ", arguments) + ")"; if (reads.Length == 0) { return "static ctx => " + invocation; } var builder = new StringBuilder(); builder.AppendLine("static ctx =>"); builder.AppendLine(" {"); builder.Append(reads); builder.AppendLine(" return " + invocation + ";"); builder.Append(" }"); return builder.ToString(); } private static string Literal(string value) => "\"" + value.Replace("\\", "\\\\").Replace("\"", "\\\"") + "\""; }