godot icon indicating copy to clipboard operation
godot copied to clipboard

[.Net / GDScript Interop] Cannot Call GDScript-Lambda-Backed `Callable` in C#

Open Delsin-Yu opened this issue 1 year ago • 3 comments

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

  1. Create a C# Godot Project.
  2. Create main.gd.
  3. Create a scene, attach the main.gd to the root node, and save it to a file.
  4. Create Helper.cs.
  5. 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();
}

Delsin-Yu avatar Sep 23 '24 11:09 Delsin-Yu

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 avatar Sep 23 '24 11:09 dalexeev

@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()

Delsin-Yu avatar Sep 23 '24 11:09 Delsin-Yu

GodotSharp does not yet support some CallableCustom, such as GDScriptLambdaCallable, CallableCustomBind/Unbind.

zaevi avatar Sep 23 '24 16:09 zaevi

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.

shabbywu avatar Sep 25 '24 05:09 shabbywu

A tiny GDExtension to make csharp can work well with CallableCustom.

have tested in windows.

shabbywu avatar Sep 25 '24 14:09 shabbywu

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)

MikeSchulze avatar Apr 02 '25 20:04 MikeSchulze

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

MikeSchulze avatar Apr 04 '25 16:04 MikeSchulze

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.

Delsin-Yu avatar Apr 04 '25 16:04 Delsin-Yu