2022-07-28 17:41:47 +02:00
|
|
|
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 ScriptSignalsGenerator : ISourceGenerator
|
|
|
|
{
|
|
|
|
public void Initialize(GeneratorInitializationContext context)
|
|
|
|
{
|
|
|
|
}
|
|
|
|
|
|
|
|
public void Execute(GeneratorExecutionContext context)
|
|
|
|
{
|
2023-01-08 02:04:15 +01:00
|
|
|
if (context.IsGodotSourceGeneratorDisabled("ScriptSignals"))
|
2022-07-28 17:41:47 +02:00
|
|
|
return;
|
|
|
|
|
|
|
|
INamedTypeSymbol[] godotClasses = context
|
|
|
|
.Compilation.SyntaxTrees
|
|
|
|
.SelectMany(tree =>
|
|
|
|
tree.GetRoot().DescendantNodes()
|
|
|
|
.OfType<ClassDeclarationSyntax>()
|
|
|
|
.SelectGodotScriptClasses(context.Compilation)
|
|
|
|
// Report and skip non-partial classes
|
|
|
|
.Where(x =>
|
|
|
|
{
|
|
|
|
if (x.cds.IsPartial())
|
|
|
|
{
|
2024-01-16 15:30:45 +01:00
|
|
|
if (x.cds.IsNested() && !x.cds.AreAllOuterTypesPartial(out _))
|
2022-07-28 17:41:47 +02:00
|
|
|
{
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
return false;
|
|
|
|
})
|
|
|
|
.Select(x => x.symbol)
|
|
|
|
)
|
|
|
|
.Distinct<INamedTypeSymbol>(SymbolEqualityComparer.Default)
|
|
|
|
.ToArray();
|
|
|
|
|
|
|
|
if (godotClasses.Length > 0)
|
|
|
|
{
|
2022-08-15 05:57:52 +02:00
|
|
|
var typeCache = new MarshalUtils.TypeCache(context.Compilation);
|
2022-07-28 17:41:47 +02:00
|
|
|
|
|
|
|
foreach (var godotClass in godotClasses)
|
|
|
|
{
|
|
|
|
VisitGodotScriptClass(context, typeCache, godotClass);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
internal static string SignalDelegateSuffix = "EventHandler";
|
|
|
|
|
|
|
|
private static void VisitGodotScriptClass(
|
|
|
|
GeneratorExecutionContext context,
|
|
|
|
MarshalUtils.TypeCache typeCache,
|
|
|
|
INamedTypeSymbol symbol
|
|
|
|
)
|
|
|
|
{
|
|
|
|
INamespaceSymbol namespaceSymbol = symbol.ContainingNamespace;
|
|
|
|
string classNs = namespaceSymbol != null && !namespaceSymbol.IsGlobalNamespace ?
|
2022-11-24 01:04:15 +01:00
|
|
|
namespaceSymbol.FullQualifiedNameOmitGlobal() :
|
2022-07-28 17:41:47 +02:00
|
|
|
string.Empty;
|
|
|
|
bool hasNamespace = classNs.Length != 0;
|
|
|
|
|
|
|
|
bool isInnerClass = symbol.ContainingType != null;
|
|
|
|
|
2022-11-24 01:04:15 +01:00
|
|
|
string uniqueHint = symbol.FullQualifiedNameOmitGlobal().SanitizeQualifiedNameForUniqueHint()
|
2022-10-22 23:13:52 +02:00
|
|
|
+ "_ScriptSignals.generated";
|
2022-07-28 17:41:47 +02:00
|
|
|
|
|
|
|
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;
|
2023-10-18 03:25:24 +02:00
|
|
|
AppendPartialContainingTypeDeclarations(containingType);
|
2022-07-28 17:41:47 +02:00
|
|
|
|
2023-10-18 03:25:24 +02:00
|
|
|
void AppendPartialContainingTypeDeclarations(INamedTypeSymbol? containingType)
|
2022-07-28 17:41:47 +02:00
|
|
|
{
|
2023-10-18 03:25:24 +02:00
|
|
|
if (containingType == null)
|
|
|
|
return;
|
|
|
|
|
|
|
|
AppendPartialContainingTypeDeclarations(containingType.ContainingType);
|
|
|
|
|
2022-07-28 17:41:47 +02:00
|
|
|
source.Append("partial ");
|
|
|
|
source.Append(containingType.GetDeclarationKeyword());
|
|
|
|
source.Append(" ");
|
|
|
|
source.Append(containingType.NameWithTypeParameters());
|
|
|
|
source.Append("\n{\n");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
source.Append("partial class ");
|
|
|
|
source.Append(symbol.NameWithTypeParameters());
|
|
|
|
source.Append("\n{\n");
|
|
|
|
|
|
|
|
var members = symbol.GetMembers();
|
|
|
|
|
|
|
|
var signalDelegateSymbols = members
|
|
|
|
.Where(s => s.Kind == SymbolKind.NamedType)
|
|
|
|
.Cast<INamedTypeSymbol>()
|
|
|
|
.Where(namedTypeSymbol => namedTypeSymbol.TypeKind == TypeKind.Delegate)
|
|
|
|
.Where(s => s.GetAttributes()
|
|
|
|
.Any(a => a.AttributeClass?.IsGodotSignalAttribute() ?? false));
|
|
|
|
|
|
|
|
List<GodotSignalDelegateData> godotSignalDelegates = new();
|
|
|
|
|
|
|
|
foreach (var signalDelegateSymbol in signalDelegateSymbols)
|
|
|
|
{
|
|
|
|
if (!signalDelegateSymbol.Name.EndsWith(SignalDelegateSuffix))
|
|
|
|
{
|
Clean diagnostic rules
Move the following diagnostics into static readonly fields: GD0101, GD0102, GD0103, GD0104, GD0105, GD0106, GD0107, GD0201, GD0202, GD0203, GD0301, GD0302, GD0303, GD0401, GD0402.
To be more consistent, the titles for the following diagnostics were modified: GD0101, GD0105, GD0106, GD0302, GD0303, GD0401, GD0402. A subsequent update of the documentation repo is needed.
Tests for the following diagnostics were created: GD0201, GD0202, GD0203.
2024-02-17 21:12:06 +01:00
|
|
|
context.ReportDiagnostic(Diagnostic.Create(
|
|
|
|
Common.SignalDelegateMissingSuffixRule,
|
|
|
|
signalDelegateSymbol.Locations.FirstLocationWithSourceTreeOrDefault(),
|
|
|
|
signalDelegateSymbol.ToDisplayString()
|
|
|
|
));
|
2022-07-28 17:41:47 +02:00
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
string signalName = signalDelegateSymbol.Name;
|
|
|
|
signalName = signalName.Substring(0, signalName.Length - SignalDelegateSuffix.Length);
|
|
|
|
|
|
|
|
var invokeMethodData = signalDelegateSymbol
|
|
|
|
.DelegateInvokeMethod?.HasGodotCompatibleSignature(typeCache);
|
|
|
|
|
|
|
|
if (invokeMethodData == null)
|
|
|
|
{
|
2022-08-13 06:21:27 +02:00
|
|
|
if (signalDelegateSymbol.DelegateInvokeMethod is IMethodSymbol methodSymbol)
|
|
|
|
{
|
|
|
|
foreach (var parameter in methodSymbol.Parameters)
|
|
|
|
{
|
|
|
|
if (parameter.RefKind != RefKind.None)
|
|
|
|
{
|
Clean diagnostic rules
Move the following diagnostics into static readonly fields: GD0101, GD0102, GD0103, GD0104, GD0105, GD0106, GD0107, GD0201, GD0202, GD0203, GD0301, GD0302, GD0303, GD0401, GD0402.
To be more consistent, the titles for the following diagnostics were modified: GD0101, GD0105, GD0106, GD0302, GD0303, GD0401, GD0402. A subsequent update of the documentation repo is needed.
Tests for the following diagnostics were created: GD0201, GD0202, GD0203.
2024-02-17 21:12:06 +01:00
|
|
|
context.ReportDiagnostic(Diagnostic.Create(
|
|
|
|
Common.SignalParameterTypeNotSupportedRule,
|
|
|
|
parameter.Locations.FirstLocationWithSourceTreeOrDefault(),
|
|
|
|
parameter.ToDisplayString()
|
|
|
|
));
|
2022-08-13 06:21:27 +02:00
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
var marshalType = MarshalUtils.ConvertManagedTypeToMarshalType(parameter.Type, typeCache);
|
|
|
|
if (marshalType == null)
|
|
|
|
{
|
Clean diagnostic rules
Move the following diagnostics into static readonly fields: GD0101, GD0102, GD0103, GD0104, GD0105, GD0106, GD0107, GD0201, GD0202, GD0203, GD0301, GD0302, GD0303, GD0401, GD0402.
To be more consistent, the titles for the following diagnostics were modified: GD0101, GD0105, GD0106, GD0302, GD0303, GD0401, GD0402. A subsequent update of the documentation repo is needed.
Tests for the following diagnostics were created: GD0201, GD0202, GD0203.
2024-02-17 21:12:06 +01:00
|
|
|
context.ReportDiagnostic(Diagnostic.Create(
|
|
|
|
Common.SignalParameterTypeNotSupportedRule,
|
|
|
|
parameter.Locations.FirstLocationWithSourceTreeOrDefault(),
|
|
|
|
parameter.ToDisplayString()
|
|
|
|
));
|
2022-08-13 06:21:27 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!methodSymbol.ReturnsVoid)
|
|
|
|
{
|
Clean diagnostic rules
Move the following diagnostics into static readonly fields: GD0101, GD0102, GD0103, GD0104, GD0105, GD0106, GD0107, GD0201, GD0202, GD0203, GD0301, GD0302, GD0303, GD0401, GD0402.
To be more consistent, the titles for the following diagnostics were modified: GD0101, GD0105, GD0106, GD0302, GD0303, GD0401, GD0402. A subsequent update of the documentation repo is needed.
Tests for the following diagnostics were created: GD0201, GD0202, GD0203.
2024-02-17 21:12:06 +01:00
|
|
|
context.ReportDiagnostic(Diagnostic.Create(
|
|
|
|
Common.SignalDelegateSignatureMustReturnVoidRule,
|
|
|
|
signalDelegateSymbol.Locations.FirstLocationWithSourceTreeOrDefault(),
|
|
|
|
signalDelegateSymbol.ToDisplayString()
|
|
|
|
));
|
2022-08-13 06:21:27 +02:00
|
|
|
}
|
|
|
|
}
|
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<int, string>)delegateObj)(
VariantConversionCallbacks.GetToManagedCallback<int>()(args[0])
);
ret = VariantConversionCallbacks.GetToVariantCallback<string>()(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.
2022-10-28 22:59:13 +02:00
|
|
|
|
2022-07-28 17:41:47 +02:00
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
godotSignalDelegates.Add(new(signalName, signalDelegateSymbol, invokeMethodData.Value));
|
|
|
|
}
|
|
|
|
|
2022-09-06 14:43:40 +02:00
|
|
|
source.Append("#pragma warning disable CS0109 // Disable warning about redundant 'new' keyword\n");
|
|
|
|
|
2023-07-09 14:14:36 +02:00
|
|
|
source.Append(" /// <summary>\n")
|
|
|
|
.Append(" /// Cached StringNames for the signals contained in this class, for fast lookup.\n")
|
|
|
|
.Append(" /// </summary>\n");
|
|
|
|
|
2022-12-01 01:45:11 +01:00
|
|
|
source.Append(
|
2024-01-24 00:49:34 +01:00
|
|
|
$" public new class SignalName : {symbol.BaseType!.FullQualifiedNameIncludeGlobal()}.SignalName {{\n");
|
2022-07-28 17:41:47 +02:00
|
|
|
|
|
|
|
// Generate cached StringNames for methods and properties, for fast lookup
|
|
|
|
|
|
|
|
foreach (var signalDelegate in godotSignalDelegates)
|
|
|
|
{
|
|
|
|
string signalName = signalDelegate.Name;
|
2023-07-09 14:14:36 +02:00
|
|
|
|
|
|
|
source.Append(" /// <summary>\n")
|
|
|
|
.Append(" /// Cached name for the '")
|
|
|
|
.Append(signalName)
|
|
|
|
.Append("' signal.\n")
|
|
|
|
.Append(" /// </summary>\n");
|
|
|
|
|
2024-06-15 18:25:37 +02:00
|
|
|
source.Append(" public new static readonly global::Godot.StringName @");
|
2022-07-28 17:41:47 +02:00
|
|
|
source.Append(signalName);
|
|
|
|
source.Append(" = \"");
|
|
|
|
source.Append(signalName);
|
|
|
|
source.Append("\";\n");
|
|
|
|
}
|
|
|
|
|
|
|
|
source.Append(" }\n"); // class GodotInternal
|
|
|
|
|
|
|
|
// Generate GetGodotSignalList
|
|
|
|
|
|
|
|
if (godotSignalDelegates.Count > 0)
|
|
|
|
{
|
2024-02-19 22:15:37 +01:00
|
|
|
const string ListType = "global::System.Collections.Generic.List<global::Godot.Bridge.MethodInfo>";
|
2022-07-28 17:41:47 +02:00
|
|
|
|
2023-07-09 14:14:36 +02:00
|
|
|
source.Append(" /// <summary>\n")
|
|
|
|
.Append(" /// Get the signal information for all the signals declared in this class.\n")
|
|
|
|
.Append(" /// This method is used by Godot to register the available signals in the editor.\n")
|
|
|
|
.Append(" /// Do not call this method.\n")
|
|
|
|
.Append(" /// </summary>\n");
|
|
|
|
|
|
|
|
source.Append(" [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]\n");
|
|
|
|
|
2022-07-28 17:41:47 +02:00
|
|
|
source.Append(" internal new static ")
|
2024-02-19 22:15:37 +01:00
|
|
|
.Append(ListType)
|
2022-07-28 17:41:47 +02:00
|
|
|
.Append(" GetGodotSignalList()\n {\n");
|
|
|
|
|
|
|
|
source.Append(" var signals = new ")
|
2024-02-19 22:15:37 +01:00
|
|
|
.Append(ListType)
|
2022-07-28 17:41:47 +02:00
|
|
|
.Append("(")
|
|
|
|
.Append(godotSignalDelegates.Count)
|
|
|
|
.Append(");\n");
|
|
|
|
|
|
|
|
foreach (var signalDelegateData in godotSignalDelegates)
|
|
|
|
{
|
|
|
|
var methodInfo = DetermineMethodInfo(signalDelegateData);
|
|
|
|
AppendMethodInfo(source, methodInfo);
|
|
|
|
}
|
|
|
|
|
|
|
|
source.Append(" return signals;\n");
|
|
|
|
source.Append(" }\n");
|
|
|
|
}
|
|
|
|
|
2022-09-06 14:43:40 +02:00
|
|
|
source.Append("#pragma warning restore CS0109\n");
|
|
|
|
|
2022-07-28 17:41:47 +02:00
|
|
|
// Generate signal event
|
|
|
|
|
|
|
|
foreach (var signalDelegate in godotSignalDelegates)
|
|
|
|
{
|
|
|
|
string signalName = signalDelegate.Name;
|
|
|
|
|
|
|
|
// TODO: Hide backing event from code-completion and debugger
|
|
|
|
// The reason we have a backing field is to hide the invoke method from the event,
|
|
|
|
// as it doesn't emit the signal, only the event delegates. This can confuse users.
|
|
|
|
// Maybe we should directly connect the delegates, as we do with native signals?
|
|
|
|
source.Append(" private ")
|
2022-11-24 01:04:15 +01:00
|
|
|
.Append(signalDelegate.DelegateSymbol.FullQualifiedNameIncludeGlobal())
|
2022-07-28 17:41:47 +02:00
|
|
|
.Append(" backing_")
|
|
|
|
.Append(signalName)
|
|
|
|
.Append(";\n");
|
|
|
|
|
2022-12-01 01:45:11 +01:00
|
|
|
source.Append(
|
|
|
|
$" /// <inheritdoc cref=\"{signalDelegate.DelegateSymbol.FullQualifiedNameIncludeGlobal()}\"/>\n");
|
2022-09-18 17:08:22 +02:00
|
|
|
|
2022-11-03 20:10:11 +01:00
|
|
|
source.Append($" {signalDelegate.DelegateSymbol.GetAccessibilityKeyword()} event ")
|
2022-11-24 01:04:15 +01:00
|
|
|
.Append(signalDelegate.DelegateSymbol.FullQualifiedNameIncludeGlobal())
|
2024-06-15 18:25:37 +02:00
|
|
|
.Append(" @")
|
2022-07-28 17:41:47 +02:00
|
|
|
.Append(signalName)
|
|
|
|
.Append(" {\n")
|
|
|
|
.Append(" add => backing_")
|
|
|
|
.Append(signalName)
|
|
|
|
.Append(" += value;\n")
|
|
|
|
.Append(" remove => backing_")
|
|
|
|
.Append(signalName)
|
|
|
|
.Append(" -= value;\n")
|
|
|
|
.Append("}\n");
|
2022-11-03 20:10:33 +01:00
|
|
|
|
|
|
|
// Generate On{EventName} method to raise the event
|
|
|
|
|
|
|
|
var invokeMethodSymbol = signalDelegate.InvokeMethodData.Method;
|
|
|
|
int paramCount = invokeMethodSymbol.Parameters.Length;
|
|
|
|
|
|
|
|
string raiseMethodModifiers = signalDelegate.DelegateSymbol.ContainingType.IsSealed ?
|
|
|
|
"private" :
|
|
|
|
"protected";
|
|
|
|
|
|
|
|
source.Append($" {raiseMethodModifiers} void On{signalName}(");
|
|
|
|
for (int i = 0; i < paramCount; i++)
|
|
|
|
{
|
|
|
|
var paramSymbol = invokeMethodSymbol.Parameters[i];
|
|
|
|
source.Append($"{paramSymbol.Type.FullQualifiedNameIncludeGlobal()} {paramSymbol.Name}");
|
|
|
|
if (i < paramCount - 1)
|
|
|
|
{
|
|
|
|
source.Append(", ");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
source.Append(")\n");
|
|
|
|
source.Append(" {\n");
|
|
|
|
source.Append($" EmitSignal(SignalName.{signalName}");
|
|
|
|
foreach (var paramSymbol in invokeMethodSymbol.Parameters)
|
|
|
|
{
|
|
|
|
// Enums must be converted to the underlying type before they can be implicitly converted to Variant
|
|
|
|
if (paramSymbol.Type.TypeKind == TypeKind.Enum)
|
|
|
|
{
|
|
|
|
var underlyingType = ((INamedTypeSymbol)paramSymbol.Type).EnumUnderlyingType;
|
|
|
|
source.Append($", ({underlyingType.FullQualifiedNameIncludeGlobal()}){paramSymbol.Name}");
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
source.Append($", {paramSymbol.Name}");
|
|
|
|
}
|
|
|
|
source.Append(");\n");
|
|
|
|
source.Append(" }\n");
|
2022-07-28 17:41:47 +02:00
|
|
|
}
|
|
|
|
|
2022-07-28 17:41:48 +02:00
|
|
|
// Generate RaiseGodotClassSignalCallbacks
|
|
|
|
|
|
|
|
if (godotSignalDelegates.Count > 0)
|
|
|
|
{
|
2023-07-09 14:14:36 +02:00
|
|
|
source.Append(" /// <inheritdoc/>\n");
|
|
|
|
source.Append(" [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]\n");
|
2022-07-28 17:41:48 +02:00
|
|
|
source.Append(
|
|
|
|
" protected override void RaiseGodotClassSignalCallbacks(in godot_string_name signal, ");
|
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<int, string>)delegateObj)(
VariantConversionCallbacks.GetToManagedCallback<int>()(args[0])
);
ret = VariantConversionCallbacks.GetToVariantCallback<string>()(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.
2022-10-28 22:59:13 +02:00
|
|
|
source.Append("NativeVariantPtrArgs args)\n {\n");
|
2022-07-28 17:41:48 +02:00
|
|
|
|
|
|
|
foreach (var signal in godotSignalDelegates)
|
|
|
|
{
|
|
|
|
GenerateSignalEventInvoker(signal, source);
|
|
|
|
}
|
|
|
|
|
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<int, string>)delegateObj)(
VariantConversionCallbacks.GetToManagedCallback<int>()(args[0])
);
ret = VariantConversionCallbacks.GetToVariantCallback<string>()(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.
2022-10-28 22:59:13 +02:00
|
|
|
source.Append(" base.RaiseGodotClassSignalCallbacks(signal, args);\n");
|
2022-07-28 17:41:48 +02:00
|
|
|
|
|
|
|
source.Append(" }\n");
|
|
|
|
}
|
|
|
|
|
2023-01-13 20:59:02 +01:00
|
|
|
// Generate HasGodotClassSignal
|
|
|
|
|
|
|
|
if (godotSignalDelegates.Count > 0)
|
|
|
|
{
|
2023-07-09 14:14:36 +02:00
|
|
|
source.Append(" /// <inheritdoc/>\n");
|
|
|
|
source.Append(" [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]\n");
|
2023-01-13 20:59:02 +01:00
|
|
|
source.Append(
|
|
|
|
" protected override bool HasGodotClassSignal(in godot_string_name signal)\n {\n");
|
|
|
|
|
|
|
|
foreach (var signal in godotSignalDelegates)
|
|
|
|
{
|
2024-05-14 01:51:04 +02:00
|
|
|
GenerateHasSignalEntry(signal.Name, source);
|
2023-01-13 20:59:02 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
source.Append(" return base.HasGodotClassSignal(signal);\n");
|
|
|
|
|
|
|
|
source.Append(" }\n");
|
|
|
|
}
|
|
|
|
|
2022-07-28 17:41:47 +02:00
|
|
|
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)
|
|
|
|
{
|
2024-06-15 18:25:37 +02:00
|
|
|
source.Append(" signals.Add(new(name: SignalName.@")
|
2022-07-28 17:41:47 +02:00
|
|
|
.Append(methodInfo.Name)
|
|
|
|
.Append(", returnVal: ");
|
|
|
|
|
|
|
|
AppendPropertyInfo(source, methodInfo.ReturnVal);
|
|
|
|
|
2022-11-24 01:04:15 +01:00
|
|
|
source.Append(", flags: (global::Godot.MethodFlags)")
|
2022-07-28 17:41:47 +02:00
|
|
|
.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)
|
|
|
|
{
|
2022-11-24 01:04:15 +01:00
|
|
|
source.Append("new(type: (global::Godot.Variant.Type)")
|
2022-07-28 17:41:47 +02:00
|
|
|
.Append((int)propertyInfo.Type)
|
|
|
|
.Append(", name: \"")
|
|
|
|
.Append(propertyInfo.Name)
|
2022-11-24 01:04:15 +01:00
|
|
|
.Append("\", hint: (global::Godot.PropertyHint)")
|
2022-07-28 17:41:47 +02:00
|
|
|
.Append((int)propertyInfo.Hint)
|
|
|
|
.Append(", hintString: \"")
|
|
|
|
.Append(propertyInfo.HintString)
|
2022-11-24 01:04:15 +01:00
|
|
|
.Append("\", usage: (global::Godot.PropertyUsageFlags)")
|
2022-07-28 17:41:47 +02:00
|
|
|
.Append((int)propertyInfo.Usage)
|
|
|
|
.Append(", exported: ")
|
2023-06-16 23:05:11 +02:00
|
|
|
.Append(propertyInfo.Exported ? "true" : "false");
|
|
|
|
if (propertyInfo.ClassName != null)
|
|
|
|
{
|
|
|
|
source.Append(", className: new global::Godot.StringName(\"")
|
|
|
|
.Append(propertyInfo.ClassName)
|
|
|
|
.Append("\")");
|
|
|
|
}
|
|
|
|
source.Append(")");
|
2022-07-28 17:41:47 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
private static MethodInfo DetermineMethodInfo(GodotSignalDelegateData signalDelegateData)
|
|
|
|
{
|
|
|
|
var invokeMethodData = signalDelegateData.InvokeMethodData;
|
|
|
|
|
|
|
|
PropertyInfo returnVal;
|
|
|
|
|
|
|
|
if (invokeMethodData.RetType != null)
|
|
|
|
{
|
2023-06-16 23:05:11 +02:00
|
|
|
returnVal = DeterminePropertyInfo(invokeMethodData.RetType.Value.MarshalType,
|
|
|
|
invokeMethodData.RetType.Value.TypeSymbol,
|
|
|
|
name: string.Empty);
|
2022-07-28 17:41:47 +02:00
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
returnVal = new PropertyInfo(VariantType.Nil, string.Empty, PropertyHint.None,
|
|
|
|
hintString: null, PropertyUsageFlags.Default, exported: false);
|
|
|
|
}
|
|
|
|
|
|
|
|
int paramCount = invokeMethodData.ParamTypes.Length;
|
|
|
|
|
|
|
|
List<PropertyInfo>? arguments;
|
|
|
|
|
|
|
|
if (paramCount > 0)
|
|
|
|
{
|
|
|
|
arguments = new(capacity: paramCount);
|
|
|
|
|
|
|
|
for (int i = 0; i < paramCount; i++)
|
|
|
|
{
|
|
|
|
arguments.Add(DeterminePropertyInfo(invokeMethodData.ParamTypes[i],
|
2023-06-16 23:05:11 +02:00
|
|
|
invokeMethodData.Method.Parameters[i].Type,
|
2022-07-28 17:41:47 +02:00
|
|
|
name: invokeMethodData.Method.Parameters[i].Name));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
arguments = null;
|
|
|
|
}
|
|
|
|
|
|
|
|
return new MethodInfo(signalDelegateData.Name, returnVal, MethodFlags.Default, arguments,
|
|
|
|
defaultArguments: null);
|
|
|
|
}
|
|
|
|
|
2023-06-16 23:05:11 +02:00
|
|
|
private static PropertyInfo DeterminePropertyInfo(MarshalType marshalType, ITypeSymbol typeSymbol, string name)
|
2022-07-28 17:41:47 +02:00
|
|
|
{
|
|
|
|
var memberVariantType = MarshalUtils.ConvertMarshalTypeToVariantType(marshalType)!.Value;
|
|
|
|
|
|
|
|
var propUsage = PropertyUsageFlags.Default;
|
|
|
|
|
|
|
|
if (memberVariantType == VariantType.Nil)
|
|
|
|
propUsage |= PropertyUsageFlags.NilIsVariant;
|
|
|
|
|
2023-06-16 23:05:11 +02:00
|
|
|
string? className = null;
|
|
|
|
if (memberVariantType == VariantType.Object && typeSymbol is INamedTypeSymbol namedTypeSymbol)
|
|
|
|
{
|
|
|
|
className = namedTypeSymbol.GetGodotScriptNativeClassName();
|
|
|
|
}
|
|
|
|
|
2022-07-28 17:41:47 +02:00
|
|
|
return new PropertyInfo(memberVariantType, name,
|
2023-06-16 23:05:11 +02:00
|
|
|
PropertyHint.None, string.Empty, propUsage, className, exported: false);
|
2022-07-28 17:41:47 +02:00
|
|
|
}
|
2022-07-28 17:41:48 +02:00
|
|
|
|
2023-01-13 20:59:02 +01:00
|
|
|
private static void GenerateHasSignalEntry(
|
|
|
|
string signalName,
|
2024-05-14 01:51:04 +02:00
|
|
|
StringBuilder source
|
2023-01-13 20:59:02 +01:00
|
|
|
)
|
|
|
|
{
|
|
|
|
source.Append(" ");
|
2024-06-15 18:25:37 +02:00
|
|
|
source.Append("if (signal == SignalName.@");
|
2023-01-13 20:59:02 +01:00
|
|
|
source.Append(signalName);
|
|
|
|
source.Append(") {\n return true;\n }\n");
|
|
|
|
}
|
|
|
|
|
2022-07-28 17:41:48 +02:00
|
|
|
private static void GenerateSignalEventInvoker(
|
|
|
|
GodotSignalDelegateData signal,
|
|
|
|
StringBuilder source
|
|
|
|
)
|
|
|
|
{
|
|
|
|
string signalName = signal.Name;
|
|
|
|
var invokeMethodData = signal.InvokeMethodData;
|
|
|
|
|
2024-06-15 18:25:37 +02:00
|
|
|
source.Append(" if (signal == SignalName.@");
|
2022-07-28 17:41:48 +02:00
|
|
|
source.Append(signalName);
|
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<int, string>)delegateObj)(
VariantConversionCallbacks.GetToManagedCallback<int>()(args[0])
);
ret = VariantConversionCallbacks.GetToVariantCallback<string>()(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.
2022-10-28 22:59:13 +02:00
|
|
|
source.Append(" && args.Count == ");
|
2022-07-28 17:41:48 +02:00
|
|
|
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(", ");
|
|
|
|
|
2022-07-28 17:41:50 +02:00
|
|
|
source.AppendNativeVariantToManagedExpr(string.Concat("args[", i.ToString(), "]"),
|
2022-07-28 17:41:48 +02:00
|
|
|
invokeMethodData.ParamTypeSymbols[i], invokeMethodData.ParamTypes[i]);
|
|
|
|
}
|
|
|
|
|
|
|
|
source.Append(");\n");
|
|
|
|
|
|
|
|
source.Append(" return;\n");
|
|
|
|
|
|
|
|
source.Append(" }\n");
|
|
|
|
}
|
2022-07-28 17:41:47 +02:00
|
|
|
}
|
|
|
|
}
|