virtualx-engine/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptBoilerplateGenerator.cs
Ignacio Roldán Etcheverry 88e367a406 C#/netcore: Add base desktop game export implementation
This base implementation is still very barebones but it defines the path
for how exporting will work (at least when embedding the .NET runtime).

Many manual steps are still needed, which should be automatized in the
future. For example, in addition to the API assemblies, now you also
need to copy the GodotPlugins assembly to each game project.
2022-08-22 03:36:51 +02:00

534 lines
19 KiB
C#

using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Text;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Text;
namespace Godot.SourceGenerators
{
[Generator]
public class ScriptBoilerplateGenerator : ISourceGenerator
{
public void Execute(GeneratorExecutionContext context)
{
if (context.AreGodotSourceGeneratorsDisabled())
return;
INamedTypeSymbol[] godotClasses = context
.Compilation.SyntaxTrees
.SelectMany(tree =>
tree.GetRoot().DescendantNodes()
.OfType<ClassDeclarationSyntax>()
.SelectGodotScriptClasses(context.Compilation)
// Report and skip non-partial classes
.Where(x =>
{
if (x.cds.IsPartial())
{
if (x.cds.IsNested() && !x.cds.AreAllOuterTypesPartial(out var typeMissingPartial))
{
Common.ReportNonPartialGodotScriptOuterClass(context, typeMissingPartial!);
return false;
}
return true;
}
Common.ReportNonPartialGodotScriptClass(context, x.cds, x.symbol);
return false;
})
.Select(x => x.symbol)
)
.Distinct<INamedTypeSymbol>(SymbolEqualityComparer.Default)
.ToArray();
if (godotClasses.Length > 0)
{
var typeCache = new MarshalUtils.TypeCache(context);
foreach (var godotClass in godotClasses)
{
VisitGodotScriptClass(context, typeCache, godotClass);
}
}
}
private static void VisitGodotScriptClass(
GeneratorExecutionContext context,
MarshalUtils.TypeCache typeCache,
INamedTypeSymbol symbol
)
{
string className = symbol.Name;
INamespaceSymbol namespaceSymbol = symbol.ContainingNamespace;
string classNs = namespaceSymbol != null && !namespaceSymbol.IsGlobalNamespace ?
namespaceSymbol.FullQualifiedName() :
string.Empty;
bool hasNamespace = classNs.Length != 0;
bool isInnerClass = symbol.ContainingType != null;
string uniqueName = hasNamespace ?
classNs + "." + className + "_ScriptBoilerplate_Generated" :
className + "_ScriptBoilerplate_Generated";
var source = new StringBuilder();
source.Append("using Godot;\n");
source.Append("using Godot.NativeInterop;\n");
source.Append("\n");
if (hasNamespace)
{
source.Append("namespace ");
source.Append(classNs);
source.Append(" {\n\n");
}
if (isInnerClass)
{
var containingType = symbol.ContainingType;
while (containingType != null)
{
source.Append("partial ");
source.Append(containingType.GetDeclarationKeyword());
source.Append(" ");
source.Append(containingType.Name);
source.Append("\n{\n");
containingType = containingType.ContainingType;
}
}
source.Append("partial class ");
source.Append(className);
source.Append("\n{\n");
var members = symbol.GetMembers();
// TODO: Static static marshaling (no reflection, no runtime type checks)
var methodSymbols = members
.Where(s => s.Kind == SymbolKind.Method)
.Cast<IMethodSymbol>()
.Where(m => m.MethodKind == MethodKind.Ordinary && !m.IsImplicitlyDeclared);
var propertySymbols = members
.Where(s => s.Kind == SymbolKind.Property)
.Cast<IPropertySymbol>();
var fieldSymbols = members
.Where(s => s.Kind == SymbolKind.Field)
.Cast<IFieldSymbol>()
.Where(p => !p.IsImplicitlyDeclared);
var godotClassMethods = WhereHasCompatibleGodotType(methodSymbols, typeCache).ToArray();
var godotClassProperties = WhereIsCompatibleGodotType(propertySymbols, typeCache).ToArray();
var godotClassFields = WhereIsCompatibleGodotType(fieldSymbols, typeCache).ToArray();
source.Append(" private class GodotInternal {\n");
// Generate cached StringNames for methods and properties, for fast lookup
foreach (var method in godotClassMethods)
{
string methodName = method.Method.Name;
source.Append(" public static readonly StringName MethodName_");
source.Append(methodName);
source.Append(" = \"");
source.Append(methodName);
source.Append("\";\n");
}
foreach (var property in godotClassProperties)
{
string propertyName = property.Property.Name;
source.Append(" public static readonly StringName PropName_");
source.Append(propertyName);
source.Append(" = \"");
source.Append(propertyName);
source.Append("\";\n");
}
foreach (var field in godotClassFields)
{
string fieldName = field.Field.Name;
source.Append(" public static readonly StringName PropName_");
source.Append(fieldName);
source.Append(" = \"");
source.Append(fieldName);
source.Append("\";\n");
}
source.Append(" }\n"); // class GodotInternal
// Generate InvokeGodotClassMethod
if (godotClassMethods.Length > 0)
{
source.Append(" protected override bool InvokeGodotClassMethod(in godot_string_name method, ");
source.Append("NativeVariantPtrArgs args, int argCount, out godot_variant ret)\n {\n");
foreach (var method in godotClassMethods)
{
GenerateMethodInvoker(method, source);
}
source.Append(" return base.InvokeGodotClassMethod(method, args, argCount, out ret);\n");
source.Append(" }\n");
}
// Generate Set/GetGodotClassPropertyValue
if (godotClassProperties.Length > 0 || godotClassFields.Length > 0)
{
bool isFirstEntry;
// Setters
bool allPropertiesAreReadOnly = godotClassFields.All(fi => fi.Field.IsReadOnly) &&
godotClassProperties.All(pi => pi.Property.IsReadOnly);
if (!allPropertiesAreReadOnly)
{
source.Append(" protected override bool SetGodotClassPropertyValue(in godot_string_name name, ");
source.Append("in godot_variant value)\n {\n");
isFirstEntry = true;
foreach (var property in godotClassProperties)
{
if (property.Property.IsReadOnly)
continue;
GeneratePropertySetter(property.Property.Name,
property.Property.Type.FullQualifiedName(), source, isFirstEntry);
isFirstEntry = false;
}
foreach (var field in godotClassFields)
{
if (field.Field.IsReadOnly)
continue;
GeneratePropertySetter(field.Field.Name,
field.Field.Type.FullQualifiedName(), source, isFirstEntry);
isFirstEntry = false;
}
source.Append(" return base.SetGodotClassPropertyValue(name, value);\n");
source.Append(" }\n");
}
// Getters
source.Append(" protected override bool GetGodotClassPropertyValue(in godot_string_name name, ");
source.Append("out godot_variant value)\n {\n");
isFirstEntry = true;
foreach (var property in godotClassProperties)
{
GeneratePropertyGetter(property.Property.Name, source, isFirstEntry);
isFirstEntry = false;
}
foreach (var field in godotClassFields)
{
GeneratePropertyGetter(field.Field.Name, source, isFirstEntry);
isFirstEntry = false;
}
source.Append(" return base.GetGodotClassPropertyValue(name, out value);\n");
source.Append(" }\n");
}
// Generate HasGodotClassMethod
if (godotClassMethods.Length > 0)
{
source.Append(" protected override bool HasGodotClassMethod(in godot_string_name method)\n {\n");
bool isFirstEntry = true;
foreach (var method in godotClassMethods)
{
GenerateHasMethodEntry(method, source, isFirstEntry);
isFirstEntry = false;
}
source.Append(" return base.HasGodotClassMethod(method);\n");
source.Append(" }\n");
}
source.Append("}\n"); // partial class
if (isInnerClass)
{
var containingType = symbol.ContainingType;
while (containingType != null)
{
source.Append("}\n"); // outer class
containingType = containingType.ContainingType;
}
}
if (hasNamespace)
{
source.Append("\n}\n");
}
context.AddSource(uniqueName, SourceText.From(source.ToString(), Encoding.UTF8));
}
private static void GenerateMethodInvoker(
GodotMethodInfo method,
StringBuilder source
)
{
string methodName = method.Method.Name;
source.Append(" if (method == GodotInternal.MethodName_");
source.Append(methodName);
source.Append(" && argCount == ");
source.Append(method.ParamTypes.Length);
source.Append(") {\n");
if (method.RetType != null)
source.Append(" object retBoxed = ");
else
source.Append(" ");
source.Append(methodName);
source.Append("(");
for (int i = 0; i < method.ParamTypes.Length; i++)
{
if (i != 0)
source.Append(", ");
// TODO: static marshaling (no reflection, no runtime type checks)
string paramTypeQualifiedName = method.ParamTypeSymbols[i].FullQualifiedName();
source.Append("(");
source.Append(paramTypeQualifiedName);
source.Append(")Marshaling.ConvertVariantToManagedObjectOfType(args[");
source.Append(i);
source.Append("], typeof(");
source.Append(paramTypeQualifiedName);
source.Append("))");
}
source.Append(");\n");
if (method.RetType != null)
{
// TODO: static marshaling (no reflection, no runtime type checks)
source.Append(" ret = Marshaling.ConvertManagedObjectToVariant(retBoxed);\n");
source.Append(" return true;\n");
}
else
{
source.Append(" ret = default;\n");
source.Append(" return true;\n");
}
source.Append(" }\n");
}
private static void GeneratePropertySetter(
string propertyMemberName,
string propertyTypeQualifiedName,
StringBuilder source,
bool isFirstEntry
)
{
source.Append(" ");
if (!isFirstEntry)
source.Append("else ");
source.Append("if (name == GodotInternal.PropName_");
source.Append(propertyMemberName);
source.Append(") {\n");
source.Append(" ");
source.Append(propertyMemberName);
source.Append(" = ");
// TODO: static marshaling (no reflection, no runtime type checks)
source.Append("(");
source.Append(propertyTypeQualifiedName);
source.Append(")Marshaling.ConvertVariantToManagedObjectOfType(value, typeof(");
source.Append(propertyTypeQualifiedName);
source.Append("));\n");
source.Append(" return true;\n");
source.Append(" }\n");
}
private static void GeneratePropertyGetter(
string propertyMemberName,
StringBuilder source,
bool isFirstEntry
)
{
source.Append(" ");
if (!isFirstEntry)
source.Append("else ");
source.Append("if (name == GodotInternal.PropName_");
source.Append(propertyMemberName);
source.Append(") {\n");
// TODO: static marshaling (no reflection, no runtime type checks)
source.Append(" value = Marshaling.ConvertManagedObjectToVariant(");
source.Append(propertyMemberName);
source.Append(");\n");
source.Append(" return true;\n");
source.Append(" }\n");
}
private static void GenerateHasMethodEntry(
GodotMethodInfo method,
StringBuilder source,
bool isFirstEntry
)
{
string methodName = method.Method.Name;
source.Append(" ");
if (!isFirstEntry)
source.Append("else ");
source.Append("if (method == GodotInternal.MethodName_");
source.Append(methodName);
source.Append(") {\n return true;\n }\n");
}
public void Initialize(GeneratorInitializationContext context)
{
}
private struct GodotMethodInfo
{
public GodotMethodInfo(IMethodSymbol method, ImmutableArray<MarshalType> paramTypes,
ImmutableArray<ITypeSymbol> paramTypeSymbols, MarshalType? retType)
{
Method = method;
ParamTypes = paramTypes;
ParamTypeSymbols = paramTypeSymbols;
RetType = retType;
}
public IMethodSymbol Method { get; }
public ImmutableArray<MarshalType> ParamTypes { get; }
public ImmutableArray<ITypeSymbol> ParamTypeSymbols { get; }
public MarshalType? RetType { get; }
}
private struct GodotPropertyInfo
{
public GodotPropertyInfo(IPropertySymbol property, MarshalType type)
{
Property = property;
Type = type;
}
public IPropertySymbol Property { get; }
public MarshalType Type { get; }
}
private struct GodotFieldInfo
{
public GodotFieldInfo(IFieldSymbol field, MarshalType type)
{
Field = field;
Type = type;
}
public IFieldSymbol Field { get; }
public MarshalType Type { get; }
}
private static IEnumerable<GodotMethodInfo> WhereHasCompatibleGodotType(
IEnumerable<IMethodSymbol> methods,
MarshalUtils.TypeCache typeCache
)
{
foreach (var method in methods)
{
if (method.IsGenericMethod)
continue;
var retType = method.ReturnsVoid ?
null :
MarshalUtils.ConvertManagedTypeToVariantType(method.ReturnType, typeCache);
if (retType == null && !method.ReturnsVoid)
continue;
var parameters = method.Parameters;
var paramTypes = parameters
// Currently we don't support `ref`, `out`, `in`, `ref readonly` parameters (and we never may)
.Where(p => p.RefKind == RefKind.None)
// Attempt to determine the variant type
.Select(p => MarshalUtils.ConvertManagedTypeToVariantType(p.Type, typeCache))
// Discard parameter types that couldn't be determined (null entries)
.Where(t => t != null).Cast<MarshalType>().ToImmutableArray();
// If any parameter type was incompatible, it was discarded so the length won't match
if (parameters.Length > paramTypes.Length)
continue; // Ignore incompatible method
yield return new GodotMethodInfo(method, paramTypes, parameters
.Select(p => p.Type).ToImmutableArray(), retType);
}
}
private static IEnumerable<GodotPropertyInfo> WhereIsCompatibleGodotType(
IEnumerable<IPropertySymbol> properties,
MarshalUtils.TypeCache typeCache
)
{
foreach (var property in properties)
{
// Ignore properties without a getter. Godot properties must be readable.
if (property.IsWriteOnly)
continue;
var marshalType = MarshalUtils.ConvertManagedTypeToVariantType(property.Type, typeCache);
if (marshalType == null)
continue;
yield return new GodotPropertyInfo(property, marshalType.Value);
}
}
private static IEnumerable<GodotFieldInfo> WhereIsCompatibleGodotType(
IEnumerable<IFieldSymbol> fields,
MarshalUtils.TypeCache typeCache
)
{
foreach (var field in fields)
{
var marshalType = MarshalUtils.ConvertManagedTypeToVariantType(field.Type, typeCache);
if (marshalType == null)
continue;
yield return new GodotFieldInfo(field, marshalType.Value);
}
}
}
}