virtualx-engine/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptBoilerplateGenerator.cs
Ignacio Roldán Etcheverry 4d710bf659 C#: Add initial implementation of source generator for script members
This replaces the way we invoke methods and set/get properties.
This first iteration rids us of runtime type checking in those
cases, as it's now done at compile time.
Later it will also stop needing the use of reflection. After that,
we will only depend on reflection for generic Godot Array and
Dictionary. We're stuck with reflection in generic collections
for now as C# doesn't support generic/template specialization.

This is only the initial implementation. Further iterations are
coming, specially once we switch to the native extension system
which completely changes the way members are accessed/invoked.
For example, with the native extension system we will likely need
to create `UnmanagedCallersOnly` invoke wrapper methods and return
function pointers to the engine.

Other kind of members, like event signals will be receiving the
same treatment in the future.
2022-08-22 03:36:51 +02:00

423 lines
15 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;
// False positive for RS1024. We're already using `SymbolEqualityComparer.Default`...
#pragma warning disable RS1024
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())
return true;
Common.ReportNonPartialGodotScriptClass(context, x.cds, x.symbol);
return false;
})
.Select(x => x.symbol)
)
.Distinct<INamedTypeSymbol>(SymbolEqualityComparer.Default)
.ToArray();
#pragma warning restore RS1024
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;
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");
}
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 methods = WhereHasCompatibleGodotType(methodSymbols, typeCache).ToArray();
var properties = WhereIsCompatibleGodotType(propertySymbols, typeCache).ToArray();
var fields = WhereIsCompatibleGodotType(fieldSymbols, typeCache).ToArray();
source.Append(" private class GodotInternal {\n");
// Generate cached StringNames for methods and properties, for fast lookup
foreach (var method in methods)
{
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 properties)
{
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 fields)
{
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");
if (methods.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 methods)
{
GenerateMethodInvoker(method, source);
}
source.Append(" return base.InvokeGodotClassMethod(method, args, argCount, out ret);\n");
source.Append(" }\n");
}
if (properties.Length > 0 || fields.Length > 0)
{
// Setters
source.Append(" protected override bool SetGodotClassPropertyValue(in godot_string_name name, ");
source.Append("in godot_variant value)\n {\n");
foreach (var property in properties)
{
GeneratePropertySetter(property.Property.Name,
property.Property.Type.FullQualifiedName(), source);
}
foreach (var field in fields)
{
GeneratePropertySetter(field.Field.Name,
field.Field.Type.FullQualifiedName(), source);
}
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");
foreach (var property in properties)
{
GeneratePropertyGetter(property.Property.Name, source);
}
foreach (var field in fields)
{
GeneratePropertyGetter(field.Field.Name, source);
}
source.Append(" return base.GetGodotClassPropertyValue(name, out value);\n");
source.Append(" }\n");
}
source.Append("}\n");
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
)
{
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
)
{
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");
}
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.Select(p =>
MarshalUtils.ConvertManagedTypeToVariantType(p.Type, typeCache))
.Where(t => t != null).Cast<MarshalType>().ToImmutableArray();
if (parameters.Length > paramTypes.Length)
continue; // Some param types weren't compatible
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)
{
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);
}
}
}
}