Skip to content

Commit

Permalink
C#: Add source generator for signals as events
Browse files Browse the repository at this point in the history
Changed the signal declaration signal to:

```
// The following generates a MySignal event
[Signal] public delegate void MySignalEventHandler(int param);
```
  • Loading branch information
neikeq committed Jul 28, 2022
1 parent 56fd0b8 commit e7d8df7
Show file tree
Hide file tree
Showing 31 changed files with 985 additions and 518 deletions.
112 changes: 73 additions & 39 deletions modules/mono/csharp_script.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ Error CSharpLanguage::execute_file(const String &p_path) {
return OK;
}

extern void *godotsharp_pinvoke_funcs[178];
extern void *godotsharp_pinvoke_funcs[177];
[[maybe_unused]] volatile void **do_not_strip_godotsharp_pinvoke_funcs;
#ifdef TOOLS_ENABLED
extern void *godotsharp_editor_pinvoke_funcs[30];
Expand Down Expand Up @@ -1431,6 +1431,8 @@ void CSharpLanguage::tie_user_managed_to_unmanaged(GCHandleIntPtr p_gchandle_int
CSharpInstance *csharp_instance = CSharpInstance::create_for_managed_type(p_unmanaged, script.ptr(), gchandle);

p_unmanaged->set_script_and_instance(script, csharp_instance);

csharp_instance->connect_event_signals();
}

void CSharpLanguage::tie_managed_to_unmanaged_with_pre_setup(GCHandleIntPtr p_gchandle_intptr, Object *p_unmanaged) {
Expand Down Expand Up @@ -1458,6 +1460,8 @@ void CSharpLanguage::tie_managed_to_unmanaged_with_pre_setup(GCHandleIntPtr p_gc
// instances is a set, so it's safe to insert multiple times (e.g.: from _internal_new_managed)
instance->script->instances.insert(instance->owner);
}

instance->connect_event_signals();
}

CSharpInstance *CSharpInstance::create_for_managed_type(Object *p_owner, CSharpScript *p_script, const MonoGCHandleData &p_gchandle) {
Expand Down Expand Up @@ -1713,13 +1717,22 @@ void CSharpInstance::mono_object_disposed_baseref(GCHandleIntPtr p_gchandle_to_f
}
}

