godotengine / godotengine/godot
Callable obtained from classes (static and non-static methods) have suspect and inconsistent behaviors in user defined & native classes
- Dominant language
- C++
- Stars
- 117k
- Forks
- 26.8k
- PR merge metrics
- PR metrics pending
Description
### Tested versions
- Reproducible in `4.2.1.stable`, `4.2.stable`
- `4.2.1.stable` outputs error message `Attempt to call function 'null:: (Callable)' on a null instance.` and `4.2.stable` has no outputs on it.
### System information
Godot v4.2.1.stable - macOS 14.3.1 - Vulkan (Forward+) - integrated Apple M2 Max - Apple M2 Max (12 Threads)
### Issue description
Callable instances of a static function, retrieved directly from the class, have strange & inconsistent behaviors.
## Classes Used in Test
Suppose a user defined (inner or global, by `class` or `class_name` will have the same behavior regarding this issue) class is claimed as the following, and then `MyClass` should be an instance of `GDScript`:
```gdscript
class MyClass:
static func my_static() -> void:
print("success")
```
And the built-in classes as `GDScriptNativeClass` instances also have many static functions, take class `Node` and its static function `print_orphan_nodes()` as an example.
## Behavior Observation and Conclusion
Details of the testing results please see the steps section.
The callable instance of functions `print_orphan_nodes` and `my_static` will have the following behaviors:
### User Classes
- For user defined classes, using a **string name** to retrieve the method directly from the class (instead of an instance) will get a callable on `null` object.
- Callables obtained by `MyClass.my_static` and `my_instance.my_static` work normally.
- Callables obtained by `my_instance["my_static"]` and `Callable(my_instance, "my_static")` work normally.
- Callables obtained by `MyClass["my_static"]` and `Callable(MyClass, "my_static")` will raise error `Attempt to call function 'null::my_static (Callable)' on a null instance.`.
- However, explicitly **casting** `MyClass` as any of its ancestor built-in classes' instance (`GDScript`, `Script`, ..., `Object`) will allow the string name to work, like `MyClass as GDScript`.
### Native Classes
- For built-in classes, the callables obtained from an instance still works in all the cases, but from the class itself the behaviors get even more confusing:
- Static methods are even **not considered as a member** of the class itself, checking with the `in` operator.
- Thus, you cannot obtain any callable (even not like in the user defined classes, an invalid/unusable callable `null::my_static`) by `Node.print_orphan_nodes`, `Node["print_orphan_nodes"]`, `(Node as Object).print_orphan_nodes`, `(Node as Object)["print_orphan_nodes"]`.
- However, you can find and successfully call directly by `Node.print_orphan_nodes()` (how weird given that `Node.print_orphan_nodes` doesn't even exist!)
- And strangely, you can create callable instance `Callable(Node, "print_orphan_nodes")` and call it with no problem.
### Moreover, for non-static methods on `Object`...
Might be related to this #77567 issue. Since those classes are instances of `GDScript` and `GDScriptNativeClass`, and therefore instances of `Object`, they should have access to `Object`'s instance method, taking `get_method_list` as an example here.
I tested on user and native classes and their behavior also have discrepancy.
- In user classes and native classes, `get_method_list` is confirmed to be a member of both the classes themselves.
- In user classes,
- For string name retrived callables, the behavior is mostly the same as static method defined on the user class: callables on `null` object, and can be fixed by casting.
- Noticable difference:
- Directly calling `MyClass.get_method_list()` raises a parse error (since the parser thinks it's not a static function and cannot be called on a class)
- But geting the callable `MyClass.get_method_list` first and then `.call()` on it will successfully execute.
- Or, it can be fixed by casting and reminding the parser `MyClass` is an `Object`: `(MyClass as Object).get_method_list()` will work.
- Therefore, there are some ways to obtain valid callables:
- `MyClass.get_method_list`
- `(MyClass as Object).get_method_list`
- `(MyClass as Object)["get_method_list"]`
- `Callable(MyClass as Object, "get_method_list")`
- In native classes,
- All the aforementioned ways will be able to obtain a seemingly valid callable `GDScriptNativeClass::get_method_list`, but none of them are really "callable". Calling them will all generate `Nonexistent function` error.
- Compared to user classes:
- Directly calling `Node.get_method_list()` also raises Parse Error as the same.
- However, not like in user classes, `Node.get_method_list.call()` won't fix it, as mentioned before, it is a fake callable on nonexistent function.
- The only way to run the function immediately is by casting: `(Node as Object).get_method_list()`
- But I don't see any way to produce a valid callable for storing and passing the function reference. Not like in user classes. All will raise nonexistent function error.
- It is strange given that `(Node as Object).get_method_list()` can run, but `(Node as Object).get_method_list.call()` is nonexistent.
### Steps to reproduce
## User Classes
```gdscript
@tool
extends EditorScript
func _run() -> void:
var my_instance := MyClass.new()
print(MyClass) #
print(my_instance) #
# ------------- Direct calls from identifier -------------
MyClass.my_static() # success
my_instance.my_static() # success (with warning)
# ------------- Callables from identifier -------------
# From class
var callable_from_class := MyClass.my_static
print(callable_from_class) # GDScript::my_static
callable_from_class.call() # success
# From instance
var callable_from_instance := my_instance.my_static
print(callable_from_instance) # RefCounted::my_static
callable_from_instance.call() # success (with no warning)
# ------------- Callables from string name -------------
# Check if MyClass and my_instance has member "my_static"
print("my_static" in MyClass) # true
print("my_static" in my_instance) # true
# ------------- Callables from index -------------
var callable_from_index := MyClass["my_static"]
print(callable_from_index) # null::my_static
# error: Attempt to call function 'null::my_static (Callable)' on a null instance.
# callable_from_index.call()
callable_from_index = my_instance["my_static"]
print(callable_from_index) # RefCounted::my_static
callable_from_index.call() # success (with no warning)
# ------------- Callables from constructor -------------
var callable_from_constructor := Callable(MyClass, "my_static")
print(callable_from_constructor) # null::my_static
# error: Attempt to call function 'null::my_static (Callable)' on a null instance.
# callable_from_constructor.call()
callable_from_object = Callable(my_instance, "my_static")
print(callable_from_object) # RefCounted::my_static
callable_from_object.call() # success (with no warning)
# ------------- Callables from cast class -------------
# Store the cast class,
# or using (MyClass as GDScript) in place will work as the same.
# Do not have to be GDScript,
# All builtin ancestor classes can work, such as Object.
var cast_class: GDScript = MyClass
var callable_from_cast_class: Callable = cast_class["my_static"]
print(callable_from_cast_class) # GDScript::my_static
callable_from_cast_class.call() # success
callable_from_cast_class = (MyClass as Object)["my_static"]
print(callable_from_cast_class) # GDScript::my_static <- will still be GDScript instance
callable_from_cast_class.call() # success
# Creating callables using the cast class as its object will also work.
callable_from_cast_class = Callable(cast_class, "my_static")
print(callable_from_cast_class) # GDScript::my_static
callable_from_cast_class.call() # success
```
## Native Classes
```gdscript
@tool
extends EditorScript
func _run() -> void:
var node_instance := Node.new()
print(Node) #
print(node_instance) #
# ------------- Direct calls from identifier -------------
Node.print_orphan_nodes() # success
node_instance.print_orphan_nodes() # success (with warning)
# ------------- Callables from identifier -------------
# Cannot find from class
# error: Invalid get index 'print_orphan_nodes' (on base: 'Node').
#var callable_from_class := Node.print_orphan_nodes
# From instance
var callable_from_instance := node_instance.print_orphan_nodes
print(callable_from_instance) # Node::print_orphan_nodes
callable_from_instance.call() # success (with no warning)
# ------------- Callables from string name -------------
# Check if Node and node_instance has member "print_orphan_nodes"
print("print_orphan_nodes" in Node) # false, but why?
print("print_orphan_nodes" in node_instance) # true
# ------------- Callables from index -------------
# error: Invalid get index 'print_orphan_nodes' (on base: 'Node').
#var callable_from_index: Callable = Node["print_orphan_nodes"]
var callable_from_index: Callable = node_instance["print_orphan_nodes"]
print(callable_from_index) # Node::print_orphan_nodes
callable_from_index.call() # success (with no warning)
# ------------- Callables from constructor -------------
var callable_from_constructor := Callable(Node, "print_orphan_nodes")
print(callable_from_constructor) # GDScriptNativeClass::print_orphan_nodes
callable_from_constructor.call() # success!
callable_from_object = Callable(node_instance, "print_orphan_nodes")
print(callable_from_object) # Node::print_orphan_nodes
callable_from_object.call() # success (with no warning)
# ------------- Callables from cast class -------------
# Store the cast class,
var cast_class: Object = Node
# Still cannot find from class
#var callable_from_cast_class: Callable = cast_class["print_orphan_nodes"]
#var callable_from_cast_class: Callable = cast_class.print_orphan_nodes
# Only creating callable instance from cast class works the same
var callable_from_cast_class := Callable(cast_class, "print_orphan_nodes")
print(callable_from_cast_class) # GDScriptNativeClass::print_orphan_nodes
callable_from_cast_class.call() # success
```
## Object Instance Method
```gdscript
# Directly call from MyClass or Node will not succeed,
# even when they are instances of Object
print(MyClass is Object) # true
print(Node is Object) # true
# Parse Error: Cannot call non-static function "get_method_list()"
# on the class "MyClass" directly. Make an instance instead.
#MyClass.get_method_list() # error
#Node.get_method_list() # error
# Checking if the classes have the member
print("get_method_list" in MyClass) # true
print("get_method_list" in Node) # true
var test: Callable
# Trying to call the two callables below will raise error:
# Attempt to call function 'null::get_method_list (Callable)' on a null instance.
test = MyClass["get_method_list"] # null::get_method_list
test = Callable(MyClass, "get_method_list") # null::get_method_list
# And the following three will succeed
test = MyClass.get_method_list # GDScript::get_method_list
test = (MyClass as Object).get_method_list # GDScript::get_method_list
test = (MyClass as Object)["get_method_list"] # GDScript::get_method_list
test = Callable(MyClass as Object, "get_method_list") # GDScript::get_method_list
# Trying to call all the callables below will raise error:
# Invalid call. Nonexistent function 'GDScriptNativeClass::get_method_list (Callable)'.
test = Node["get_method_list"] # GDScriptNativeClass::get_method_list
test = Callable(Node, "get_method_list") # GDScriptNativeClass::get_method_list
test = Node.get_method_list # GDScriptNativeClass::get_method_list
test = (Node as Object).get_method_list # GDScriptNativeClass::get_method_list
test = (Node as Object)["get_method_list"] # GDScriptNativeClass::get_method_list
test = Callable(Node as Object, "get_method_list")
# Explicitly casting as Object will allow calling
(MyClass as Object).get_method_list() # success
(Node as Object).get_method_list() # success
```
### Minimal reproduction project (MRP)
N/A
Contributor guide
Assessment
This issue has not been assessed yet.