3
\$\begingroup\$

In Godot, how can I check whether e.g. Call(...) or Callv(...) succeeded or failed?

By failed I mean due to e.g. missing method, wrong number of arguments, etc., not because of the method itself failing or returning an error. This API returns Variant, not Error and, if I'm not mistaken, it just returns null on failure (and logs an error). How can I at runtime differentiate that from a successful void or null result, "catching" that an error happened?

My question is mainly for C#, but I'd like to know that for GDScript too.

\$\endgroup\$

1 Answer 1

2
\$\begingroup\$

Checking a method signature with reflection

I believe Godot does not have a means to check if a call failed...

But you can check your concerns before making the call.

If you are going to call like this:

obj.call(method_name)

You can check if obj has that method like this:

if obj.has_method(method_name):
    obj.call(method_name)

Checking the number of parameters takes more effort, for example:

if has_method(method_name):
    var parameter_count := 0
    for method in obj.get_method_list():
        if method["name"] != method_name:
            continue

        var parameters:Array = method["args"]
        parameter_count = parameters.size()
        break

    if parameter_count == 3:
        obj.call(method_name, arg1, arg2, arg3)

If you also want to check the types, well:

var method_name := "my_method"
var signature := [TYPE_INT, TYPE_STRING]
var valid := false

for method in obj.get_method_list():
    if method["name"] != method_name:
        continue

    valid = true
    var parameters:Array = method["args"]
    if signature.size() != parameters.size():
        valid = false
        break

    for index in parameters.size():
        if parameters[index]["type"] != signature[index]:
            valid = false
            break

    break

if valid:
    obj.call(method_name, 123, "abc")

Oh, you want to check the class too? Hmm...

var method_name := "my_method"
var signature := [TYPE_INT, TYPE_STRING, "Node"]
var valid := false

for method in obj.get_method_list():
    if method["name"] != method_name:
        continue

    valid = true
    var parameters:Array = method["args"]
    if signature.size() != parameters.size():
        valid = false
        break

    for index in parameters.size():
        var expected = signature[index]
        if typeof(expected) == TYPE_STRING:
            if parameters[index]["class_name"] != expected:
                valid = false
                break

        else:
            if parameters[index]["type"] != expected:
                valid = false
                break

    break

if valid:
    obj.call(method_name, 123, "abc", self)

I presume you might want to encapsulate this for ease of reuse, but I'll leave that to you.


Checking with code generation

When would this be useful?

  • You are writing some generic code, duck typing, etc.

    This is bound to what you write, and to code under your control, so you should be able to remove any possible errors.

    Presumably you test your game and identify and fix any problems, so this won't happen in runtime. And no, Godot won't chance versions underneath you (much less in runtime).

  • It might be useful to interface with code beyond your control. For example, if you are making an addon/plugin that calls into other game code yet to be written.

    In this case, I'd believe the underlying problem is the lack of interfaces/contracts in GDScript. Current plan for GDScript is that it will have traits.

    I submit to your consideration using a signal bus. The signal bus will be a global object that both caller and called have access to. So the caller can emit the signal, and the called connect to the signal... Without the caller and called knowing each other. This gives you fully decoupled code.

    For C# in particular, you do have interfaces, so you can define an interface and have the other code implement it.

  • You are doing code generation. And that is what I'll tackle here.

You can generate and run code in runtime in Godot two ways:

  • Create an Script object.
  • Create an Expression object.

Script

I'll create a new GDScript object:

var my_runtuime_script:= GDScript.new()