void CSharpInstance::connect_event_signal(const StringName &p_event_signal) {
// TODO: Use pooling for ManagedCallable instances.
EventSignalCallable *event_signal_callable = memnew(EventSignalCallable(owner, p_event_signal));
void CSharpInstance::connect_event_signals() {
CSharpScript *top = script.ptr();
while (top != nullptr) {
for (CSharpScript::EventSignalInfo &signal : top->get_script_event_signals()) {
String signal_name = signal.name;

// TODO: Use pooling for ManagedCallable instances.
EventSignalCallable *event_signal_callable = memnew(EventSignalCallable(owner, signal_name));

Callable callable(event_signal_callable);
connected_event_signals.push_back(callable);
owner->connect(signal_name, callable);
}

Callable callable(event_signal_callable);
connected_event_signals.push_back(callable);
owner->connect(p_event_signal, callable);
top = top->base_script.ptr();
}
}

void CSharpInstance::disconnect_event_signals() {
Expand Down Expand Up @@ -2097,17 +2110,25 @@ void CSharpScript::reload_registered_script(Ref<CSharpScript> p_script) {
// Extract information about the script using the mono class.
void CSharpScript::update_script_class_info(Ref<CSharpScript> p_script) {
bool tool = false;

Dictionary rpc_functions_dict;
// Destructor won't be called from C#, and I don't want to include the GDNative header
// only for this, so need to call the destructor manually before passing this to C#.
rpc_functions_dict.~Dictionary();

Dictionary signals_dict;
// Destructor won't be called from C#, and I don't want to include the GDNative header
// only for this, so need to call the destructor manually before passing this to C#.
signals_dict.~Dictionary();

Ref<CSharpScript> base_script;
GDMonoCache::managed_callbacks.ScriptManagerBridge_UpdateScriptClassInfo(
p_script.ptr(), &tool, &rpc_functions_dict, &base_script);
p_script.ptr(), &tool, &rpc_functions_dict, &signals_dict, &base_script);

p_script->tool = tool;

// RPC functions

p_script->rpc_functions.clear();

// Performance is not critical here as this will be replaced with source generators.
Expand All @@ -2128,6 +2149,36 @@ void CSharpScript::update_script_class_info(Ref<CSharpScript> p_script) {
// Sort so we are 100% that they are always the same.
p_script->rpc_functions.sort_custom<Multiplayer::SortRPCConfig>();

// Event signals

// Performance is not critical here as this will be replaced with source generators.

p_script->event_signals.clear();

// Sigh... can't we just have capacity?
p_script->event_signals.resize(signals_dict.size());
int push_index = 0;

for (const Variant *s = signals_dict.next(nullptr); s != nullptr; s = signals_dict.next(s)) {
StringName name = *s;

MethodInfo mi;
mi.name = name;

Array params = signals_dict[*s];

for (int i = 0; i < params.size(); i++) {
Dictionary param = params[i];

Variant::Type param_type = (Variant::Type)(int)param["type"];
PropertyInfo arg_info = PropertyInfo(param_type, (String)param["name"]);
arg_info.usage = (uint32_t)param["usage"];
mi.arguments.push_back(arg_info);
}

p_script->event_signals.set(push_index++, EventSignalInfo{ name, mi });
}

p_script->base_script = base_script;
}

Expand Down Expand Up @@ -2412,48 +2463,31 @@ bool CSharpScript::has_script_signal(const StringName &p_signal) const {
return false;
}

String signal = p_signal;
for (const EventSignalInfo &signal : event_signals) {
if (signal.name == p_signal) {
return true;
}
}

return GDMonoCache::managed_callbacks.ScriptManagerBridge_HasScriptSignal(this, &signal);
return false;
}

void CSharpScript::get_script_signal_list(List<MethodInfo> *r_signals) const {
if (!valid) {
return;
}

// Performance is not critical here as this will be replaced with source generators.

if (!GDMonoCache::godot_api_cache_updated) {
return;
for (const EventSignalInfo &signal : get_script_event_signals()) {
r_signals->push_back(signal.method_info);
}
}

Dictionary signals_dict;
// Destructor won't be called from C#, and I don't want to include the GDNative header
// only for this, so need to call the destructor manually before passing this to C#.
signals_dict.~Dictionary();

GDMonoCache::managed_callbacks.ScriptManagerBridge_GetScriptSignalList(this, &signals_dict);

for (const Variant *s = signals_dict.next(nullptr); s != nullptr; s = signals_dict.next(s)) {
MethodInfo mi;
mi.name = *s;

Array params = signals_dict[*s];

for (int i = 0; i < params.size(); i++) {
Dictionary param = params[i];

Variant::Type param_type = (Variant::Type)(int)param["type"];
PropertyInfo arg_info = PropertyInfo(param_type, (String)param["name"]);
if (param_type == Variant::NIL && (bool)param["nil_is_variant"]) {
arg_info.usage |= PROPERTY_USAGE_NIL_IS_VARIANT;
}
mi.arguments.push_back(arg_info);
}

r_signals->push_back(mi);
Vector<CSharpScript::EventSignalInfo> CSharpScript::get_script_event_signals() const {
if (!valid) {
return Vector<EventSignalInfo>();
}

return event_signals;
}

bool CSharpScript::inherits_script(const Ref<Script> &p_script) const {
Expand Down
11 changes: 10 additions & 1 deletion modules/mono/csharp_script.h
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,13 @@ class CSharpScript : public Script {

Vector<Multiplayer::RPCConfig> rpc_functions;

struct EventSignalInfo {
StringName name; // MethodInfo stores a string...
MethodInfo method_info;
};

Vector<EventSignalInfo> event_signals;

#ifdef TOOLS_ENABLED
List<PropertyInfo> exported_members_cache; // members_cache
Map<StringName, Variant> exported_members_defval_cache; // member_default_values_cache
Expand Down Expand Up @@ -157,6 +164,8 @@ class CSharpScript : public Script {
bool has_script_signal(const StringName &p_signal) const override;
void get_script_signal_list(List<MethodInfo> *r_signals) const override;

Vector<EventSignalInfo> get_script_event_signals() const;

bool get_property_default_value(const StringName &p_property, Variant &r_value) const override;
void get_script_property_list(List<PropertyInfo> *r_list) const override;
void update_exports() override;
Expand Down Expand Up @@ -250,7 +259,7 @@ class CSharpInstance : public ScriptInstance {
*/
void mono_object_disposed_baseref(GCHandleIntPtr p_gchandle_to_free, bool p_is_finalizer, bool &r_delete_owner, bool &r_remove_script_instance);

void connect_event_signal(const StringName &p_event_signal);
void connect_event_signals();
void disconnect_event_signals();

void refcount_incremented() override;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace Godot.SourceGenerators.Sample;

public partial class EventSignals : Godot.Object
{
[Signal]
public delegate void MySignalEventHandler(string str, int num);
}
49 changes: 49 additions & 0 deletions modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/Common.cs
Original file line number Diff line number Diff line change
Expand Up @@ -166,5 +166,54 @@ ISymbol exportedMemberSymbol
location,
location?.SourceTree?.FilePath));
}

public static void ReportSignalDelegateMissingSuffix(
GeneratorExecutionContext context,
INamedTypeSymbol delegateSymbol)
{
var locations = delegateSymbol.Locations;
var location = locations.FirstOrDefault(l => l.SourceTree != null) ?? locations.FirstOrDefault();

string message = "The name of the delegate must end with 'EventHandler': " +
delegateSymbol.ToDisplayString() +
$". Did you mean '{delegateSymbol.Name}EventHandler'?";

string description = $"{message}. Rename the delegate accordingly or remove the '[Signal]' attribute.";

context.ReportDiagnostic(Diagnostic.Create(
new DiagnosticDescriptor(id: "GODOT-G0201",
title: message,
messageFormat: message,
category: "Usage",
DiagnosticSeverity.Error,
isEnabledByDefault: true,
description),
location,
location?.SourceTree?.FilePath));
}

public static void ReportSignalDelegateSignatureNotSupported(
GeneratorExecutionContext context,
INamedTypeSymbol delegateSymbol)
{
var locations = delegateSymbol.Locations;
var location = locations.FirstOrDefault(l => l.SourceTree != null) ?? locations.FirstOrDefault();

string message = "The delegate signature of the signal " +
$"is not supported: '{delegateSymbol.ToDisplayString()}'";

string description = $"{message}. Use supported types only or remove the '[Signal]' attribute.";

context.ReportDiagnostic(Diagnostic.Create(
new DiagnosticDescriptor(id: "GODOT-G0202",
title: message,
messageFormat: message,
category: "Usage",
DiagnosticSeverity.Error,
isEnabledByDefault: true,
description),
location,
location?.SourceTree?.FilePath));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -174,40 +174,54 @@ public static string SanitizeQualifiedNameForUniqueHint(this string qualifiedNam
public static bool IsGodotExportAttribute(this INamedTypeSymbol symbol)
=> symbol.ToString() == GodotClasses.ExportAttr;

public static bool IsGodotSignalAttribute(this INamedTypeSymbol symbol)
=> symbol.ToString() == GodotClasses.SignalAttr;

public static bool IsGodotClassNameAttribute(this INamedTypeSymbol symbol)
=> symbol.ToString() == GodotClasses.GodotClassNameAttr;

public static IEnumerable<GodotMethodData> WhereHasGodotCompatibleSignature(
this IEnumerable<IMethodSymbol> methods,
public static GodotMethodData? HasGodotCompatibleSignature(
this IMethodSymbol method,
MarshalUtils.TypeCache typeCache
)
{
foreach (var method in methods)
{
var retSymbol = method.ReturnType;
var retType = method.ReturnsVoid ?
null :
MarshalUtils.ConvertManagedTypeToMarshalType(method.ReturnType, typeCache);
var retSymbol = method.ReturnType;
var retType = method.ReturnsVoid ?
null :
MarshalUtils.ConvertManagedTypeToMarshalType(method.ReturnType, typeCache);

if (retType == null && !method.ReturnsVoid)
continue;
if (retType == null && !method.ReturnsVoid)
return null;

var parameters = method.Parameters;
var parameters = method.Parameters;

var paramTypes = parameters
// Currently we don't support `ref`, `out`, `in`, `ref readonly` parameters (and we never may)
.Where(p => p.RefKind == RefKind.None)
// Attempt to determine the variant type
.Select(p => MarshalUtils.ConvertManagedTypeToMarshalType(p.Type, typeCache))
// Discard parameter types that couldn't be determined (null entries)
.Where(t => t != null).Cast<MarshalType>().ToImmutableArray();
var paramTypes = parameters
// Currently we don't support `ref`, `out`, `in`, `ref readonly` parameters (and we never may)
.Where(p => p.RefKind == RefKind.None)
// Attempt to determine the variant type
.Select(p => MarshalUtils.ConvertManagedTypeToMarshalType(p.Type, typeCache))
// Discard parameter types that couldn't be determined (null entries)
.Where(t => t != null).Cast<MarshalType>().ToImmutableArray();

// If any parameter type was incompatible, it was discarded so the length won't match
if (parameters.Length > paramTypes.Length)
continue; // Ignore incompatible method
// If any parameter type was incompatible, it was discarded so the length won't match
if (parameters.Length > paramTypes.Length)
return null; // Ignore incompatible method

return new GodotMethodData(method, paramTypes, parameters
.Select(p => p.Type).ToImmutableArray(), retType, retSymbol);
}

public static IEnumerable<GodotMethodData> WhereHasGodotCompatibleSignature(
this IEnumerable<IMethodSymbol> methods,
MarshalUtils.TypeCache typeCache
)
{
foreach (var method in methods)
{
var methodData = HasGodotCompatibleSignature(method, typeCache);

yield return new GodotMethodData(method, paramTypes, parameters
.Select(p => p.Type).ToImmutableArray(), retType, retSymbol);
if (methodData != null)
yield return methodData.Value;
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ public static class GodotClasses
public const string Object = "Godot.Object";
public const string AssemblyHasScriptsAttr = "Godot.AssemblyHasScriptsAttribute";
public const string ExportAttr = "Godot.ExportAttribute";
public const string SignalAttr = "Godot.SignalAttribute";
public const string GodotClassNameAttr = "Godot.GodotClassName";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

namespace Godot.SourceGenerators
{
// TODO: May need to think about compatibility here. Could Godot change these values between minor versions?

internal enum VariantType
{
Nil = 0,
Expand Down Expand Up @@ -126,4 +128,18 @@ internal enum PropertyUsageFlags
DefaultIntl = 71,
NoEditor = 5
}

public enum MethodFlags
{
Normal = 1,
Editor = 2,
Noscript = 4,
Const = 8,
Reverse = 16,
Virtual = 32,
FromScript = 64,
Static = 256,
ObjectCore = 512,
Default = 1
}
}
Loading

0 comments on commit e7d8df7

Please sign in to comment.