From a9892f257153a2d760a5d221dc16e484e1428c71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ignacio=20Rold=C3=A1n=20Etcheverry?= Date: Thu, 28 Jul 2022 17:41:48 +0200 Subject: C#: Add source generator for method list --- modules/mono/csharp_script.cpp | 109 ++--- modules/mono/csharp_script.h | 6 + .../Godot.SourceGenerators.Sample/Methods.cs | 31 ++ .../Godot.SourceGenerators/MarshalUtils.cs | 12 + .../ScriptMemberInvokerGenerator.cs | 463 --------------------- .../ScriptMethodsGenerator.cs | 408 ++++++++++++++++++ .../ScriptPropertiesGenerator.cs | 113 ++++- .../ScriptSignalsGenerator.cs | 51 +++ .../GodotSharp/Core/Bridge/ManagedCallbacks.cs | 2 +- .../GodotSharp/Core/Bridge/ScriptManagerBridge.cs | 75 +++- modules/mono/mono_gd/gd_mono_cache.h | 2 +- 11 files changed, 746 insertions(+), 526 deletions(-) create mode 100644 modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Sample/Methods.cs delete mode 100644 modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptMemberInvokerGenerator.cs create mode 100644 modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptMethodsGenerator.cs diff --git a/modules/mono/csharp_script.cpp b/modules/mono/csharp_script.cpp index 5235c6d176..c69a149d9c 100644 --- a/modules/mono/csharp_script.cpp +++ b/modules/mono/csharp_script.cpp @@ -1618,29 +1618,18 @@ bool CSharpInstance::property_get_revert(const StringName &p_name, Variant &r_re } void CSharpInstance::get_method_list(List *p_list) const { -#warning TODO -#if 0 - if (!script->is_valid() || !script->script_class) { + if (!script->is_valid() || !script->valid) { return; } - GD_MONO_SCOPE_THREAD_ATTACH; - - // TODO: We're filtering out constructors but there may be other methods unsuitable for explicit calls. - GDMonoClass *top = script->script_class; - - while (top && top != script->native) { - const Vector &methods = top->get_all_methods(); - for (int i = 0; i < methods.size(); ++i) { - MethodInfo minfo = methods[i]->get_method_info(); - if (minfo.name != CACHED_STRING_NAME(dotctor)) { - p_list->push_back(minfo); - } + const CSharpScript *top = script.ptr(); + while (top != nullptr) { + for (const CSharpScript::CSharpMethodInfo &E : top->methods) { + p_list->push_back(E.method_info); } - top = top->get_parent_class(); + top = top->base_script.ptr(); } -#endif } bool CSharpInstance::has_method(const StringName &p_method) const { @@ -2183,25 +2172,52 @@ void CSharpScript::reload_registered_script(Ref p_script) { void CSharpScript::update_script_class_info(Ref p_script) { bool tool = false; + // TODO: Use GDNative godot_dictionary + Array methods_array; + methods_array.~Array(); Dictionary rpc_functions_dict; - // Destructor won't be called from C#, and I don't want to include the GDNative header - // only for this, so need to call the destructor manually before passing this to C#. rpc_functions_dict.~Dictionary(); - Dictionary signals_dict; - // Destructor won't be called from C#, and I don't want to include the GDNative header - // only for this, so need to call the destructor manually before passing this to C#. signals_dict.~Dictionary(); Ref base_script; GDMonoCache::managed_callbacks.ScriptManagerBridge_UpdateScriptClassInfo( - p_script.ptr(), &tool, &rpc_functions_dict, &signals_dict, &base_script); + p_script.ptr(), &tool, &methods_array, &rpc_functions_dict, &signals_dict, &base_script); p_script->tool = tool; p_script->rpc_config.clear(); p_script->rpc_config = rpc_functions_dict; + // Methods + + p_script->methods.clear(); + + p_script->methods.resize(methods_array.size()); + int push_index = 0; + + for (int i = 0; i < methods_array.size(); i++) { + Dictionary method_info_dict = methods_array[i]; + + StringName name = method_info_dict["name"]; + + MethodInfo mi; + mi.name = name; + + Array params = method_info_dict["params"]; + + for (int j = 0; j < params.size(); j++) { + Dictionary param = params[j]; + + Variant::Type param_type = (Variant::Type)(int)param["type"]; + PropertyInfo arg_info = PropertyInfo(param_type, (String)param["name"]); + arg_info.usage = (uint32_t)param["usage"]; + mi.arguments.push_back(arg_info); + } + + p_script->methods.set(push_index++, CSharpMethodInfo{ name, mi }); + } + // Event signals // Performance is not critical here as this will be replaced with source generators. @@ -2210,7 +2226,7 @@ void CSharpScript::update_script_class_info(Ref p_script) { // Sigh... can't we just have capacity? p_script->event_signals.resize(signals_dict.size()); - int push_index = 0; + push_index = 0; for (const Variant *s = signals_dict.next(nullptr); s != nullptr; s = signals_dict.next(s)) { StringName name = *s; @@ -2407,28 +2423,27 @@ void CSharpScript::get_script_method_list(List *p_list) const { return; } -#warning TODO -#if 0 - // TODO: We're filtering out constructors but there may be other methods unsuitable for explicit calls. - GDMonoClass *top = script_class; - - while (top && top != native) { - const Vector &methods = top->get_all_methods(); - for (int i = 0; i < methods.size(); ++i) { - MethodInfo minfo = methods[i]->get_method_info(); - if (minfo.name != CACHED_STRING_NAME(dotctor)) { - p_list->push_back(methods[i]->get_method_info()); - } + const CSharpScript *top = this; + while (top != nullptr) { + for (const CSharpMethodInfo &E : top->methods) { + p_list->push_back(E.method_info); } - top = top->get_parent_class(); + top = top->base_script.ptr(); } -#endif } bool CSharpScript::has_method(const StringName &p_method) const { - // The equivalent of this will be implemented once we switch to the GDExtension system - ERR_PRINT_ONCE("CSharpScript::has_method is not implemented"); + if (!valid) { + return false; + } + + for (const CSharpMethodInfo &E : methods) { + if (E.name == p_method) { + return true; + } + } + return false; } @@ -2437,19 +2452,11 @@ MethodInfo CSharpScript::get_method_info(const StringName &p_method) const { return MethodInfo(); } -#warning TODO -#if 0 - GDMonoClass *top = script_class; - - while (top && top != native) { - GDMonoMethod *params = top->get_method_unknown_params(p_method); - if (params) { - return params->get_method_info(); + for (const CSharpMethodInfo &E : methods) { + if (E.name == p_method) { + return E.method_info; } - - top = top->get_parent_class(); } -#endif return MethodInfo(); } diff --git a/modules/mono/csharp_script.h b/modules/mono/csharp_script.h index f3a8ead319..3509a5c87d 100644 --- a/modules/mono/csharp_script.h +++ b/modules/mono/csharp_script.h @@ -103,7 +103,13 @@ class CSharpScript : public Script { MethodInfo method_info; }; + struct CSharpMethodInfo { + StringName name; // MethodInfo stores a string... + MethodInfo method_info; + }; + Vector event_signals; + Vector methods; #ifdef TOOLS_ENABLED List exported_members_cache; // members_cache diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Sample/Methods.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Sample/Methods.cs new file mode 100644 index 0000000000..618ba24abc --- /dev/null +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Sample/Methods.cs @@ -0,0 +1,31 @@ +using System.Diagnostics.CodeAnalysis; + +namespace Godot.SourceGenerators.Sample; + +[SuppressMessage("ReSharper", "RedundantNameQualifier")] +public partial class Methods : Godot.Object +{ + private void MethodWithOverload() + { + } + + private void MethodWithOverload(int a) + { + } + + private void MethodWithOverload(int a, int b) + { + } + + // Should be ignored. The previous one is picked. + // ReSharper disable once UnusedMember.Local + private void MethodWithOverload(float a, float b) + { + } + + // Generic methods should be ignored. + // ReSharper disable once UnusedMember.Local + private void GenericMethod(T t) + { + } +} diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/MarshalUtils.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/MarshalUtils.cs index 9dff404ede..cd4c8ff828 100644 --- a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/MarshalUtils.cs +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/MarshalUtils.cs @@ -400,6 +400,12 @@ namespace Godot.SourceGenerators source.Append(VariantUtils, ".ConvertToQuaternion(", inputExpr, ")"), MarshalType.Transform3D => source.Append(VariantUtils, ".ConvertToTransform3D(", inputExpr, ")"), + MarshalType.Vector4 => + source.Append(VariantUtils, ".ConvertToVector4(", inputExpr, ")"), + MarshalType.Vector4i => + source.Append(VariantUtils, ".ConvertToVector4i(", inputExpr, ")"), + MarshalType.Projection => + source.Append(VariantUtils, ".ConvertToProjection(", inputExpr, ")"), MarshalType.AABB => source.Append(VariantUtils, ".ConvertToAABB(", inputExpr, ")"), MarshalType.Color => @@ -535,6 +541,12 @@ namespace Godot.SourceGenerators source.Append(VariantUtils, ".CreateFromQuaternion(", inputExpr, ")"), MarshalType.Transform3D => source.Append(VariantUtils, ".CreateFromTransform3D(", inputExpr, ")"), + MarshalType.Vector4 => + source.Append(VariantUtils, ".CreateFromVector4(", inputExpr, ")"), + MarshalType.Vector4i => + source.Append(VariantUtils, ".CreateFromVector4i(", inputExpr, ")"), + MarshalType.Projection => + source.Append(VariantUtils, ".CreateFromProjection(", inputExpr, ")"), MarshalType.AABB => source.Append(VariantUtils, ".CreateFromAABB(", inputExpr, ")"), MarshalType.Color => diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptMemberInvokerGenerator.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptMemberInvokerGenerator.cs deleted file mode 100644 index f10942fc24..0000000000 --- a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptMemberInvokerGenerator.cs +++ /dev/null @@ -1,463 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using System.Text; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp.Syntax; -using Microsoft.CodeAnalysis.Text; - -namespace Godot.SourceGenerators -{ - [Generator] - public class ScriptMemberInvokerGenerator : ISourceGenerator - { - public void Execute(GeneratorExecutionContext context) - { - if (context.AreGodotSourceGeneratorsDisabled()) - return; - - INamedTypeSymbol[] godotClasses = context - .Compilation.SyntaxTrees - .SelectMany(tree => - tree.GetRoot().DescendantNodes() - .OfType() - .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(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 - ) - { - 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 uniqueHint = symbol.FullQualifiedName().SanitizeQualifiedNameForUniqueHint() - + "_ScriptMemberInvoker_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.NameWithTypeParameters()); - source.Append("\n{\n"); - - containingType = containingType.ContainingType; - } - } - - source.Append("partial class "); - source.Append(symbol.NameWithTypeParameters()); - source.Append("\n{\n"); - - var members = symbol.GetMembers(); - - var methodSymbols = members - .Where(s => !s.IsStatic && s.Kind == SymbolKind.Method && !s.IsImplicitlyDeclared) - .Cast() - .Where(m => m.MethodKind == MethodKind.Ordinary); - - var propertySymbols = members - .Where(s => !s.IsStatic && s.Kind == SymbolKind.Property) - .Cast(); - - var fieldSymbols = members - .Where(s => !s.IsStatic && s.Kind == SymbolKind.Field && !s.IsImplicitlyDeclared) - .Cast(); - - var godotClassMethods = methodSymbols.WhereHasGodotCompatibleSignature(typeCache).ToArray(); - var godotClassProperties = propertySymbols.WhereIsGodotCompatibleType(typeCache).ToArray(); - var godotClassFields = fieldSymbols.WhereIsGodotCompatibleType(typeCache).ToArray(); - - var signalDelegateSymbols = members - .Where(s => s.Kind == SymbolKind.NamedType) - .Cast() - .Where(namedTypeSymbol => namedTypeSymbol.TypeKind == TypeKind.Delegate) - .Where(s => s.GetAttributes() - .Any(a => a.AttributeClass?.IsGodotSignalAttribute() ?? false)); - - List godotSignalDelegates = new(); - - foreach (var signalDelegateSymbol in signalDelegateSymbols) - { - if (!signalDelegateSymbol.Name.EndsWith(ScriptSignalsGenerator.SignalDelegateSuffix)) - continue; - - string signalName = signalDelegateSymbol.Name; - signalName = signalName.Substring(0, - signalName.Length - ScriptSignalsGenerator.SignalDelegateSuffix.Length); - - var invokeMethodData = signalDelegateSymbol - .DelegateInvokeMethod?.HasGodotCompatibleSignature(typeCache); - - if (invokeMethodData == null) - continue; - - godotSignalDelegates.Add(new(signalName, signalDelegateSymbol, invokeMethodData.Value)); - } - - source.Append(" private partial class GodotInternal {\n"); - - // Generate cached StringNames for methods and properties, for fast lookup - - // TODO: Move the generation of these cached StringNames to its own generator - - 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"); - } - - 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 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"); - } - - // Generate RaiseGodotClassSignalCallbacks - - if (godotSignalDelegates.Count > 0) - { - source.Append( - " protected override void RaiseGodotClassSignalCallbacks(in godot_string_name signal, "); - source.Append("NativeVariantPtrArgs args, int argCount)\n {\n"); - - foreach (var signal in godotSignalDelegates) - { - GenerateSignalEventInvoker(signal, source); - } - - source.Append(" base.RaiseGodotClassSignalCallbacks(signal, args, argCount);\n"); - - source.Append(" }\n"); - } - - // Generate Set/GetGodotClassPropertyValue - - if (godotClassProperties.Length > 0 || godotClassFields.Length > 0) - { - bool isFirstEntry; - - // Setters - - bool allPropertiesAreReadOnly = godotClassFields.All(fi => fi.FieldSymbol.IsReadOnly) && - godotClassProperties.All(pi => pi.PropertySymbol.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.PropertySymbol.IsReadOnly) - continue; - - GeneratePropertySetter(property.PropertySymbol.Name, - property.PropertySymbol.Type, property.Type, source, isFirstEntry); - isFirstEntry = false; - } - - foreach (var field in godotClassFields) - { - if (field.FieldSymbol.IsReadOnly) - continue; - - GeneratePropertySetter(field.FieldSymbol.Name, - field.FieldSymbol.Type, field.Type, 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.PropertySymbol.Name, - property.Type, source, isFirstEntry); - isFirstEntry = false; - } - - foreach (var field in godotClassFields) - { - GeneratePropertyGetter(field.FieldSymbol.Name, - field.Type, source, isFirstEntry); - isFirstEntry = false; - } - - source.Append(" return base.GetGodotClassPropertyValue(name, out value);\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(uniqueHint, SourceText.From(source.ToString(), Encoding.UTF8)); - } - - private static void GenerateMethodInvoker( - GodotMethodData 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(" var callRet = "); - else - source.Append(" "); - - source.Append(methodName); - source.Append("("); - - for (int i = 0; i < method.ParamTypes.Length; i++) - { - if (i != 0) - source.Append(", "); - - source.AppendVariantToManagedExpr(string.Concat("args[", i.ToString(), "]"), - method.ParamTypeSymbols[i], method.ParamTypes[i]); - } - - source.Append(");\n"); - - if (method.RetType != null) - { - source.Append(" ret = "); - - source.AppendManagedToVariantExpr("callRet", method.RetType.Value); - source.Append(";\n"); - - source.Append(" return true;\n"); - } - else - { - source.Append(" ret = default;\n"); - source.Append(" return true;\n"); - } - - source.Append(" }\n"); - } - - private static void GenerateSignalEventInvoker( - GodotSignalDelegateData signal, - StringBuilder source - ) - { - string signalName = signal.Name; - var invokeMethodData = signal.InvokeMethodData; - - source.Append(" if (signal == GodotInternal.SignalName_"); - source.Append(signalName); - source.Append(" && argCount == "); - source.Append(invokeMethodData.ParamTypes.Length); - source.Append(") {\n"); - source.Append(" backing_"); - source.Append(signalName); - source.Append("?.Invoke("); - - for (int i = 0; i < invokeMethodData.ParamTypes.Length; i++) - { - if (i != 0) - source.Append(", "); - - source.AppendVariantToManagedExpr(string.Concat("args[", i.ToString(), "]"), - invokeMethodData.ParamTypeSymbols[i], invokeMethodData.ParamTypes[i]); - } - - source.Append(");\n"); - - source.Append(" return;\n"); - - source.Append(" }\n"); - } - - private static void GeneratePropertySetter( - string propertyMemberName, - ITypeSymbol propertyTypeSymbol, - MarshalType propertyMarshalType, - StringBuilder source, - bool isFirstEntry - ) - { - source.Append(" "); - - if (!isFirstEntry) - source.Append("else "); - - source.Append("if (name == GodotInternal.PropName_") - .Append(propertyMemberName) - .Append(") {\n") - .Append(" ") - .Append(propertyMemberName) - .Append(" = ") - .AppendVariantToManagedExpr("value", propertyTypeSymbol, propertyMarshalType) - .Append(";\n") - .Append(" return true;\n") - .Append(" }\n"); - } - - private static void GeneratePropertyGetter( - string propertyMemberName, - MarshalType propertyMarshalType, - StringBuilder source, - bool isFirstEntry - ) - { - source.Append(" "); - - if (!isFirstEntry) - source.Append("else "); - - source.Append("if (name == GodotInternal.PropName_") - .Append(propertyMemberName) - .Append(") {\n") - .Append(" value = ") - .AppendManagedToVariantExpr(propertyMemberName, propertyMarshalType) - .Append(";\n") - .Append(" return true;\n") - .Append(" }\n"); - } - - private static void GenerateHasMethodEntry( - GodotMethodData 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) - { - } - } -} diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptMethodsGenerator.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptMethodsGenerator.cs new file mode 100644 index 0000000000..8ee9489fe2 --- /dev/null +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptMethodsGenerator.cs @@ -0,0 +1,408 @@ +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Text; + +namespace Godot.SourceGenerators +{ + [Generator] + public class ScriptMethodsGenerator : ISourceGenerator + { + public void Initialize(GeneratorInitializationContext context) + { + } + + public void Execute(GeneratorExecutionContext context) + { + if (context.AreGodotSourceGeneratorsDisabled()) + return; + + INamedTypeSymbol[] godotClasses = context + .Compilation.SyntaxTrees + .SelectMany(tree => + tree.GetRoot().DescendantNodes() + .OfType() + .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(SymbolEqualityComparer.Default) + .ToArray(); + + if (godotClasses.Length > 0) + { + var typeCache = new MarshalUtils.TypeCache(context); + + foreach (var godotClass in godotClasses) + { + VisitGodotScriptClass(context, typeCache, godotClass); + } + } + } + + private class MethodOverloadEqualityComparer : IEqualityComparer + { + public bool Equals(GodotMethodData x, GodotMethodData y) + => x.ParamTypes.Length == y.ParamTypes.Length && x.Method.Name == y.Method.Name; + + public int GetHashCode(GodotMethodData obj) + { + unchecked + { + return (obj.ParamTypes.Length.GetHashCode() * 397) ^ obj.Method.Name.GetHashCode(); + } + } + } + + private static void VisitGodotScriptClass( + GeneratorExecutionContext context, + MarshalUtils.TypeCache typeCache, + INamedTypeSymbol symbol + ) + { + 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 uniqueHint = symbol.FullQualifiedName().SanitizeQualifiedNameForUniqueHint() + + "_ScriptMethods_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.NameWithTypeParameters()); + source.Append("\n{\n"); + + containingType = containingType.ContainingType; + } + } + + source.Append("partial class "); + source.Append(symbol.NameWithTypeParameters()); + source.Append("\n{\n"); + + var members = symbol.GetMembers(); + + var methodSymbols = members + .Where(s => !s.IsStatic && s.Kind == SymbolKind.Method && !s.IsImplicitlyDeclared) + .Cast() + .Where(m => m.MethodKind == MethodKind.Ordinary); + + var godotClassMethods = methodSymbols.WhereHasGodotCompatibleSignature(typeCache) + .Distinct(new MethodOverloadEqualityComparer()) + .ToArray(); + + source.Append(" private partial class GodotInternal {\n"); + + // Generate cached StringNames for methods and properties, for fast lookup + + var distinctMethodNames = godotClassMethods + .Select(m => m.Method.Name) + .Distinct() + .ToArray(); + + foreach (string methodName in distinctMethodNames) + { + source.Append(" public static readonly StringName MethodName_"); + source.Append(methodName); + source.Append(" = \""); + source.Append(methodName); + source.Append("\";\n"); + } + + source.Append(" }\n"); // class GodotInternal + + // Generate GetGodotMethodList + + if (godotClassMethods.Length > 0) + { + source.Append("#pragma warning disable CS0109 // Disable warning about redundant 'new' keyword\n"); + + const string listType = "System.Collections.Generic.List"; + + source.Append(" internal new static ") + .Append(listType) + .Append(" GetGodotMethodList()\n {\n"); + + source.Append(" var methods = new ") + .Append(listType) + .Append("(") + .Append(godotClassMethods.Length) + .Append(");\n"); + + foreach (var method in godotClassMethods) + { + var methodInfo = DetermineMethodInfo(method); + AppendMethodInfo(source, methodInfo); + } + + source.Append(" return methods;\n"); + source.Append(" }\n"); + + source.Append("#pragma warning restore CS0109\n"); + } + + // 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 HasGodotClassMethod + + if (distinctMethodNames.Length > 0) + { + source.Append(" protected override bool HasGodotClassMethod(in godot_string_name method)\n {\n"); + + bool isFirstEntry = true; + foreach (string methodName in distinctMethodNames) + { + GenerateHasMethodEntry(methodName, 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(uniqueHint, SourceText.From(source.ToString(), Encoding.UTF8)); + } + + private static void AppendMethodInfo(StringBuilder source, MethodInfo methodInfo) + { + source.Append(" methods.Add(new(name: GodotInternal.MethodName_") + .Append(methodInfo.Name) + .Append(", returnVal: "); + + AppendPropertyInfo(source, methodInfo.ReturnVal); + + source.Append(", flags: (Godot.MethodFlags)") + .Append((int)methodInfo.Flags) + .Append(", arguments: "); + + if (methodInfo.Arguments is { Count: > 0 }) + { + source.Append("new() { "); + + foreach (var param in methodInfo.Arguments) + { + AppendPropertyInfo(source, param); + + // C# allows colon after the last element + source.Append(", "); + } + + source.Append(" }"); + } + else + { + source.Append("null"); + } + + source.Append(", defaultArguments: null));\n"); + } + + private static void AppendPropertyInfo(StringBuilder source, PropertyInfo propertyInfo) + { + source.Append("new(type: (Godot.Variant.Type)") + .Append((int)propertyInfo.Type) + .Append(", name: \"") + .Append(propertyInfo.Name) + .Append("\", hint: (Godot.PropertyHint)") + .Append((int)propertyInfo.Hint) + .Append(", hintString: \"") + .Append(propertyInfo.HintString) + .Append("\", usage: (Godot.PropertyUsageFlags)") + .Append((int)propertyInfo.Usage) + .Append(", exported: ") + .Append(propertyInfo.Exported ? "true" : "false") + .Append(")"); + } + + private static MethodInfo DetermineMethodInfo(GodotMethodData method) + { + PropertyInfo returnVal; + + if (method.RetType != null) + { + returnVal = DeterminePropertyInfo(method.RetType.Value, name: string.Empty); + } + else + { + returnVal = new PropertyInfo(VariantType.Nil, string.Empty, PropertyHint.None, + hintString: null, PropertyUsageFlags.Default, exported: false); + } + + int paramCount = method.ParamTypes.Length; + + List? arguments; + + if (paramCount > 0) + { + arguments = new(capacity: paramCount); + + for (int i = 0; i < paramCount; i++) + { + arguments.Add(DeterminePropertyInfo(method.ParamTypes[i], + name: method.Method.Parameters[i].Name)); + } + } + else + { + arguments = null; + } + + return new MethodInfo(method.Method.Name, returnVal, MethodFlags.Default, arguments, + defaultArguments: null); + } + + private static PropertyInfo DeterminePropertyInfo(MarshalType marshalType, string name) + { + var memberVariantType = MarshalUtils.ConvertMarshalTypeToVariantType(marshalType)!.Value; + + var propUsage = PropertyUsageFlags.Default; + + if (memberVariantType == VariantType.Nil) + propUsage |= PropertyUsageFlags.NilIsVariant; + + return new PropertyInfo(memberVariantType, name, + PropertyHint.None, string.Empty, propUsage, exported: false); + } + + private static void GenerateHasMethodEntry( + string methodName, + StringBuilder source, + bool isFirstEntry + ) + { + source.Append(" "); + if (!isFirstEntry) + source.Append("else "); + source.Append("if (method == GodotInternal.MethodName_"); + source.Append(methodName); + source.Append(") {\n return true;\n }\n"); + } + + private static void GenerateMethodInvoker( + GodotMethodData 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(" var callRet = "); + else + source.Append(" "); + + source.Append(methodName); + source.Append("("); + + for (int i = 0; i < method.ParamTypes.Length; i++) + { + if (i != 0) + source.Append(", "); + + source.AppendVariantToManagedExpr(string.Concat("args[", i.ToString(), "]"), + method.ParamTypeSymbols[i], method.ParamTypes[i]); + } + + source.Append(");\n"); + + if (method.RetType != null) + { + source.Append(" ret = "); + + source.AppendManagedToVariantExpr("callRet", method.RetType.Value); + source.Append(";\n"); + + source.Append(" return true;\n"); + } + else + { + source.Append(" ret = default;\n"); + source.Append(" return true;\n"); + } + + source.Append(" }\n"); + } + } +} diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptPropertiesGenerator.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptPropertiesGenerator.cs index c527f738b3..29a15e2d16 100644 --- a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptPropertiesGenerator.cs +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptPropertiesGenerator.cs @@ -146,10 +146,72 @@ namespace Godot.SourceGenerators source.Append(" }\n"); // class GodotInternal - // Generate GetGodotPropertyList - if (godotClassProperties.Length > 0 || godotClassFields.Length > 0) { + bool isFirstEntry; + + // Generate SetGodotClassPropertyValue + + bool allPropertiesAreReadOnly = godotClassFields.All(fi => fi.FieldSymbol.IsReadOnly) && + godotClassProperties.All(pi => pi.PropertySymbol.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.PropertySymbol.IsReadOnly) + continue; + + GeneratePropertySetter(property.PropertySymbol.Name, + property.PropertySymbol.Type, property.Type, source, isFirstEntry); + isFirstEntry = false; + } + + foreach (var field in godotClassFields) + { + if (field.FieldSymbol.IsReadOnly) + continue; + + GeneratePropertySetter(field.FieldSymbol.Name, + field.FieldSymbol.Type, field.Type, source, isFirstEntry); + isFirstEntry = false; + } + + source.Append(" return base.SetGodotClassPropertyValue(name, value);\n"); + + source.Append(" }\n"); + } + + // Generate GetGodotClassPropertyValue + + 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.PropertySymbol.Name, + property.Type, source, isFirstEntry); + isFirstEntry = false; + } + + foreach (var field in godotClassFields) + { + GeneratePropertyGetter(field.FieldSymbol.Name, + field.Type, source, isFirstEntry); + isFirstEntry = false; + } + + source.Append(" return base.GetGodotClassPropertyValue(name, out value);\n"); + + source.Append(" }\n"); + + // Generate GetGodotPropertyList + source.Append("#pragma warning disable CS0109 // Disable warning about redundant 'new' keyword\n"); string dictionaryType = "System.Collections.Generic.List"; @@ -212,6 +274,53 @@ namespace Godot.SourceGenerators context.AddSource(uniqueHint, SourceText.From(source.ToString(), Encoding.UTF8)); } + private static void GeneratePropertySetter( + string propertyMemberName, + ITypeSymbol propertyTypeSymbol, + MarshalType propertyMarshalType, + StringBuilder source, + bool isFirstEntry + ) + { + source.Append(" "); + + if (!isFirstEntry) + source.Append("else "); + + source.Append("if (name == GodotInternal.PropName_") + .Append(propertyMemberName) + .Append(") {\n") + .Append(" ") + .Append(propertyMemberName) + .Append(" = ") + .AppendVariantToManagedExpr("value", propertyTypeSymbol, propertyMarshalType) + .Append(";\n") + .Append(" return true;\n") + .Append(" }\n"); + } + + private static void GeneratePropertyGetter( + string propertyMemberName, + MarshalType propertyMarshalType, + StringBuilder source, + bool isFirstEntry + ) + { + source.Append(" "); + + if (!isFirstEntry) + source.Append("else "); + + source.Append("if (name == GodotInternal.PropName_") + .Append(propertyMemberName) + .Append(") {\n") + .Append(" value = ") + .AppendManagedToVariantExpr(propertyMemberName, propertyMarshalType) + .Append(";\n") + .Append(" return true;\n") + .Append(" }\n"); + } + private static void AppendPropertyInfo(StringBuilder source, PropertyInfo propertyInfo) { source.Append(" properties.Add(new(type: (Godot.Variant.Type)") diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptSignalsGenerator.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptSignalsGenerator.cs index 31cc8e220b..10f4ddd149 100644 --- a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptSignalsGenerator.cs +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptSignalsGenerator.cs @@ -232,6 +232,24 @@ namespace Godot.SourceGenerators .Append("}\n"); } + // Generate RaiseGodotClassSignalCallbacks + + if (godotSignalDelegates.Count > 0) + { + source.Append( + " protected override void RaiseGodotClassSignalCallbacks(in godot_string_name signal, "); + source.Append("NativeVariantPtrArgs args, int argCount)\n {\n"); + + foreach (var signal in godotSignalDelegates) + { + GenerateSignalEventInvoker(signal, source); + } + + source.Append(" base.RaiseGodotClassSignalCallbacks(signal, args, argCount);\n"); + + source.Append(" }\n"); + } + source.Append("}\n"); // partial class if (isInnerClass) @@ -356,5 +374,38 @@ namespace Godot.SourceGenerators return new PropertyInfo(memberVariantType, name, PropertyHint.None, string.Empty, propUsage, exported: false); } + + private static void GenerateSignalEventInvoker( + GodotSignalDelegateData signal, + StringBuilder source + ) + { + string signalName = signal.Name; + var invokeMethodData = signal.InvokeMethodData; + + source.Append(" if (signal == GodotInternal.SignalName_"); + source.Append(signalName); + source.Append(" && argCount == "); + source.Append(invokeMethodData.ParamTypes.Length); + source.Append(") {\n"); + source.Append(" backing_"); + source.Append(signalName); + source.Append("?.Invoke("); + + for (int i = 0; i < invokeMethodData.ParamTypes.Length; i++) + { + if (i != 0) + source.Append(", "); + + source.AppendVariantToManagedExpr(string.Concat("args[", i.ToString(), "]"), + invokeMethodData.ParamTypeSymbols[i], invokeMethodData.ParamTypes[i]); + } + + source.Append(");\n"); + + source.Append(" return;\n"); + + source.Append(" }\n"); + } } } diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/ManagedCallbacks.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/ManagedCallbacks.cs index 5d3f140cba..57240624bc 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/ManagedCallbacks.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/ManagedCallbacks.cs @@ -24,7 +24,7 @@ namespace Godot.Bridge public delegate* unmanaged ScriptManagerBridge_GetOrCreateScriptBridgeForPath; public delegate* unmanaged ScriptManagerBridge_RemoveScriptBridge; public delegate* unmanaged ScriptManagerBridge_TryReloadRegisteredScriptWithClass; - public delegate* unmanaged ScriptManagerBridge_UpdateScriptClassInfo; + public delegate* unmanaged ScriptManagerBridge_UpdateScriptClassInfo; public delegate* unmanaged ScriptManagerBridge_SwapGCHandleForType; public delegate* unmanaged, void> ScriptManagerBridge_GetPropertyInfoList; public delegate* unmanaged, void> ScriptManagerBridge_GetPropertyDefaultValues; diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/ScriptManagerBridge.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/ScriptManagerBridge.cs index 4b9f851925..03094cbe81 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/ScriptManagerBridge.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/ScriptManagerBridge.cs @@ -557,7 +557,8 @@ namespace Godot.Bridge [UnmanagedCallersOnly] internal static unsafe void UpdateScriptClassInfo(IntPtr scriptPtr, godot_bool* outTool, - godot_dictionary* outRpcFunctionsDest, godot_dictionary* outEventSignalsDest, godot_ref* outBaseScript) + godot_array* outMethodsDest, godot_dictionary* outRpcFunctionsDest, + godot_dictionary* outEventSignalsDest, godot_ref* outBaseScript) { try { @@ -578,13 +579,59 @@ namespace Godot.Bridge if (!(*outTool).ToBool() && scriptType.Assembly.GetName().Name == "GodotTools") *outTool = godot_bool.True; - // RPC functions + // Methods - Collections.Dictionary rpcFunctions = new(); + // Performance is not critical here as this will be replaced with source generators. + using var methods = new Collections.Array(); Type? top = scriptType; Type native = Object.InternalGetClassNativeBase(top); + while (top != null && top != native) + { + var methodList = GetMethodListForType(top); + + if (methodList != null) + { + foreach (var method in methodList) + { + var methodInfo = new Collections.Dictionary(); + + methodInfo.Add("name", method.Name); + + var methodParams = new Collections.Array(); + + if (method.Arguments != null) + { + foreach (var param in method.Arguments) + { + methodParams.Add(new Collections.Dictionary() + { + { "name", param.Name }, + { "type", param.Type }, + { "usage", param.Usage } + }); + } + } + + methodInfo.Add("params", methodParams); + + methods.Add(methodInfo); + } + } + + top = top.BaseType; + } + + *outMethodsDest = NativeFuncs.godotsharp_array_new_copy( + (godot_array)methods.NativeValue); + + // RPC functions + + Collections.Dictionary rpcFunctions = new(); + + top = scriptType; + while (top != null && top != native) { foreach (var method in top.GetMethods(BindingFlags.DeclaredOnly | BindingFlags.Instance | @@ -617,9 +664,8 @@ namespace Godot.Bridge top = top.BaseType; } - *outRpcFunctionsDest = - NativeFuncs.godotsharp_dictionary_new_copy( - (godot_dictionary)((Collections.Dictionary)rpcFunctions).NativeValue); + *outRpcFunctionsDest = NativeFuncs.godotsharp_dictionary_new_copy( + (godot_dictionary)((Collections.Dictionary)rpcFunctions).NativeValue); // Event signals @@ -663,8 +709,8 @@ namespace Godot.Bridge top = top.BaseType; } - *outEventSignalsDest = - NativeFuncs.godotsharp_dictionary_new_copy((godot_dictionary)signals.NativeValue); + *outEventSignalsDest = NativeFuncs.godotsharp_dictionary_new_copy( + (godot_dictionary)signals.NativeValue); // Base script @@ -701,6 +747,19 @@ namespace Godot.Bridge return (List?)getGodotSignalListMethod.Invoke(null, null); } + private static List? GetMethodListForType(Type type) + { + var getGodotMethodListMethod = type.GetMethod( + "GetGodotMethodList", + BindingFlags.DeclaredOnly | BindingFlags.Static | + BindingFlags.NonPublic | BindingFlags.Public); + + if (getGodotMethodListMethod == null) + return null; + + return (List?)getGodotMethodListMethod.Invoke(null, null); + } + // ReSharper disable once InconsistentNaming [SuppressMessage("ReSharper", "NotAccessedField.Local")] [StructLayout(LayoutKind.Sequential)] diff --git a/modules/mono/mono_gd/gd_mono_cache.h b/modules/mono/mono_gd/gd_mono_cache.h index 3c58c251b7..ca3a6c95a7 100644 --- a/modules/mono/mono_gd/gd_mono_cache.h +++ b/modules/mono/mono_gd/gd_mono_cache.h @@ -87,7 +87,7 @@ struct ManagedCallbacks { using FuncScriptManagerBridge_GetOrCreateScriptBridgeForPath = void(GD_CLR_STDCALL *)(const String *, Ref *); using FuncScriptManagerBridge_RemoveScriptBridge = void(GD_CLR_STDCALL *)(const CSharpScript *); using FuncScriptManagerBridge_TryReloadRegisteredScriptWithClass = bool(GD_CLR_STDCALL *)(const CSharpScript *); - using FuncScriptManagerBridge_UpdateScriptClassInfo = void(GD_CLR_STDCALL *)(const CSharpScript *, bool *, Dictionary *, Dictionary *, Ref *); + using FuncScriptManagerBridge_UpdateScriptClassInfo = void(GD_CLR_STDCALL *)(const CSharpScript *, bool *, Array *, Dictionary *, Dictionary *, Ref *); using FuncScriptManagerBridge_SwapGCHandleForType = bool(GD_CLR_STDCALL *)(GCHandleIntPtr, GCHandleIntPtr *, bool); using FuncScriptManagerBridge_GetPropertyInfoList = void(GD_CLR_STDCALL *)(CSharpScript *, Callback_ScriptManagerBridge_GetPropertyInfoList_Add); using FuncScriptManagerBridge_GetPropertyDefaultValues = void(GD_CLR_STDCALL *)(CSharpScript *, Callback_ScriptManagerBridge_GetPropertyDefaultValues_Add); -- cgit v1.2.3