// 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("\"", "\\\"") + "\"";
}