From 282bd37e5c9dbc1bba4fe349fc7868643a54164b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ignacio=20Rold=C3=A1n=20Etcheverry?= Date: Fri, 28 Oct 2022 22:59:13 +0200 Subject: C#: Remove need for reflection to invoking callable delegates We aim to make the C# API reflection-free, mainly for concerns about performance, and to be able to target NativeAOT in refletion-free mode, which reduces the binary size. One of the main usages of reflection still left was the dynamic invokation of callable delegates, and for some time I wasn't sure I would find an alternative solution that I'd be happy with. The new solution uses trampoline functions to invoke the delegates: ``` static void Trampoline(object delegateObj, NativeVariantPtrArgs args, out godot_variant ret) { if (args.Count != 1) throw new ArgumentException($"Callable expected 1 arguments but received {args.Count}."); string res = ((Func)delegateObj)( VariantConversionCallbacks.GetToManagedCallback()(args[0]) ); ret = VariantConversionCallbacks.GetToVariantCallback()(res); } Callable.CreateWithUnsafeTrampoline((int num) => "Foo" + num, &Trampoline); ``` Of course, this is too much boilerplate for user code. To improve this, the `Callable.From` methods were added. These are overloads that take `Action` and `Func` delegates, which covers the most common use cases: lambdas and method groups: ``` // Lambda Callable.From((int num) => "Foo" + num); // Method group string AppendNum(int num) => "Foo" + num; Callable.From(AppendNum); ``` Unfortunately, due to limitations in the C# language, implicit conversions from delegates to `Callable` are not supported. `Callable.From` does not support custom delegates. These should be uncommon, but the Godot C# API actually uses them for event signals. As such, the bindings generator was updated to generate trampoline functions for event signals. It was also optimized to use `Action` instead of a custom delegate for parameterless signals, which removes the need for the trampoline functions for those signals. The change to reflection-free invokation removes one of the last needs for `ConvertVariantToManagedObjectOfType`. The only remaining usage is from calling script constructors with parameters from the engine (`CreateManagedForGodotObjectScriptInstance`). Once that one is made reflection-free, `ConvertVariantToManagedObjectOfType` can be removed. --- .../ScriptMethodsGenerator.cs | 6 +- .../ScriptSignalsGenerator.cs | 7 +- modules/mono/editor/bindings_generator.cpp | 89 +++- .../GodotSharp/Core/Bridge/CSharpInstanceBridge.cs | 5 +- .../GodotSharp/Core/Bridge/ManagedCallbacks.cs | 2 +- .../GodotSharp/Core/Bridge/ScriptManagerBridge.cs | 2 +- .../glue/GodotSharp/GodotSharp/Core/Callable.cs | 74 +++- .../GodotSharp/Core/Callable.generics.cs | 480 +++++++++++++++++++++ .../GodotSharp/GodotSharp/Core/DelegateUtils.cs | 28 +- .../GodotSharp/Core/NativeInterop/Marshaling.cs | 38 +- .../GodotSharp/Core/NativeInterop/NativeFuncs.cs | 6 +- .../Core/NativeInterop/NativeVariantPtrArgs.cs | 16 +- .../NativeInterop/VariantConversionCallbacks.cs | 1 + .../glue/GodotSharp/GodotSharp/Core/Object.base.cs | 2 +- .../glue/GodotSharp/GodotSharp/GodotSharp.csproj | 1 + modules/mono/glue/runtime_interop.cpp | 12 +- modules/mono/managed_callable.cpp | 5 +- modules/mono/managed_callable.h | 4 +- modules/mono/mono_gd/gd_mono_cache.h | 2 +- 19 files changed, 700 insertions(+), 80 deletions(-) create mode 100644 modules/mono/glue/GodotSharp/GodotSharp/Core/Callable.generics.cs diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptMethodsGenerator.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptMethodsGenerator.cs index d915eeac0b..d5d80df643 100644 --- a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptMethodsGenerator.cs +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptMethodsGenerator.cs @@ -188,14 +188,14 @@ namespace Godot.SourceGenerators 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"); + source.Append("NativeVariantPtrArgs args, 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(" return base.InvokeGodotClassMethod(method, args, out ret);\n"); source.Append(" }\n"); } @@ -364,7 +364,7 @@ namespace Godot.SourceGenerators source.Append(" if (method == MethodName."); source.Append(methodName); - source.Append(" && argCount == "); + source.Append(" && args.Count == "); source.Append(method.ParamTypes.Length); source.Append(") {\n"); 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 4e443ce26e..50196b84f0 100644 --- a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptSignalsGenerator.cs +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptSignalsGenerator.cs @@ -167,6 +167,7 @@ namespace Godot.SourceGenerators Common.ReportSignalDelegateSignatureMustReturnVoid(context, signalDelegateSymbol); } } + continue; } @@ -257,14 +258,14 @@ namespace Godot.SourceGenerators { source.Append( " protected override void RaiseGodotClassSignalCallbacks(in godot_string_name signal, "); - source.Append("NativeVariantPtrArgs args, int argCount)\n {\n"); + source.Append("NativeVariantPtrArgs args)\n {\n"); foreach (var signal in godotSignalDelegates) { GenerateSignalEventInvoker(signal, source); } - source.Append(" base.RaiseGodotClassSignalCallbacks(signal, args, argCount);\n"); + source.Append(" base.RaiseGodotClassSignalCallbacks(signal, args);\n"); source.Append(" }\n"); } @@ -404,7 +405,7 @@ namespace Godot.SourceGenerators source.Append(" if (signal == SignalName."); source.Append(signalName); - source.Append(" && argCount == "); + source.Append(" && args.Count == "); source.Append(invokeMethodData.ParamTypes.Length); source.Append(") {\n"); source.Append(" backing_"); diff --git a/modules/mono/editor/bindings_generator.cpp b/modules/mono/editor/bindings_generator.cpp index 3be8dd87c0..b90321b586 100644 --- a/modules/mono/editor/bindings_generator.cpp +++ b/modules/mono/editor/bindings_generator.cpp @@ -1614,7 +1614,7 @@ Error BindingsGenerator::_generate_cs_type(const TypeInterface &itype, const Str output << MEMBER_BEGIN "protected internal " << (is_derived_type ? "override" : "virtual") << " bool " CS_METHOD_INVOKE_GODOT_CLASS_METHOD "(in godot_string_name method, " - << "NativeVariantPtrArgs args, int argCount, out godot_variant ret)\n" + << "NativeVariantPtrArgs args, out godot_variant ret)\n" << INDENT1 "{\n"; for (const MethodInterface &imethod : itype.methods) { @@ -1630,7 +1630,7 @@ Error BindingsGenerator::_generate_cs_type(const TypeInterface &itype, const Str // We check both native names (snake_case) and proxy names (PascalCase) output << INDENT2 "if ((method == " << CS_STATIC_FIELD_METHOD_PROXY_NAME_PREFIX << imethod.name << " || method == MethodName." << imethod.proxy_name - << ") && argCount == " << itos(imethod.arguments.size()) + << ") && args.Count == " << itos(imethod.arguments.size()) << " && " << CS_METHOD_HAS_GODOT_CLASS_METHOD << "((godot_string_name)" << CS_STATIC_FIELD_METHOD_PROXY_NAME_PREFIX << imethod.name << ".NativeValue))\n" << INDENT2 "{\n"; @@ -1682,7 +1682,7 @@ Error BindingsGenerator::_generate_cs_type(const TypeInterface &itype, const Str } if (is_derived_type) { - output << INDENT2 "return base." CS_METHOD_INVOKE_GODOT_CLASS_METHOD "(method, args, argCount, out ret);\n"; + output << INDENT2 "return base." CS_METHOD_INVOKE_GODOT_CLASS_METHOD "(method, args, out ret);\n"; } else { output << INDENT2 "ret = default;\n" << INDENT2 "return false;\n"; @@ -2200,6 +2200,11 @@ Error BindingsGenerator::_generate_cs_method(const BindingsGenerator::TypeInterf Error BindingsGenerator::_generate_cs_signal(const BindingsGenerator::TypeInterface &p_itype, const BindingsGenerator::SignalInterface &p_isignal, StringBuilder &p_output) { String arguments_sig; + String delegate_type_params; + + if (!p_isignal.arguments.is_empty()) { + delegate_type_params += "<"; + } // Retrieve information from the arguments const ArgumentInterface &first = p_isignal.arguments.front()->get(); @@ -2220,11 +2225,18 @@ Error BindingsGenerator::_generate_cs_signal(const BindingsGenerator::TypeInterf if (&iarg != &first) { arguments_sig += ", "; + delegate_type_params += ", "; } arguments_sig += arg_type->cs_type; arguments_sig += " "; arguments_sig += iarg.name; + + delegate_type_params += arg_type->cs_type; + } + + if (!p_isignal.arguments.is_empty()) { + delegate_type_params += ">"; } // Generate signal @@ -2248,15 +2260,46 @@ Error BindingsGenerator::_generate_cs_signal(const BindingsGenerator::TypeInterf p_output.append("\")]"); } - String delegate_name = p_isignal.proxy_name; - delegate_name += "EventHandler"; // Delegate name is [SignalName]EventHandler + bool is_parameterless = p_isignal.arguments.size() == 0; - // Generate delegate - p_output.append(MEMBER_BEGIN "public delegate void "); - p_output.append(delegate_name); - p_output.append("("); - p_output.append(arguments_sig); - p_output.append(");\n"); + // Delegate name is [SignalName]EventHandler + String delegate_name = is_parameterless ? "Action" : p_isignal.proxy_name + "EventHandler"; + + if (!is_parameterless) { + // Generate delegate + p_output.append(MEMBER_BEGIN "public delegate void "); + p_output.append(delegate_name); + p_output.append("("); + p_output.append(arguments_sig); + p_output.append(");\n"); + + // Generate Callable trampoline for the delegate + p_output << MEMBER_BEGIN "private static unsafe void " << p_isignal.proxy_name << "Trampoline" + << "(object delegateObj, NativeVariantPtrArgs args, out godot_variant ret)\n" + << INDENT1 "{\n" + << INDENT2 "Callable.ThrowIfArgCountMismatch(args, " << itos(p_isignal.arguments.size()) << ");\n" + << INDENT2 "((" << delegate_name << ")delegateObj)("; + + int idx = 0; + for (const ArgumentInterface &iarg : p_isignal.arguments) { + const TypeInterface *arg_type = _get_type_or_null(iarg.type); + ERR_FAIL_NULL_V(arg_type, ERR_BUG); // Argument type not found + + if (idx != 0) { + p_output << ","; + } + + // TODO: We don't need to use VariantConversionCallbacks. We have the type information so we can use [cs_variant_to_managed] and [cs_managed_to_variant]. + p_output << "\n" INDENT3 "VariantConversionCallbacks.GetToManagedCallback<" + << arg_type->cs_type << ">()(args[" << itos(idx) << "])"; + + idx++; + } + + p_output << ");\n" + << INDENT2 "ret = default;\n" + << INDENT1 "}\n"; + } if (p_isignal.method_doc && p_isignal.method_doc->description.size()) { String xml_summary = bbcode_to_xml(fix_doc_description(p_isignal.method_doc->description), &p_itype); @@ -2292,6 +2335,11 @@ Error BindingsGenerator::_generate_cs_signal(const BindingsGenerator::TypeInterf p_output.append("static "); } + if (!is_parameterless) { + // `unsafe` is needed for taking the trampoline's function pointer + p_output << "unsafe "; + } + p_output.append("event "); p_output.append(delegate_name); p_output.append(" "); @@ -2304,8 +2352,13 @@ Error BindingsGenerator::_generate_cs_signal(const BindingsGenerator::TypeInterf p_output.append("add => Connect(SignalName."); } - p_output.append(p_isignal.proxy_name); - p_output.append(", new Callable(value));\n"); + if (is_parameterless) { + // Delegate type is Action. No need for custom trampoline. + p_output << p_isignal.proxy_name << ", Callable.From(value));\n"; + } else { + p_output << p_isignal.proxy_name + << ", Callable.CreateWithUnsafeTrampoline(value, &" << p_isignal.proxy_name << "Trampoline));\n"; + } if (p_itype.is_singleton) { p_output.append(INDENT2 "remove => " CS_PROPERTY_SINGLETON ".Disconnect(SignalName."); @@ -2313,8 +2366,14 @@ Error BindingsGenerator::_generate_cs_signal(const BindingsGenerator::TypeInterf p_output.append(INDENT2 "remove => Disconnect(SignalName."); } - p_output.append(p_isignal.proxy_name); - p_output.append(", new Callable(value));\n"); + if (is_parameterless) { + // Delegate type is Action. No need for custom trampoline. + p_output << p_isignal.proxy_name << ", Callable.From(value));\n"; + } else { + p_output << p_isignal.proxy_name + << ", Callable.CreateWithUnsafeTrampoline(value, &" << p_isignal.proxy_name << "Trampoline));\n"; + } + p_output.append(CLOSE_BLOCK_L1); } diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/CSharpInstanceBridge.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/CSharpInstanceBridge.cs index ae44f8f4ba..354212da1b 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/CSharpInstanceBridge.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/CSharpInstanceBridge.cs @@ -22,8 +22,7 @@ namespace Godot.Bridge } bool methodInvoked = godotObject.InvokeGodotClassMethod(CustomUnsafe.AsRef(method), - new NativeVariantPtrArgs(args), - argCount, out godot_variant retValue); + new NativeVariantPtrArgs(args, argCount), out godot_variant retValue); if (!methodInvoked) { @@ -102,7 +101,7 @@ namespace Godot.Bridge return godot_bool.False; } - *outRet = Marshaling.ConvertManagedObjectToVariant(ret); + *outRet = ret.CopyNativeVariant(); return godot_bool.True; } catch (Exception e) diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/ManagedCallbacks.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/ManagedCallbacks.cs index 57240624bc..44ea8fc83d 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/ManagedCallbacks.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/ManagedCallbacks.cs @@ -9,7 +9,7 @@ namespace Godot.Bridge { // @formatter:off public delegate* unmanaged SignalAwaiter_SignalCallback; - public delegate* unmanaged DelegateUtils_InvokeWithVariantArgs; + public delegate* unmanaged DelegateUtils_InvokeWithVariantArgs; public delegate* unmanaged DelegateUtils_DelegateEquals; public delegate* unmanaged DelegateUtils_TrySerializeDelegateWithGCHandle; public delegate* unmanaged DelegateUtils_TryDeserializeDelegateWithGCHandle; diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/ScriptManagerBridge.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/ScriptManagerBridge.cs index 092724a6b1..d83cf43eb2 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/ScriptManagerBridge.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/ScriptManagerBridge.cs @@ -339,7 +339,7 @@ namespace Godot.Bridge *outOwnerIsNull = godot_bool.False; owner.RaiseGodotClassSignalCallbacks(CustomUnsafe.AsRef(eventSignalName), - new NativeVariantPtrArgs(args), argCount); + new NativeVariantPtrArgs(args, argCount)); } catch (Exception e) { diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/Callable.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/Callable.cs index bdedd2e87a..f9309ca13e 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/Callable.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/Callable.cs @@ -26,11 +26,12 @@ namespace Godot /// } /// /// - public readonly struct Callable + public readonly partial struct Callable { private readonly Object _target; private readonly StringName _method; private readonly Delegate _delegate; + private readonly unsafe delegate* managed _trampoline; /// /// Object that contains the method. @@ -48,10 +49,10 @@ namespace Godot public Delegate Delegate => _delegate; /// - /// Converts a to a . + /// Trampoline function pointer for dynamically invoking . /// - /// The delegate to convert. - public static implicit operator Callable(Delegate @delegate) => new Callable(@delegate); + public unsafe delegate* managed Trampoline + => _trampoline; /// /// Constructs a new for the method called @@ -59,22 +60,21 @@ namespace Godot /// /// Object that contains the method. /// Name of the method that will be called. - public Callable(Object target, StringName method) + public unsafe Callable(Object target, StringName method) { _target = target; _method = method; _delegate = null; + _trampoline = null; } - /// - /// Constructs a new for the given . - /// - /// Delegate method that will be called. - public Callable(Delegate @delegate) + private unsafe Callable(Delegate @delegate, + delegate* managed trampoline) { _target = @delegate?.Target as Object; _method = null; _delegate = @delegate; + _trampoline = trampoline; } private const int VarArgsSpanThreshold = 5; @@ -149,5 +149,59 @@ namespace Godot NativeFuncs.godotsharp_callable_call_deferred(callable, (godot_variant**)argsPtr, argc); } } + + /// + /// + /// Constructs a new using the + /// function pointer to dynamically invoke the given . + /// + /// + /// The parameters passed to the function are: + /// + /// + /// + /// delegateObj + /// The given , upcast to . + /// + /// + /// args + /// Array of arguments. + /// + /// + /// ret + /// Return value of type . + /// + /// + /// + /// The delegate should be downcast to a more specific delegate type before invoking. + /// + /// + /// + /// Usage example: + /// + /// + /// static void Trampoline(object delegateObj, NativeVariantPtrArgs args, out godot_variant ret) + /// { + /// if (args.Count != 1) + /// throw new ArgumentException($"Callable expected {1} arguments but received {args.Count}."); + /// + /// TResult res = ((Func<int, string>)delegateObj)( + /// VariantConversionCallbacks.GetToManagedCallback<int>()(args[0]) + /// ); + /// + /// ret = VariantConversionCallbacks.GetToVariantCallback<string>()(res); + /// } + /// + /// var callable = Callable.CreateWithUnsafeTrampoline((int num) => "foo" + num.ToString(), &Trampoline); + /// var res = (string)callable.Call(10); + /// Console.WriteLine(res); + /// + /// + /// Delegate method that will be called. + /// Trampoline function pointer for invoking the delegate. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static unsafe Callable CreateWithUnsafeTrampoline(Delegate @delegate, + delegate* managed trampoline) + => new(@delegate, trampoline); } } diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/Callable.generics.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/Callable.generics.cs new file mode 100644 index 0000000000..6c6a104019 --- /dev/null +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/Callable.generics.cs @@ -0,0 +1,480 @@ +using System; +using System.Runtime.CompilerServices; +using Godot.NativeInterop; + +namespace Godot; + +#nullable enable + +public readonly partial struct Callable +{ + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static void ThrowIfArgCountMismatch(NativeVariantPtrArgs args, int countExpected, + [CallerArgumentExpression("args")] string? paramName = null) + { + if (countExpected != args.Count) + ThrowArgCountMismatch(countExpected, args.Count, paramName); + + static void ThrowArgCountMismatch(int countExpected, int countReceived, string? paramName) + { + throw new ArgumentException( + "Invalid argument count for invoking callable." + + $" Expected {countExpected} arguments, received {countReceived}.", + paramName); + } + } + + /// + /// Constructs a new for the given . + /// + /// Action method that will be called. + public static unsafe Callable From( + Action action + ) + { + static void Trampoline(object delegateObj, NativeVariantPtrArgs args, out godot_variant ret) + { + ThrowIfArgCountMismatch(args, 0); + + ((Action)delegateObj)(); + + ret = default; + } + + return CreateWithUnsafeTrampoline(action, &Trampoline); + } + + /// + public static unsafe Callable From( + Action action + ) + { + static void Trampoline(object delegateObj, NativeVariantPtrArgs args, out godot_variant ret) + { + ThrowIfArgCountMismatch(args, 1); + + ((Action)delegateObj)( + VariantConversionCallbacks.GetToManagedCallback()(args[0]) + ); + + ret = default; + } + + return CreateWithUnsafeTrampoline(action, &Trampoline); + } + + /// + public static unsafe Callable From( + Action action + ) + { + static void Trampoline(object delegateObj, NativeVariantPtrArgs args, out godot_variant ret) + { + ThrowIfArgCountMismatch(args, 2); + + ((Action)delegateObj)( + VariantConversionCallbacks.GetToManagedCallback()(args[0]), + VariantConversionCallbacks.GetToManagedCallback()(args[1]) + ); + + ret = default; + } + + return CreateWithUnsafeTrampoline(action, &Trampoline); + } + + /// + public static unsafe Callable From( + Action action + ) + { + static void Trampoline(object delegateObj, NativeVariantPtrArgs args, out godot_variant ret) + { + ThrowIfArgCountMismatch(args, 3); + + ((Action)delegateObj)( + VariantConversionCallbacks.GetToManagedCallback()(args[0]), + VariantConversionCallbacks.GetToManagedCallback()(args[1]), + VariantConversionCallbacks.GetToManagedCallback()(args[2]) + ); + + ret = default; + } + + return CreateWithUnsafeTrampoline(action, &Trampoline); + } + + /// + public static unsafe Callable From( + Action action + ) + { + static void Trampoline(object delegateObj, NativeVariantPtrArgs args, out godot_variant ret) + { + ThrowIfArgCountMismatch(args, 4); + + ((Action)delegateObj)( + VariantConversionCallbacks.GetToManagedCallback()(args[0]), + VariantConversionCallbacks.GetToManagedCallback()(args[1]), + VariantConversionCallbacks.GetToManagedCallback()(args[2]), + VariantConversionCallbacks.GetToManagedCallback()(args[3]) + ); + + ret = default; + } + + return CreateWithUnsafeTrampoline(action, &Trampoline); + } + + /// + public static unsafe Callable From( + Action action + ) + { + static void Trampoline(object delegateObj, NativeVariantPtrArgs args, out godot_variant ret) + { + ThrowIfArgCountMismatch(args, 5); + + ((Action)delegateObj)( + VariantConversionCallbacks.GetToManagedCallback()(args[0]), + VariantConversionCallbacks.GetToManagedCallback()(args[1]), + VariantConversionCallbacks.GetToManagedCallback()(args[2]), + VariantConversionCallbacks.GetToManagedCallback()(args[3]), + VariantConversionCallbacks.GetToManagedCallback()(args[4]) + ); + + ret = default; + } + + return CreateWithUnsafeTrampoline(action, &Trampoline); + } + + /// + public static unsafe Callable From( + Action action + ) + { + static void Trampoline(object delegateObj, NativeVariantPtrArgs args, out godot_variant ret) + { + ThrowIfArgCountMismatch(args, 6); + + ((Action)delegateObj)( + VariantConversionCallbacks.GetToManagedCallback()(args[0]), + VariantConversionCallbacks.GetToManagedCallback()(args[1]), + VariantConversionCallbacks.GetToManagedCallback()(args[2]), + VariantConversionCallbacks.GetToManagedCallback()(args[3]), + VariantConversionCallbacks.GetToManagedCallback()(args[4]), + VariantConversionCallbacks.GetToManagedCallback()(args[5]) + ); + + ret = default; + } + + return CreateWithUnsafeTrampoline(action, &Trampoline); + } + + /// + public static unsafe Callable From( + Action action + ) + { + static void Trampoline(object delegateObj, NativeVariantPtrArgs args, out godot_variant ret) + { + ThrowIfArgCountMismatch(args, 7); + + ((Action)delegateObj)( + VariantConversionCallbacks.GetToManagedCallback()(args[0]), + VariantConversionCallbacks.GetToManagedCallback()(args[1]), + VariantConversionCallbacks.GetToManagedCallback()(args[2]), + VariantConversionCallbacks.GetToManagedCallback()(args[3]), + VariantConversionCallbacks.GetToManagedCallback()(args[4]), + VariantConversionCallbacks.GetToManagedCallback()(args[5]), + VariantConversionCallbacks.GetToManagedCallback()(args[6]) + ); + + ret = default; + } + + return CreateWithUnsafeTrampoline(action, &Trampoline); + } + + /// + public static unsafe Callable From( + Action action + ) + { + static void Trampoline(object delegateObj, NativeVariantPtrArgs args, out godot_variant ret) + { + ThrowIfArgCountMismatch(args, 8); + + ((Action)delegateObj)( + VariantConversionCallbacks.GetToManagedCallback()(args[0]), + VariantConversionCallbacks.GetToManagedCallback()(args[1]), + VariantConversionCallbacks.GetToManagedCallback()(args[2]), + VariantConversionCallbacks.GetToManagedCallback()(args[3]), + VariantConversionCallbacks.GetToManagedCallback()(args[4]), + VariantConversionCallbacks.GetToManagedCallback()(args[5]), + VariantConversionCallbacks.GetToManagedCallback()(args[6]), + VariantConversionCallbacks.GetToManagedCallback()(args[7]) + ); + + ret = default; + } + + return CreateWithUnsafeTrampoline(action, &Trampoline); + } + + /// + public static unsafe Callable From( + Action action + ) + { + static void Trampoline(object delegateObj, NativeVariantPtrArgs args, out godot_variant ret) + { + ThrowIfArgCountMismatch(args, 9); + + ((Action)delegateObj)( + VariantConversionCallbacks.GetToManagedCallback()(args[0]), + VariantConversionCallbacks.GetToManagedCallback()(args[1]), + VariantConversionCallbacks.GetToManagedCallback()(args[2]), + VariantConversionCallbacks.GetToManagedCallback()(args[3]), + VariantConversionCallbacks.GetToManagedCallback()(args[4]), + VariantConversionCallbacks.GetToManagedCallback()(args[5]), + VariantConversionCallbacks.GetToManagedCallback()(args[6]), + VariantConversionCallbacks.GetToManagedCallback()(args[7]), + VariantConversionCallbacks.GetToManagedCallback()(args[8]) + ); + + ret = default; + } + + return CreateWithUnsafeTrampoline(action, &Trampoline); + } + + /// + /// Constructs a new for the given . + /// + /// Action method that will be called. + public static unsafe Callable From( + Func func + ) + { + static void Trampoline(object delegateObj, NativeVariantPtrArgs args, out godot_variant ret) + { + ThrowIfArgCountMismatch(args, 0); + + TResult res = ((Func)delegateObj)(); + + ret = VariantConversionCallbacks.GetToVariantCallback()(res); + } + + return CreateWithUnsafeTrampoline(func, &Trampoline); + } + + /// + public static unsafe Callable From( + Func func + ) + { + static void Trampoline(object delegateObj, NativeVariantPtrArgs args, out godot_variant ret) + { + ThrowIfArgCountMismatch(args, 1); + + TResult res = ((Func)delegateObj)( + VariantConversionCallbacks.GetToManagedCallback()(args[0]) + ); + + ret = VariantConversionCallbacks.GetToVariantCallback()(res); + } + + return CreateWithUnsafeTrampoline(func, &Trampoline); + } + + /// + public static unsafe Callable From( + Func func + ) + { + static void Trampoline(object delegateObj, NativeVariantPtrArgs args, out godot_variant ret) + { + ThrowIfArgCountMismatch(args, 2); + + TResult res = ((Func)delegateObj)( + VariantConversionCallbacks.GetToManagedCallback()(args[0]), + VariantConversionCallbacks.GetToManagedCallback()(args[1]) + ); + + ret = VariantConversionCallbacks.GetToVariantCallback()(res); + } + + return CreateWithUnsafeTrampoline(func, &Trampoline); + } + + /// + public static unsafe Callable From( + Func func + ) + { + static void Trampoline(object delegateObj, NativeVariantPtrArgs args, out godot_variant ret) + { + ThrowIfArgCountMismatch(args, 3); + + TResult res = ((Func)delegateObj)( + VariantConversionCallbacks.GetToManagedCallback()(args[0]), + VariantConversionCallbacks.GetToManagedCallback()(args[1]), + VariantConversionCallbacks.GetToManagedCallback()(args[2]) + ); + + ret = VariantConversionCallbacks.GetToVariantCallback()(res); + } + + return CreateWithUnsafeTrampoline(func, &Trampoline); + } + + /// + public static unsafe Callable From( + Func func + ) + { + static void Trampoline(object delegateObj, NativeVariantPtrArgs args, out godot_variant ret) + { + ThrowIfArgCountMismatch(args, 4); + + TResult res = ((Func)delegateObj)( + VariantConversionCallbacks.GetToManagedCallback()(args[0]), + VariantConversionCallbacks.GetToManagedCallback()(args[1]), + VariantConversionCallbacks.GetToManagedCallback()(args[2]), + VariantConversionCallbacks.GetToManagedCallback()(args[3]) + ); + + ret = VariantConversionCallbacks.GetToVariantCallback()(res); + } + + return CreateWithUnsafeTrampoline(func, &Trampoline); + } + + /// + public static unsafe Callable From( + Func func + ) + { + static void Trampoline(object delegateObj, NativeVariantPtrArgs args, out godot_variant ret) + { + ThrowIfArgCountMismatch(args, 5); + + TResult res = ((Func)delegateObj)( + VariantConversionCallbacks.GetToManagedCallback()(args[0]), + VariantConversionCallbacks.GetToManagedCallback()(args[1]), + VariantConversionCallbacks.GetToManagedCallback()(args[2]), + VariantConversionCallbacks.GetToManagedCallback()(args[3]), + VariantConversionCallbacks.GetToManagedCallback()(args[4]) + ); + + ret = VariantConversionCallbacks.GetToVariantCallback()(res); + } + + return CreateWithUnsafeTrampoline(func, &Trampoline); + } + + /// + public static unsafe Callable From( + Func func + ) + { + static void Trampoline(object delegateObj, NativeVariantPtrArgs args, out godot_variant ret) + { + ThrowIfArgCountMismatch(args, 6); + + TResult res = ((Func)delegateObj)( + VariantConversionCallbacks.GetToManagedCallback()(args[0]), + VariantConversionCallbacks.GetToManagedCallback()(args[1]), + VariantConversionCallbacks.GetToManagedCallback()(args[2]), + VariantConversionCallbacks.GetToManagedCallback()(args[3]), + VariantConversionCallbacks.GetToManagedCallback()(args[4]), + VariantConversionCallbacks.GetToManagedCallback()(args[5]) + ); + + ret = VariantConversionCallbacks.GetToVariantCallback()(res); + } + + return CreateWithUnsafeTrampoline(func, &Trampoline); + } + + /// + public static unsafe Callable From( + Func func + ) + { + static void Trampoline(object delegateObj, NativeVariantPtrArgs args, out godot_variant ret) + { + ThrowIfArgCountMismatch(args, 7); + + TResult res = ((Func)delegateObj)( + VariantConversionCallbacks.GetToManagedCallback()(args[0]), + VariantConversionCallbacks.GetToManagedCallback()(args[1]), + VariantConversionCallbacks.GetToManagedCallback()(args[2]), + VariantConversionCallbacks.GetToManagedCallback()(args[3]), + VariantConversionCallbacks.GetToManagedCallback()(args[4]), + VariantConversionCallbacks.GetToManagedCallback()(args[5]), + VariantConversionCallbacks.GetToManagedCallback()(args[6]) + ); + + ret = VariantConversionCallbacks.GetToVariantCallback()(res); + } + + return CreateWithUnsafeTrampoline(func, &Trampoline); + } + + /// + public static unsafe Callable From( + Func func + ) + { + static void Trampoline(object delegateObj, NativeVariantPtrArgs args, out godot_variant ret) + { + ThrowIfArgCountMismatch(args, 8); + + TResult res = ((Func)delegateObj)( + VariantConversionCallbacks.GetToManagedCallback()(args[0]), + VariantConversionCallbacks.GetToManagedCallback()(args[1]), + VariantConversionCallbacks.GetToManagedCallback()(args[2]), + VariantConversionCallbacks.GetToManagedCallback()(args[3]), + VariantConversionCallbacks.GetToManagedCallback()(args[4]), + VariantConversionCallbacks.GetToManagedCallback()(args[5]), + VariantConversionCallbacks.GetToManagedCallback()(args[6]), + VariantConversionCallbacks.GetToManagedCallback()(args[7]) + ); + + ret = VariantConversionCallbacks.GetToVariantCallback()(res); + } + + return CreateWithUnsafeTrampoline(func, &Trampoline); + } + + /// + public static unsafe Callable From( + Func func + ) + { + static void Trampoline(object delegateObj, NativeVariantPtrArgs args, out godot_variant ret) + { + ThrowIfArgCountMismatch(args, 9); + + TResult res = ((Func)delegateObj)( + VariantConversionCallbacks.GetToManagedCallback()(args[0]), + VariantConversionCallbacks.GetToManagedCallback()(args[1]), + VariantConversionCallbacks.GetToManagedCallback()(args[2]), + VariantConversionCallbacks.GetToManagedCallback()(args[3]), + VariantConversionCallbacks.GetToManagedCallback()(args[4]), + VariantConversionCallbacks.GetToManagedCallback()(args[5]), + VariantConversionCallbacks.GetToManagedCallback()(args[6]), + VariantConversionCallbacks.GetToManagedCallback()(args[7]), + VariantConversionCallbacks.GetToManagedCallback()(args[8]) + ); + + ret = VariantConversionCallbacks.GetToVariantCallback()(res); + } + + return CreateWithUnsafeTrampoline(func, &Trampoline); + } +} diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/DelegateUtils.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/DelegateUtils.cs index 9b3969d453..d19e0c08f2 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/DelegateUtils.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/DelegateUtils.cs @@ -30,33 +30,23 @@ namespace Godot } [UnmanagedCallersOnly] - internal static unsafe void InvokeWithVariantArgs(IntPtr delegateGCHandle, godot_variant** args, uint argc, - godot_variant* outRet) + internal static unsafe void InvokeWithVariantArgs(IntPtr delegateGCHandle, void* trampoline, + godot_variant** args, int argc, godot_variant* outRet) { try { - // TODO: Optimize - var @delegate = (Delegate)GCHandle.FromIntPtr(delegateGCHandle).Target!; - var managedArgs = new object?[argc]; - - var parameterInfos = @delegate.Method.GetParameters(); - var paramsLength = parameterInfos.Length; - - if (argc != paramsLength) + if (trampoline == null) { - throw new InvalidOperationException( - $"The delegate expects {paramsLength} arguments, but received {argc}."); + throw new ArgumentNullException(nameof(trampoline), + "Cannot dynamically invoke delegate because the trampoline is null."); } - for (uint i = 0; i < argc; i++) - { - managedArgs[i] = Marshaling.ConvertVariantToManagedObjectOfType( - *args[i], parameterInfos[i].ParameterType); - } + var @delegate = (Delegate)GCHandle.FromIntPtr(delegateGCHandle).Target!; + var trampolineFn = (delegate* managed)trampoline; - object? invokeRet = @delegate.DynamicInvoke(managedArgs); + trampolineFn(@delegate, new NativeVariantPtrArgs(args, argc), out godot_variant ret); - *outRet = Marshaling.ConvertManagedObjectToVariant(invokeRet); + *outRet = ret; } catch (Exception e) { diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/NativeInterop/Marshaling.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/NativeInterop/Marshaling.cs index 76b186cd15..649661ee06 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/NativeInterop/Marshaling.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/NativeInterop/Marshaling.cs @@ -721,10 +721,19 @@ namespace Godot.NativeInterop if (p_managed_callable.Delegate != null) { var gcHandle = CustomGCHandle.AllocStrong(p_managed_callable.Delegate); - IntPtr objectPtr = p_managed_callable.Target != null ? Object.GetPtr(p_managed_callable.Target) : IntPtr.Zero; - NativeFuncs.godotsharp_callable_new_with_delegate( - GCHandle.ToIntPtr(gcHandle), objectPtr, out godot_callable callable); - return callable; + + IntPtr objectPtr = p_managed_callable.Target != null ? + Object.GetPtr(p_managed_callable.Target) : + IntPtr.Zero; + + unsafe + { + NativeFuncs.godotsharp_callable_new_with_delegate( + GCHandle.ToIntPtr(gcHandle), (IntPtr)p_managed_callable.Trampoline, + objectPtr, out godot_callable callable); + + return callable; + } } else { @@ -748,19 +757,22 @@ namespace Godot.NativeInterop public static Callable ConvertCallableToManaged(in godot_callable p_callable) { if (NativeFuncs.godotsharp_callable_get_data_for_marshalling(p_callable, - out IntPtr delegateGCHandle, out IntPtr godotObject, - out godot_string_name name).ToBool()) + out IntPtr delegateGCHandle, out IntPtr trampoline, + out IntPtr godotObject, out godot_string_name name).ToBool()) { if (delegateGCHandle != IntPtr.Zero) { - return new Callable((Delegate?)GCHandle.FromIntPtr(delegateGCHandle).Target); - } - else - { - return new Callable( - InteropUtils.UnmanagedGetManaged(godotObject), - StringName.CreateTakingOwnershipOfDisposableValue(name)); + unsafe + { + return Callable.CreateWithUnsafeTrampoline( + (Delegate?)GCHandle.FromIntPtr(delegateGCHandle).Target, + (delegate* managed)trampoline); + } } + + return new Callable( + InteropUtils.UnmanagedGetManaged(godotObject), + StringName.CreateTakingOwnershipOfDisposableValue(name)); } // Some other unsupported callable diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/NativeInterop/NativeFuncs.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/NativeInterop/NativeFuncs.cs index 20ede9f0dd..088f4e7ecf 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/NativeInterop/NativeFuncs.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/NativeInterop/NativeFuncs.cs @@ -141,11 +141,11 @@ namespace Godot.NativeInterop public static partial void godotsharp_packed_string_array_add(ref godot_packed_string_array r_dest, in godot_string p_element); - public static partial void godotsharp_callable_new_with_delegate(IntPtr p_delegate_handle, IntPtr p_object, - out godot_callable r_callable); + public static partial void godotsharp_callable_new_with_delegate(IntPtr p_delegate_handle, IntPtr p_trampoline, + IntPtr p_object, out godot_callable r_callable); internal static partial godot_bool godotsharp_callable_get_data_for_marshalling(in godot_callable p_callable, - out IntPtr r_delegate_handle, out IntPtr r_object, out godot_string_name r_name); + out IntPtr r_delegate_handle, out IntPtr r_trampoline, out IntPtr r_object, out godot_string_name r_name); internal static partial godot_variant godotsharp_callable_call(in godot_callable p_callable, godot_variant** p_args, int p_arg_count, out godot_variant_call_error p_call_error); diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/NativeInterop/NativeVariantPtrArgs.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/NativeInterop/NativeVariantPtrArgs.cs index 422df74c23..d8c5d99cb8 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/NativeInterop/NativeVariantPtrArgs.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/NativeInterop/NativeVariantPtrArgs.cs @@ -8,8 +8,22 @@ namespace Godot.NativeInterop public unsafe ref struct NativeVariantPtrArgs { private godot_variant** _args; + private int _argc; - internal NativeVariantPtrArgs(godot_variant** args) => _args = args; + internal NativeVariantPtrArgs(godot_variant** args, int argc) + { + _args = args; + _argc = argc; + } + + /// + /// Returns the number of arguments. + /// + public int Count + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => _argc; + } public ref godot_variant this[int index] { diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/NativeInterop/VariantConversionCallbacks.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/NativeInterop/VariantConversionCallbacks.cs index 9cde62c7c5..2707cb9bd2 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/NativeInterop/VariantConversionCallbacks.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/NativeInterop/VariantConversionCallbacks.cs @@ -3,6 +3,7 @@ using System.Diagnostics.CodeAnalysis; namespace Godot.NativeInterop; +// TODO: Change VariantConversionCallbacks. Store the callback in a static field for quick repeated access, instead of checking every time. internal static unsafe class VariantConversionCallbacks { [SuppressMessage("ReSharper", "RedundantNameQualifier")] diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/Object.base.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/Object.base.cs index 5cb678c280..60ee6eb6f4 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/Object.base.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/Object.base.cs @@ -202,7 +202,7 @@ namespace Godot // ReSharper disable once VirtualMemberNeverOverridden.Global protected internal virtual void RaiseGodotClassSignalCallbacks(in godot_string_name signal, - NativeVariantPtrArgs args, int argCount) + NativeVariantPtrArgs args) { } diff --git a/modules/mono/glue/GodotSharp/GodotSharp/GodotSharp.csproj b/modules/mono/glue/GodotSharp/GodotSharp/GodotSharp.csproj index a63b668387..e3fb254f49 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/GodotSharp.csproj +++ b/modules/mono/glue/GodotSharp/GodotSharp/GodotSharp.csproj @@ -52,6 +52,7 @@ + diff --git a/modules/mono/glue/runtime_interop.cpp b/modules/mono/glue/runtime_interop.cpp index 2717b945f6..e20a88076a 100644 --- a/modules/mono/glue/runtime_interop.cpp +++ b/modules/mono/glue/runtime_interop.cpp @@ -447,15 +447,16 @@ void godotsharp_packed_string_array_add(PackedStringArray *r_dest, const String r_dest->append(*p_element); } -void godotsharp_callable_new_with_delegate(GCHandleIntPtr p_delegate_handle, const Object *p_object, Callable *r_callable) { +void godotsharp_callable_new_with_delegate(GCHandleIntPtr p_delegate_handle, void *p_trampoline, + const Object *p_object, Callable *r_callable) { // TODO: Use pooling for ManagedCallable instances. ObjectID objid = p_object ? p_object->get_instance_id() : ObjectID(); - CallableCustom *managed_callable = memnew(ManagedCallable(p_delegate_handle, objid)); + CallableCustom *managed_callable = memnew(ManagedCallable(p_delegate_handle, p_trampoline, objid)); memnew_placement(r_callable, Callable(managed_callable)); } bool godotsharp_callable_get_data_for_marshalling(const Callable *p_callable, - GCHandleIntPtr *r_delegate_handle, Object **r_object, StringName *r_name) { + GCHandleIntPtr *r_delegate_handle, void **r_trampoline, Object **r_object, StringName *r_name) { if (p_callable->is_custom()) { CallableCustom *custom = p_callable->get_custom(); CallableCustom::CompareEqualFunc compare_equal_func = custom->get_compare_equal_func(); @@ -463,18 +464,21 @@ bool godotsharp_callable_get_data_for_marshalling(const Callable *p_callable, if (compare_equal_func == ManagedCallable::compare_equal_func_ptr) { ManagedCallable *managed_callable = static_cast(custom); *r_delegate_handle = managed_callable->get_delegate(); + *r_trampoline = managed_callable->get_trampoline(); *r_object = nullptr; memnew_placement(r_name, StringName()); return true; } else if (compare_equal_func == SignalAwaiterCallable::compare_equal_func_ptr) { SignalAwaiterCallable *signal_awaiter_callable = static_cast(custom); *r_delegate_handle = { nullptr }; + *r_trampoline = nullptr; *r_object = ObjectDB::get_instance(signal_awaiter_callable->get_object()); memnew_placement(r_name, StringName(signal_awaiter_callable->get_signal())); return true; } else if (compare_equal_func == EventSignalCallable::compare_equal_func_ptr) { EventSignalCallable *event_signal_callable = static_cast(custom); *r_delegate_handle = { nullptr }; + *r_trampoline = nullptr; *r_object = ObjectDB::get_instance(event_signal_callable->get_object()); memnew_placement(r_name, StringName(event_signal_callable->get_signal())); return true; @@ -482,11 +486,13 @@ bool godotsharp_callable_get_data_for_marshalling(const Callable *p_callable, // Some other CallableCustom. We only support ManagedCallable. *r_delegate_handle = { nullptr }; + *r_trampoline = nullptr; *r_object = nullptr; memnew_placement(r_name, StringName()); return false; } else { *r_delegate_handle = { nullptr }; + *r_trampoline = nullptr; *r_object = ObjectDB::get_instance(p_callable->get_object_id()); memnew_placement(r_name, StringName(p_callable->get_method())); return true; diff --git a/modules/mono/managed_callable.cpp b/modules/mono/managed_callable.cpp index 0c2c533090..28edc41d98 100644 --- a/modules/mono/managed_callable.cpp +++ b/modules/mono/managed_callable.cpp @@ -92,7 +92,7 @@ void ManagedCallable::call(const Variant **p_arguments, int p_argcount, Variant ERR_FAIL_COND(delegate_handle.value == nullptr); GDMonoCache::managed_callbacks.DelegateUtils_InvokeWithVariantArgs( - delegate_handle, p_arguments, p_argcount, &r_return_value); + delegate_handle, trampoline, p_arguments, p_argcount, &r_return_value); r_call_error.error = Callable::CallError::CALL_OK; } @@ -106,7 +106,8 @@ void ManagedCallable::release_delegate_handle() { // Why you do this clang-format... /* clang-format off */ -ManagedCallable::ManagedCallable(GCHandleIntPtr p_delegate_handle, ObjectID p_object_id) : delegate_handle(p_delegate_handle), object_id(p_object_id) { +ManagedCallable::ManagedCallable(GCHandleIntPtr p_delegate_handle, void *p_trampoline, ObjectID p_object_id) : + delegate_handle(p_delegate_handle), trampoline(p_trampoline), object_id(p_object_id) { #ifdef GD_MONO_HOT_RELOAD { MutexLock lock(instances_mutex); diff --git a/modules/mono/managed_callable.h b/modules/mono/managed_callable.h index 26cd164fb6..b3a137dedf 100644 --- a/modules/mono/managed_callable.h +++ b/modules/mono/managed_callable.h @@ -40,6 +40,7 @@ class ManagedCallable : public CallableCustom { friend class CSharpLanguage; GCHandleIntPtr delegate_handle; + void *trampoline = nullptr; ObjectID object_id; #ifdef GD_MONO_HOT_RELOAD @@ -58,6 +59,7 @@ public: void call(const Variant **p_arguments, int p_argcount, Variant &r_return_value, Callable::CallError &r_call_error) const override; _FORCE_INLINE_ GCHandleIntPtr get_delegate() const { return delegate_handle; } + _FORCE_INLINE_ void *get_trampoline() const { return trampoline; } static bool compare_equal(const CallableCustom *p_a, const CallableCustom *p_b); static bool compare_less(const CallableCustom *p_a, const CallableCustom *p_b); @@ -67,7 +69,7 @@ public: void release_delegate_handle(); - ManagedCallable(GCHandleIntPtr p_delegate_handle, ObjectID p_object_id); + ManagedCallable(GCHandleIntPtr p_delegate_handle, void *p_trampoline, ObjectID p_object_id); ~ManagedCallable(); }; diff --git a/modules/mono/mono_gd/gd_mono_cache.h b/modules/mono/mono_gd/gd_mono_cache.h index 13b599fe55..9c26fa2b0a 100644 --- a/modules/mono/mono_gd/gd_mono_cache.h +++ b/modules/mono/mono_gd/gd_mono_cache.h @@ -74,7 +74,7 @@ struct ManagedCallbacks { using Callback_ScriptManagerBridge_GetPropertyDefaultValues_Add = void(GD_CLR_STDCALL *)(CSharpScript *p_script, void *p_def_vals, int32_t p_count); using FuncSignalAwaiter_SignalCallback = void(GD_CLR_STDCALL *)(GCHandleIntPtr, const Variant **, int32_t, bool *); - using FuncDelegateUtils_InvokeWithVariantArgs = void(GD_CLR_STDCALL *)(GCHandleIntPtr, const Variant **, uint32_t, const Variant *); + using FuncDelegateUtils_InvokeWithVariantArgs = void(GD_CLR_STDCALL *)(GCHandleIntPtr, void *, const Variant **, int32_t, const Variant *); using FuncDelegateUtils_DelegateEquals = bool(GD_CLR_STDCALL *)(GCHandleIntPtr, GCHandleIntPtr); using FuncDelegateUtils_TrySerializeDelegateWithGCHandle = bool(GD_CLR_STDCALL *)(GCHandleIntPtr, const Array *); using FuncDelegateUtils_TryDeserializeDelegateWithGCHandle = bool(GD_CLR_STDCALL *)(const Array *, GCHandleIntPtr *); -- cgit v1.2.3 From f66a352c0fc03758e3ac54443a70ded6afe22d92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ignacio=20Rold=C3=A1n=20Etcheverry?= Date: Fri, 28 Oct 2022 22:59:15 +0200 Subject: C#: Add Variant conversion callbacks for generic Godot collections This allows using generic Godot collections as type arguments for other generic Godot collections. This also allows generic Godot collections as parameter or return type in dynamic Callable invocations. --- .../mono/glue/GodotSharp/GodotSharp/Core/Array.cs | 32 +++++++---- .../glue/GodotSharp/GodotSharp/Core/Dictionary.cs | 67 +++++++++++++--------- .../NativeInterop/VariantConversionCallbacks.cs | 44 ++++++++++++++ 3 files changed, 106 insertions(+), 37 deletions(-) diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/Array.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/Array.cs index 1c98dfcdf6..f1b46e293b 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/Array.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/Array.cs @@ -489,25 +489,37 @@ namespace Godot.Collections ICollection, IEnumerable { + private static godot_variant ToVariantFunc(in Array godotArray) => + VariantUtils.CreateFromArray(godotArray); + + private static Array FromVariantFunc(in godot_variant variant) => + VariantUtils.ConvertToArrayObject(variant); + // ReSharper disable StaticMemberInGenericType // Warning is about unique static fields being created for each generic type combination: // https://www.jetbrains.com/help/resharper/StaticMemberInGenericType.html // In our case this is exactly what we want. - private static unsafe delegate* managed _convertToVariantCallback; - private static unsafe delegate* managed _convertToManagedCallback; + private static readonly unsafe delegate* managed ConvertToVariantCallback; + private static readonly unsafe delegate* managed ConvertToManagedCallback; // ReSharper restore StaticMemberInGenericType static unsafe Array() { - _convertToVariantCallback = VariantConversionCallbacks.GetToVariantCallback(); - _convertToManagedCallback = VariantConversionCallbacks.GetToManagedCallback(); + VariantConversionCallbacks.GenericConversionCallbacks[typeof(Array)] = + ( + (IntPtr)(delegate* managed, godot_variant>)&ToVariantFunc, + (IntPtr)(delegate* managed>)&FromVariantFunc + ); + + ConvertToVariantCallback = VariantConversionCallbacks.GetToVariantCallback(); + ConvertToManagedCallback = VariantConversionCallbacks.GetToManagedCallback(); } private static unsafe void ValidateVariantConversionCallbacks() { - if (_convertToVariantCallback == null || _convertToManagedCallback == null) + if (ConvertToVariantCallback == null || ConvertToManagedCallback == null) { throw new InvalidOperationException( $"The array element type is not supported for conversion to Variant: '{typeof(T).FullName}'."); @@ -653,7 +665,7 @@ namespace Godot.Collections get { _underlyingArray.GetVariantBorrowElementAt(index, out godot_variant borrowElem); - return _convertToManagedCallback(borrowElem); + return ConvertToManagedCallback(borrowElem); } set { @@ -663,7 +675,7 @@ namespace Godot.Collections godot_variant* ptrw = NativeFuncs.godotsharp_array_ptrw(ref self); godot_variant* itemPtr = &ptrw[index]; (*itemPtr).Dispose(); - *itemPtr = _convertToVariantCallback(value); + *itemPtr = ConvertToVariantCallback(value); } } @@ -675,7 +687,7 @@ namespace Godot.Collections /// The index of the item, or -1 if not found. public unsafe int IndexOf(T item) { - using var variantValue = _convertToVariantCallback(item); + using var variantValue = ConvertToVariantCallback(item); var self = (godot_array)_underlyingArray.NativeValue; return NativeFuncs.godotsharp_array_index_of(ref self, variantValue); } @@ -693,7 +705,7 @@ namespace Godot.Collections if (index < 0 || index > Count) throw new ArgumentOutOfRangeException(nameof(index)); - using var variantValue = _convertToVariantCallback(item); + using var variantValue = ConvertToVariantCallback(item); var self = (godot_array)_underlyingArray.NativeValue; NativeFuncs.godotsharp_array_insert(ref self, index, variantValue); } @@ -726,7 +738,7 @@ namespace Godot.Collections /// The new size after adding the item. public unsafe void Add(T item) { - using var variantValue = _convertToVariantCallback(item); + using var variantValue = ConvertToVariantCallback(item); var self = (godot_array)_underlyingArray.NativeValue; _ = NativeFuncs.godotsharp_array_add(ref self, variantValue); } diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/Dictionary.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/Dictionary.cs index 93103d0f6b..f8793332a0 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/Dictionary.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/Dictionary.cs @@ -356,35 +356,47 @@ namespace Godot.Collections IDictionary, IReadOnlyDictionary { + private static godot_variant ToVariantFunc(in Dictionary godotDictionary) => + VariantUtils.CreateFromDictionary(godotDictionary); + + private static Dictionary FromVariantFunc(in godot_variant variant) => + VariantUtils.ConvertToDictionaryObject(variant); + // ReSharper disable StaticMemberInGenericType // Warning is about unique static fields being created for each generic type combination: // https://www.jetbrains.com/help/resharper/StaticMemberInGenericType.html // In our case this is exactly what we want. - private static unsafe delegate* managed _convertKeyToVariantCallback; - private static unsafe delegate* managed _convertKeyToManagedCallback; - private static unsafe delegate* managed _convertValueToVariantCallback; - private static unsafe delegate* managed _convertValueToManagedCallback; + private static readonly unsafe delegate* managed ConvertKeyToVariantCallback; + private static readonly unsafe delegate* managed ConvertKeyToManagedCallback; + private static readonly unsafe delegate* managed ConvertValueToVariantCallback; + private static readonly unsafe delegate* managed ConvertValueToManagedCallback; // ReSharper restore StaticMemberInGenericType static unsafe Dictionary() { - _convertKeyToVariantCallback = VariantConversionCallbacks.GetToVariantCallback(); - _convertKeyToManagedCallback = VariantConversionCallbacks.GetToManagedCallback(); - _convertValueToVariantCallback = VariantConversionCallbacks.GetToVariantCallback(); - _convertValueToManagedCallback = VariantConversionCallbacks.GetToManagedCallback(); + VariantConversionCallbacks.GenericConversionCallbacks[typeof(Dictionary)] = + ( + (IntPtr)(delegate* managed, godot_variant>)&ToVariantFunc, + (IntPtr)(delegate* managed>)&FromVariantFunc + ); + + ConvertKeyToVariantCallback = VariantConversionCallbacks.GetToVariantCallback(); + ConvertKeyToManagedCallback = VariantConversionCallbacks.GetToManagedCallback(); + ConvertValueToVariantCallback = VariantConversionCallbacks.GetToVariantCallback(); + ConvertValueToManagedCallback = VariantConversionCallbacks.GetToManagedCallback(); } private static unsafe void ValidateVariantConversionCallbacks() { - if (_convertKeyToVariantCallback == null || _convertKeyToManagedCallback == null) + if (ConvertKeyToVariantCallback == null || ConvertKeyToManagedCallback == null) { throw new InvalidOperationException( $"The dictionary key type is not supported for conversion to Variant: '{typeof(TKey).FullName}'."); } - if (_convertValueToVariantCallback == null || _convertValueToManagedCallback == null) + if (ConvertValueToVariantCallback == null || ConvertValueToManagedCallback == null) { throw new InvalidOperationException( $"The dictionary value type is not supported for conversion to Variant: '{typeof(TValue).FullName}'."); @@ -473,14 +485,14 @@ namespace Godot.Collections { get { - using var variantKey = _convertKeyToVariantCallback(key); + using var variantKey = ConvertKeyToVariantCallback(key); var self = (godot_dictionary)_underlyingDict.NativeValue; if (NativeFuncs.godotsharp_dictionary_try_get_value(ref self, variantKey, out godot_variant value).ToBool()) { using (value) - return _convertValueToManagedCallback(value); + return ConvertValueToManagedCallback(value); } else { @@ -489,8 +501,8 @@ namespace Godot.Collections } set { - using var variantKey = _convertKeyToVariantCallback(key); - using var variantValue = _convertValueToVariantCallback(value); + using var variantKey = ConvertKeyToVariantCallback(key); + using var variantValue = ConvertValueToVariantCallback(value); var self = (godot_dictionary)_underlyingDict.NativeValue; NativeFuncs.godotsharp_dictionary_set_value(ref self, variantKey, variantValue); @@ -539,8 +551,8 @@ namespace Godot.Collections using (value) { return new KeyValuePair( - _convertKeyToManagedCallback(key), - _convertValueToManagedCallback(value)); + ConvertKeyToManagedCallback(key), + ConvertValueToManagedCallback(value)); } } @@ -552,13 +564,13 @@ namespace Godot.Collections /// The object to add. public unsafe void Add(TKey key, TValue value) { - using var variantKey = _convertKeyToVariantCallback(key); + using var variantKey = ConvertKeyToVariantCallback(key); var self = (godot_dictionary)_underlyingDict.NativeValue; if (NativeFuncs.godotsharp_dictionary_contains_key(ref self, variantKey).ToBool()) throw new ArgumentException("An element with the same key already exists.", nameof(key)); - using var variantValue = _convertValueToVariantCallback(value); + using var variantValue = ConvertValueToVariantCallback(value); NativeFuncs.godotsharp_dictionary_add(ref self, variantKey, variantValue); } @@ -569,7 +581,7 @@ namespace Godot.Collections /// Whether or not this dictionary contains the given key. public unsafe bool ContainsKey(TKey key) { - using var variantKey = _convertKeyToVariantCallback(key); + using var variantKey = ConvertKeyToVariantCallback(key); var self = (godot_dictionary)_underlyingDict.NativeValue; return NativeFuncs.godotsharp_dictionary_contains_key(ref self, variantKey).ToBool(); } @@ -580,7 +592,7 @@ namespace Godot.Collections /// The key of the element to remove. public unsafe bool Remove(TKey key) { - using var variantKey = _convertKeyToVariantCallback(key); + using var variantKey = ConvertKeyToVariantCallback(key); var self = (godot_dictionary)_underlyingDict.NativeValue; return NativeFuncs.godotsharp_dictionary_remove_key(ref self, variantKey).ToBool(); } @@ -593,13 +605,13 @@ namespace Godot.Collections /// If an object was found for the given . public unsafe bool TryGetValue(TKey key, [MaybeNullWhen(false)] out TValue value) { - using var variantKey = _convertKeyToVariantCallback(key); + using var variantKey = ConvertKeyToVariantCallback(key); var self = (godot_dictionary)_underlyingDict.NativeValue; bool found = NativeFuncs.godotsharp_dictionary_try_get_value(ref self, variantKey, out godot_variant retValue).ToBool(); using (retValue) - value = found ? _convertValueToManagedCallback(retValue) : default; + value = found ? ConvertValueToManagedCallback(retValue) : default; return found; } @@ -625,7 +637,7 @@ namespace Godot.Collections unsafe bool ICollection>.Contains(KeyValuePair item) { - using var variantKey = _convertKeyToVariantCallback(item.Key); + using var variantKey = ConvertKeyToVariantCallback(item.Key); var self = (godot_dictionary)_underlyingDict.NativeValue; bool found = NativeFuncs.godotsharp_dictionary_try_get_value(ref self, variantKey, out godot_variant retValue).ToBool(); @@ -635,7 +647,7 @@ namespace Godot.Collections if (!found) return false; - using var variantValue = _convertValueToVariantCallback(item.Value); + using var variantValue = ConvertValueToVariantCallback(item.Value); return NativeFuncs.godotsharp_variant_equals(variantValue, retValue).ToBool(); } } @@ -670,7 +682,7 @@ namespace Godot.Collections unsafe bool ICollection>.Remove(KeyValuePair item) { - using var variantKey = _convertKeyToVariantCallback(item.Key); + using var variantKey = ConvertKeyToVariantCallback(item.Key); var self = (godot_dictionary)_underlyingDict.NativeValue; bool found = NativeFuncs.godotsharp_dictionary_try_get_value(ref self, variantKey, out godot_variant retValue).ToBool(); @@ -680,7 +692,7 @@ namespace Godot.Collections if (!found) return false; - using var variantValue = _convertValueToVariantCallback(item.Value); + using var variantValue = ConvertValueToVariantCallback(item.Value); if (NativeFuncs.godotsharp_variant_equals(variantValue, retValue).ToBool()) { return NativeFuncs.godotsharp_dictionary_remove_key( @@ -717,6 +729,7 @@ namespace Godot.Collections public static implicit operator Variant(Dictionary from) => Variant.CreateFrom(from); [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static explicit operator Dictionary(Variant from) => from.AsGodotDictionary(); + public static explicit operator Dictionary(Variant from) => + from.AsGodotDictionary(); } } diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/NativeInterop/VariantConversionCallbacks.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/NativeInterop/VariantConversionCallbacks.cs index 2707cb9bd2..4b3db0c01a 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/NativeInterop/VariantConversionCallbacks.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/NativeInterop/VariantConversionCallbacks.cs @@ -1,11 +1,15 @@ using System; using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; namespace Godot.NativeInterop; // TODO: Change VariantConversionCallbacks. Store the callback in a static field for quick repeated access, instead of checking every time. internal static unsafe class VariantConversionCallbacks { + internal static System.Collections.Generic.Dictionary + GenericConversionCallbacks = new(); + [SuppressMessage("ReSharper", "RedundantNameQualifier")] internal static delegate* GetToVariantCallback() { @@ -503,6 +507,26 @@ internal static unsafe class VariantConversionCallbacks &FromVariant; } + // TODO: + // IsGenericType and GetGenericTypeDefinition don't work in NativeAOT's reflection-free mode. + // We could make the Godot collections implement an interface and use IsAssignableFrom instead. + // Or we could just skip the check and always look for a conversion callback for the type. + if (typeOfT.IsGenericType) + { + var genericTypeDef = typeOfT.GetGenericTypeDefinition(); + + if (genericTypeDef == typeof(Godot.Collections.Dictionary<,>) || + genericTypeDef == typeof(Godot.Collections.Array<>)) + { + RuntimeHelpers.RunClassConstructor(typeOfT.TypeHandle); + + if (GenericConversionCallbacks.TryGetValue(typeOfT, out var genericConversion)) + { + return (delegate*)genericConversion.ToVariant; + } + } + } + return null; } @@ -1006,6 +1030,26 @@ internal static unsafe class VariantConversionCallbacks &ToVariant; } + // TODO: + // IsGenericType and GetGenericTypeDefinition don't work in NativeAOT's reflection-free mode. + // We could make the Godot collections implement an interface and use IsAssignableFrom instead. + // Or we could just skip the check and always look for a conversion callback for the type. + if (typeOfT.IsGenericType) + { + var genericTypeDef = typeOfT.GetGenericTypeDefinition(); + + if (genericTypeDef == typeof(Godot.Collections.Dictionary<,>) || + genericTypeDef == typeof(Godot.Collections.Array<>)) + { + RuntimeHelpers.RunClassConstructor(typeOfT.TypeHandle); + + if (GenericConversionCallbacks.TryGetValue(typeOfT, out var genericConversion)) + { + return (delegate*)genericConversion.FromVariant; + } + } + } + // ReSharper restore RedundantCast return null; -- cgit v1.2.3