using System; using System.Collections.Generic; using System.Collections.Immutable; using System.IO; using System.Linq; using System.Reflection.Metadata; using System.Text; using System.Text.Json; using System.Text.Json.Serialization; using System.Threading; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp.Syntax; namespace ScriptCoreGenerator; internal class GlueMethod { [JsonPropertyName("name")] public string Name { get; set; } [JsonPropertyName("return_type")] public string ReturnType { get; set; } [JsonPropertyName("guaranteed_short_running")] public bool GuaranteedShortRunning { get; set; } [JsonPropertyName("parameters")] public List Parameters { get; set; } internal class Parameter { [JsonPropertyName("name")] public string Name { get; set; } [JsonPropertyName("type")] public string Type { get; set; } } } [Generator] public class ScriptGlueGenerator : IIncrementalGenerator { public class GeneratorInput { public ImmutableArray TypeMappings; public ImmutableArray GlueFunctionDefinitions; public ImmutableArray ExistingScriptGlueClasses; public ImmutableArray ExistingEngineFunctionStructs; } public void Initialize(IncrementalGeneratorInitializationContext context) { var mappedTypes = context.SyntaxProvider .CreateSyntaxProvider( CouldBeMappedType, GetTypeOrNull) .Where(type => type is not null) .Collect(); var manualScriptGlueWrappers = context.SyntaxProvider .CreateSyntaxProvider( IsScriptGlueClass, GetSymbol) .Where(type => type is not null) .SelectMany((type, _) => type!.MemberNames) .Collect(); var manualEngineFunctions = context.SyntaxProvider .CreateSyntaxProvider( IsEngineFunctionsStruct, GetSymbol) .Where(type => type is not null) .SelectMany((type, _) => type!.MemberNames) .Collect(); var glueFunctions = context.AdditionalTextsProvider .Where(text => text.Path.EndsWith("ScriptGlue.json", StringComparison.OrdinalIgnoreCase)) .Select((text, token) => text.GetText(token)?.ToString()) .Where(text => text is not null)! .Collect(); // Kombiniere zu einem sauberen Record var combinedProvider = mappedTypes .Combine(manualScriptGlueWrappers) .Combine(manualEngineFunctions) .Combine(glueFunctions) .Select((input, _) => new GeneratorInput{ TypeMappings = input.Left.Left.Left, ExistingScriptGlueClasses = input.Left.Left.Right, ExistingEngineFunctionStructs = input.Left.Right, GlueFunctionDefinitions = input.Right }); context.RegisterSourceOutput(combinedProvider, GenerateCode); } private class MappedType { public string BeefTypeName; public string? UnderlyingEnumTypeName; public bool HasUnderlyingType => UnderlyingEnumTypeName is not null; private string _cSharpTypeName; private string _cSharpWrapperType; public (MappedType, TypeModifier)? CSharpUnderlyingEnumType; public string? WrapperConvertInput; public string? WrapperCleanupInput; public string? WrapperOutConversion; public string? ReturnValueConversion = "return returnValue;"; public string CSharpWrapperType { get => _cSharpWrapperType ?? CSharpTypeName; set => _cSharpWrapperType = value; } public string CSharpTypeName { get => _cSharpTypeName ?? BeefTypeName; set => _cSharpTypeName = value; } } private enum TypeModifier { None, Ref, In, Out, Pointer } private static (MappedType Type, TypeModifier Modifier) DecodeType(string beefType, Dictionary beefTypeToMappedType) { TypeModifier modifier = TypeModifier.None; if (beefType.StartsWith("ref ")) { modifier = TypeModifier.Ref; beefType = beefType.Substring(4); } else if (beefType.StartsWith("out ")) { modifier = TypeModifier.Out; beefType = beefType.Substring(4); } else if (beefType.StartsWith("in ")) { modifier = TypeModifier.In; beefType = beefType.Substring(3); } if (beefTypeToMappedType.TryGetValue(beefType, out var mapping)) return (mapping, modifier); if (beefType.EndsWith("*")) { modifier = TypeModifier.Pointer; beefType = beefType.Substring(0, beefType.Length - 1); } if (beefTypeToMappedType.TryGetValue(beefType, out MappedType? mappedType)) { return (mappedType, modifier); } int colonIndex = beefType.IndexOf(':'); bool isEnum = colonIndex != -1; string beefTypeName = beefType; string? underlyingEnumType = null; if (isEnum) { beefTypeName = beefType.Substring(0, colonIndex).Trim(); underlyingEnumType = beefType.Substring(colonIndex + 1).Trim(); if (beefTypeToMappedType.TryGetValue(beefTypeName, out mappedType)) { if (mappedType.UnderlyingEnumTypeName is null) { mappedType.UnderlyingEnumTypeName = underlyingEnumType; mappedType.CSharpUnderlyingEnumType = DecodeType(underlyingEnumType, beefTypeToMappedType); } return (mappedType, modifier); } } MappedType newMapping = new MappedType() { BeefTypeName = beefTypeName, UnderlyingEnumTypeName = underlyingEnumType, CSharpUnderlyingEnumType = underlyingEnumType == null ? null : DecodeType(underlyingEnumType,beefTypeToMappedType), }; beefTypeToMappedType[beefType] = newMapping; beefTypeToMappedType[beefTypeName] = newMapping; return (newMapping, modifier); } private static void InitializeTypes(Dictionary beefTypeToMappedType) { beefTypeToMappedType.Add("void", new MappedType { BeefTypeName = "void", ReturnValueConversion = null }); beefTypeToMappedType.Add("uint8*", new MappedType { BeefTypeName = "uint8*", CSharpTypeName = "byte*" }); beefTypeToMappedType.Add("char16*", new MappedType { BeefTypeName = "char16*", CSharpTypeName = "char*", CSharpWrapperType = "string", ReturnValueConversion = null, // TODO! WrapperConvertInput = "fixed (char* {0} = {1}) {{", WrapperCleanupInput = "}}" }); beefTypeToMappedType.Add("char8*", new MappedType { BeefTypeName = "char8*", CSharpTypeName = "byte*", CSharpWrapperType = "string", ReturnValueConversion = "return Marshal.PtrToStringUTF8((IntPtr)returnValue);", WrapperConvertInput = "byte* {0} = (byte*)Marshal.StringToCoTaskMemUTF8({1});", WrapperCleanupInput = "Marshal.FreeCoTaskMem((IntPtr){0});", WrapperOutConversion = "{1} = Marshal.PtrToStringUTF8((IntPtr){0}) ?? \"\";" }); beefTypeToMappedType.Add("int32", new MappedType { BeefTypeName = "int32", CSharpTypeName = "int" }); beefTypeToMappedType.Add("GlitchyEngine.Math.Quaternion", new MappedType { BeefTypeName = "System.Numerics.Quaternion", ReturnValueConversion = null }); beefTypeToMappedType.Add("void*", new MappedType { BeefTypeName = "void*", CSharpTypeName = "void*", CSharpWrapperType = "IntPtr", ReturnValueConversion = "return (IntPtr)returnValue;", WrapperConvertInput = "void* {0} = (void*){1};", WrapperOutConversion = "{1} = (IntPtr){0};" }); beefTypeToMappedType.Add("GlitchyEngine.Scripting.ScriptGlue.EngineResult : int32 as void", new MappedType { BeefTypeName = "GlitchyEngine.Scripting.ScriptGlue.EngineResult", CSharpTypeName = "GlitchyEngine.Core.EngineResult", CSharpWrapperType = "void", ReturnValueConversion = "EngineErrors.ThrowIfError(returnValue);" }); beefTypeToMappedType.Add("GlitchyEngine.Scripting.ScriptGlue.EngineResult : int32 as bool", new MappedType { BeefTypeName = "GlitchyEngine.Scripting.ScriptGlue.EngineResult", CSharpTypeName = "GlitchyEngine.Core.EngineResult", CSharpWrapperType = "bool", ReturnValueConversion = "EngineErrors.ThrowIfError(returnValue);\nreturn (returnValue == EngineResult.Ok);" }); beefTypeToMappedType.Add("System.StringView", new MappedType() { BeefTypeName = "System.StringView", CSharpTypeName = "GlitchyEngine.Native.StringView", CSharpWrapperType = "string", ReturnValueConversion = "return returnValue.ToString();", WrapperConvertInput = "var {0} = GlitchyEngine.Native.StringView.FromManagedString({1});", WrapperCleanupInput = "GlitchyEngine.Native.StringView.FreeNativeMemory({0});", WrapperOutConversion = "{1} = {0}.ToString();" }); } private static void GenerateFunctionPointer(GlueMethod method, Dictionary beefTypeToMappedType, StringBuilder output) { var (returnType, _) = DecodeType(method.ReturnType, beefTypeToMappedType); List parameters = new(); StringBuilder parameterText = new(); foreach (var param in method.Parameters) { var (parameterType, parameterModifier) = DecodeType(param.Type, beefTypeToMappedType); parameters.Add(parameterType); parameterText.Append(parameterType.CSharpTypeName); switch (parameterModifier) { case TypeModifier.In: case TypeModifier.Out: case TypeModifier.Ref: case TypeModifier.Pointer: parameterText.Append("*"); break; } parameterText.Append(", "); } string callingConvention = "Cdecl"; #if !DEBUG // Normally when calling into unmanaged code the current thread transitions // from cooperative GC mode (all threads block for GC collection) to preemtive (other threads can gc independently from us). // This is done to prevent long running native code (e.g. native code opens a message box) from blocking all threads because // they wait for GC to happen, which can lead to problems. If we know however that the called native function is very short // (short than the GC transition itself) then we can explicitly prevent the suppression. // We rely on the engine (ScriptGlue.bf) to tell us that information. // // A GuaranteedShortRunning function should have a VERY short runtime (< 1μs), must NEVER block, and NEVER call back into managed code! // // We only enable it in release builds, as it will prevent the mixed mode debugger from stepping over the managed-native-border. if (method.GuaranteedShortRunning) { callingConvention += ", SuppressGCTransition"; } #endif output.AppendLine($" public delegate* unmanaged[{callingConvention}]<{parameterText}{returnType.CSharpTypeName}> {method.Name};"); } private static void GenerateWrapper(GlueMethod method, Dictionary beefTypeToMappedType, StringBuilder output) { List parameters = new(); StringBuilder parameterText = new(); List parameterConversions = new(); List cleanups = new(); List afterReturnCleanups = new(); StringBuilder call = new(); var (returnType, _) = DecodeType(method.ReturnType, beefTypeToMappedType); if (returnType.ReturnValueConversion is not null) { call.Append("var returnValue = "); } call.Append($"_engineFunctions.{method.Name}("); foreach (var param in method.Parameters) { var (parameterType, parameterModifier) = DecodeType(param.Type, beefTypeToMappedType); parameters.Add(parameterType); if (parameterText.Length > 0) { parameterText.Append(", "); call.Append(", "); } switch (parameterModifier) { case TypeModifier.In: parameterText.Append("in "); break; case TypeModifier.Out: parameterText.Append("out "); break; case TypeModifier.Ref: parameterText.Append("ref "); break; } parameterText.Append($"{parameterType.CSharpWrapperType}"); if (parameterModifier == TypeModifier.Pointer) parameterText.Append("*"); parameterText.Append($" {param.Name}"); if (parameterModifier == TypeModifier.Out && parameterType.WrapperOutConversion is not null) { string tmpArgName = $"{param.Name}Tmp"; call.Append($"&{tmpArgName}"); parameterConversions.Add($"\t\t{parameterType.CSharpTypeName} {tmpArgName} = default;\n"); StringBuilder cleanup = new(); cleanup.Append("\t\t"); cleanup.AppendFormat(parameterType.WrapperOutConversion, tmpArgName, param.Name); cleanup.AppendLine(); cleanups.Add(cleanup.ToString()); } else if (parameterType.WrapperConvertInput is not null) { switch (parameterModifier) { case TypeModifier.In: case TypeModifier.Out: case TypeModifier.Ref: case TypeModifier.Pointer: call.Append("&"); break; } string convertedParamName = $"{param.Name}Converted"; StringBuilder parameterConversion = new(); StringBuilder cleanup = new(); parameterConversion.AppendLine(); parameterConversion.Append("\t\t"); parameterConversion.AppendFormat(parameterType.WrapperConvertInput, convertedParamName, param.Name); if (parameterType.WrapperCleanupInput is not null) { cleanup.Append("\t\t"); cleanup.AppendFormat(parameterType.WrapperCleanupInput, convertedParamName); cleanup.AppendLine(); } call.Append(convertedParamName); parameterConversions.Add(parameterConversion.ToString()); cleanups.Add(cleanup.ToString()); } else { string paramName = param.Name; switch (parameterModifier) { case TypeModifier.In: case TypeModifier.Out: case TypeModifier.Ref: paramName = "__fixed" + param.Name; parameterConversions.Add($"\t\tfixed ({parameterType.CSharpTypeName}* {paramName} = &{param.Name}) {{"); afterReturnCleanups.Add("\t\t}"); break; } call.Append(paramName); } } call.Append(");"); cleanups.Reverse(); output.AppendLine($$""" public static {{returnType.CSharpWrapperType}} {{method.Name}}({{parameterText}}) { {{string.Join("\n", parameterConversions)}} {{call}} {{string.Join("\n", cleanups)}} {{returnType.ReturnValueConversion}} {{string.Join("\n", afterReturnCleanups)}} } """); } private static void GenerateCode( SourceProductionContext context, GeneratorInput input) { Dictionary beefTypeToMappedType = new(); InitializeTypes(beefTypeToMappedType); foreach (ITypeSymbol? mapping in input.TypeMappings) { if (mapping is null) continue; string csharpTypeName = mapping.ToDisplayString(); foreach (AttributeData attribute in mapping.GetAttributes() .Where(attribute => attribute.AttributeClass?.Name == "EngineClassAttribute")) { string? beefTypeName = attribute.ConstructorArguments[0].Value?.ToString(); if (beefTypeName is null) continue; (MappedType newMapping, _) = DecodeType(beefTypeName, beefTypeToMappedType); newMapping.CSharpTypeName = csharpTypeName; } } StringBuilder engineFunctions = new StringBuilder(); StringBuilder wrappers = new StringBuilder(); engineFunctions.Append(""" // using System.Collections.Generic; namespace GlitchyEngine; unsafe internal partial struct EngineFunctions { """); wrappers.Append(""" // using System; using System.Collections.Generic; using System.Runtime.InteropServices; using GlitchyEngine.Core; namespace GlitchyEngine; internal static unsafe partial class ScriptGlue { """); List methods = new List(); foreach (string glueFunctions in input.GlueFunctionDefinitions) { methods.AddRange(JsonSerializer.Deserialize(glueFunctions) ?? []); } foreach (GlueMethod method in methods) { if (!input.ExistingEngineFunctionStructs.Contains(method.Name)) { GenerateFunctionPointer(method, beefTypeToMappedType, engineFunctions); } if (!input.ExistingScriptGlueClasses.Contains(method.Name)) { GenerateWrapper(method, beefTypeToMappedType, wrappers); } } engineFunctions.Append("\n}"); wrappers.Append("\n}"); context.AddSource("EngineFunctions.g.cs", engineFunctions.ToString()); context.AddSource("ScriptGlue.g.cs", wrappers.ToString()); } private static bool IsScriptGlueClass( SyntaxNode syntaxNode, CancellationToken cancellationToken) { return syntaxNode is ClassDeclarationSyntax { Identifier.ValueText: "ScriptGlue" }; } private static bool IsEngineFunctionsStruct ( SyntaxNode syntaxNode, CancellationToken cancellationToken) { return syntaxNode is StructDeclarationSyntax { Identifier.ValueText: "EngineFunctions" }; } private static INamedTypeSymbol? GetSymbol(GeneratorSyntaxContext context, CancellationToken cancellationToken) where T : BaseTypeDeclarationSyntax { var classDecl = (T)context.Node; return context.SemanticModel.GetDeclaredSymbol(classDecl) as INamedTypeSymbol; } private static bool CouldBeMappedType( SyntaxNode syntaxNode, CancellationToken cancellationToken) { if (syntaxNode is not AttributeSyntax attribute) return false; var name = ExtractName(attribute.Name); return name is "EngineClass" or "EngineClassAttribute"; } private static string? ExtractName(NameSyntax? name) { return name switch { SimpleNameSyntax ins => ins.Identifier.Text, QualifiedNameSyntax qns => qns.Right.Identifier.Text, _ => null }; } private static ITypeSymbol? GetTypeOrNull( GeneratorSyntaxContext context, CancellationToken cancellationToken) { var attributeSyntax = (AttributeSyntax)context.Node; // "attribute.Parent" is "AttributeListSyntax" // "attribute.Parent.Parent" is a C# fragment the attributes are applied to if (attributeSyntax.Parent?.Parent is not BaseTypeDeclarationSyntax typeDeclaration) return null; return context.SemanticModel.GetDeclaredSymbol(typeDeclaration) as ITypeSymbol; } }