I need to set its source code, which will be some string with whatever code I need, generated by whatever means I have (I'm using a multiline String literal here):

var my_runtuime_script:= GDScript.new()
my_runtuime_script.source_code = """
func my_generated_func() -> void:
    prints("hello generated world")
"""

And then I need to reload it:

var my_runtuime_script:= GDScript.new()
my_runtuime_script.source_code = """
func my_generated_func() -> void:
    prints("hello generated world")
"""
my_runtuime_script.reload()

And reload will report errors! So if you have a syntax error, you find out here:

var my_runtuime_script:= GDScript.new()
my_runtuime_script.source_code = """
func my_generated_func() -> void:
    prints("hello generated world")
"""
var err := my_runtuime_script.reload()
prints(err)

Assuming everything is OK, we can instantiate the script and call into it:

var my_runtuime_script:= GDScript.new()
my_runtuime_script.source_code = """
func my_generated_func() -> void:
    prints("hello generated world")
"""
var err := my_runtuime_script.reload()
prints(err)
if err == OK:
    var instance:Object = my_runtuime_script.new()
    instance.call("my_generated_func")

Output:

0
hello generated world

Here the 0 means no error (OK), and hello generated world is the output form the function created at runtime.


Expression

Again we start by creating an object, this time the class is Expression:

var expression := Expression.new()

And we have to give it the code, which has to be an expression. We can define parameters, for example:

var expression := Expression.new()
var parameters := PackedStringArray(["p"])
expression.parse("2 + p", parameters)

And parse returns! So we get any parse errors here:

var expression := Expression.new()
var parameters := PackedStringArray(["p"])
var parse_err := expression.parse("2 + p", parameters)

If there was a parse error, we can call get_error_text:

var expression := Expression.new()
var parameters := PackedStringArray(["p"])
var parse_err := expression.parse("2 + p", parameters)
if parse_err != OK:
    push_error(expression.get_error_text())

Then to execute it, we need to pass the arguments for the parameters we defined:

var expression := Expression.new()
var parameters := PackedStringArray(["p"])
var parse_err := expression.parse("2 + p", parameters)
if parse_err != OK:
    push_error(expression.get_error_text())
else:
    var arguments := [5]
    var result = expression.execute(arguments, null, true)

Here the null is the "self" reference (the instance on which the expression will be executed), and true means we do not want it to show errors... We will handle them!

How do we handle them? Like this:

var expression := Expression.new()
var parameters := PackedStringArray(["p"])
var parse_err := expression.parse("2 + p", parameters)
if parse_err != OK:
    push_error(expression.get_error_text())
else:
    var arguments := [5]
    var result = expression.execute(arguments, null, true)
    if expression.has_execute_failed():
        push_error(expression.get_error_text())
    else:
        print(result)

output:

7

This is 2 + 5

And we can leverage this to make a call:

var method_name := "my_method"
var method_arguments := [123, "abc"]

var expression := Expression.new()
var parameters := PackedStringArray(["method_name", "args"])
var parse_err := expression.parse("callv(method_name, args)", parameters)
if parse_err != OK:
    push_error(expression.get_error_text())
else:
    var arguments := [method_name, method_arguments]
    var result = expression.execute(arguments, obj, true)
    if expression.has_execute_failed():
        push_error(expression.get_error_text())
    else:
        print(result)

Here we attempt to call "my_method" on the object obj with arguments 123 and "abc".

This way you should get an error telling you if the method was not found or if there is any problem with the arguments.

Sadly, you have has_execute_failed returning a bool, and we lack an API to read which the error was other than get_error_text (which I guess you could parse).

However, this works similar to what you want: you call a method to know if there was a problem with the call.


Checking with is_valid from Callable

I originally skipped callables, because I took the question as face value.

Yet, Callable has an is_valid method that looks promising. So, let us define a callable:

var my_callable := Callable(obj, "my_method")

And check if it is valid before calling:

var my_callable := Callable(obj, "my_method")
if my_callable.is_valid():
    my_callable.call()
else:
    push_error("invalid callable")

The problem is that this does not take into account the arguments. We might bind the arguments (this returns a new callable):

var my_callable := Callable(obj, "my_method")
my_callable = my_callable.bind(123, "abc")
if my_callable.is_valid():
    my_callable.call()
else:
    push_error("invalid callable")

But - upon testing - is_valid does not check if the bound arguments are valid. Thus, this call might still fail.

\$\endgroup\$
4
  • \$\begingroup\$ I was imagining there would be some sort of global "last_error" or similar I could check somehow. I suspected I'd have to check things beforehand, but that's so much code. Leaving that up to the user is far from ideal, and error-prone (e.g. what if they add new language features, etc). Clearly the devs should make this into a built-in API; or better, also add overloads for call that give feedback in case of failure. I wonder if there's any existing issue that discussed this on GitHub. In any case, thanks for the answer; I'll mark it as accepted for now. \$\endgroup\$
    – geekley
    Commented Sep 19, 2023 at 18:07
  • \$\begingroup\$ @geekley There shouldn't be a chance for this kind of error in runtime. Presumably you test your code in a debug environment before release. Also Godot does not change its version from underneath you, not in your development environment, much less in release. With that said, there might be another way if what you are doing is code generation in runtime, I'll try to update the answer shortly. I do not know if there is a discussion about this, but you are welcome to bring it up. \$\endgroup\$
    – Theraot
    Commented Sep 19, 2023 at 18:12
  • \$\begingroup\$ @geekley expanded answer. I'll also add callables. \$\endgroup\$
    – Theraot
    Commented Sep 19, 2023 at 18:52
  • \$\begingroup\$ Thanks for the in-depth analysis! I've started a discussion about adding error catching to the reflection APIs here \$\endgroup\$
    – geekley
    Commented Sep 19, 2023 at 19:34

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .