[.Net / GDScript Interop] Cannot Call GDScript-Lambda-Backed `Callable` in C#
Tested versions
v4.3.stable.mono.official [77dcf97d8]
System information
Godot v4.3.stable.mono - Windows 10.0.17763 - Vulkan (Forward+) - dedicated NVIDIA GeForce RTX 4060 Ti (NVIDIA; 32.0.15.6109) - AMD Ryzen 9 5900X 12-Core Processor (24 Threads)
Issue description
This is a niche use case, but we should document it if it's not supported.
In C# script, when trying to Call() a Callable that is backed by a GDScript Lambda Expression, Godot will produce the following error instead of actually making the call.
E 0:00:00:0894 main.gd:4 @ _ready(): Attempt to call callable 'null::null' on a null instance.
<C# Source> /root/godot/modules/mono/glue/GodotSharp/GodotSharp/Core/NativeInterop/ExceptionUtils.cs:160 @ void Godot.NativeInterop.ExceptionUtils.DebugCheckCallError(Godot.NativeInterop.godot_callable&, Godot.NativeInterop.godot_variant**, int, Godot.NativeInterop.godot_variant_call_error)
<Stack Trace> main.gd:4 @ _ready()
Steps to reproduce
- Create a C# Godot Project.
- Create
main.gd. - Create a scene, attach the
main.gdto the root node, and save it to a file. - Create
Helper.cs. - Run the project.
Minimal reproduction project (MRP)
main.gd
extends Node
func _ready():
Helper.new().CallCallable(func(): print("Hello World"));
Helper.cs
using Godot;
[GlobalClass]
public partial class Helper : Node
{
public void CallCallable(Callable callable) => callable.Call();
}
Does the error occur if you store the lambda callable in a local or class variable? Maybe GDScript/C#/core loses the last reference and therefore frees the lambda before the callable is called.
@dalexeev
No, neither works.
extends Node
var lambda_field := func(): print("Hello World");
func _ready():
var helper := Helper.new();
var lambda_local := func(): print("Hello World");
helper.CallCallable(lambda_local);
helper.CallCallable(lambda_field);
E 0:00:00:0814 main.gd:8 @ _ready(): Attempt to call callable 'null::null' on a null instance.
<C# Source> /root/godot/modules/mono/glue/GodotSharp/GodotSharp/Core/NativeInterop/ExceptionUtils.cs:160 @ void Godot.NativeInterop.ExceptionUtils.DebugCheckCallError(Godot.NativeInterop.godot_callable&, Godot.NativeInterop.godot_variant**, int, Godot.NativeInterop.godot_variant_call_error)
<Stack Trace> main.gd:8 @ _ready()
E 0:00:00:0815 main.gd:9 @ _ready(): Attempt to call callable 'null::null' on a null instance.
<C# Source> /root/godot/modules/mono/glue/GodotSharp/GodotSharp/Core/NativeInterop/ExceptionUtils.cs:160 @ void Godot.NativeInterop.ExceptionUtils.DebugCheckCallError(Godot.NativeInterop.godot_callable&, Godot.NativeInterop.godot_variant**, int, Godot.NativeInterop.godot_variant_call_error)
<Stack Trace> main.gd:9 @ _ready()
GodotSharp does not yet support some CallableCustom, such as GDScriptLambdaCallable, CallableCustomBind/Unbind.
same error but happen at calling the CallableCustom return from GDExtension.
By the way, if i use Variant but not Callable to define the variable, csharp can call the CallableCustom::get_as_text correctly when i print the callable to screen.
So i guess the problem happen at csharp Callable stub.
I encountered the same problem today, lambda callable is unfortunately not supported, really a pity ;( So the workaround is to create an additional object to reference the method as callable.
class EventHandler:
func PublishEvent(event: Dictionary) -> void:
prints(event)
static func execute(tests: Array[GdUnitTestCase]) -> void:
var tests_as_dict: Array[Dictionary] = Array(tests.map(GdUnitTestCase.to_dict), TYPE_DICTIONARY, "", null)
# using lamba not works to call the c# method
var listener := func PublishEvent(event: Dictionary) -> void:
prints(event)
# instead we need an extra object
var e := EventHandler.new()
listener = e.PublishEvent
# call the c# method
net_api.call("ExecuteAndWait", tests_as_dict, listener)
There are more issues, using Callable as argument to call a C# method from a GDScript results into orphan nodes.
class EventHandler extends RefCounted:
func PublishEvent(event: Dictionary) -> void:
pass
static func execute() -> void:
var e := EventHandler.new()
api_instance().call("CallWithCallable", e.PublishEvent)
public void CallWithCallable(Callable cb)
{
}
e.g. the execute() is called two times produces 2 Orphan StringNames where is the callable method name
Orphan StringName: PublishEvent (static: 0, total: 2)
StringName: 2 unclaimed string names at exit.
Run tests ends with 0
e.g. the execute() is called two times produces 2 Orphan StringNames where is the callable method name
This might be worth opening a dedicated issue.