Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Callable obtained from classes (static and non-static methods) have suspect and inconsistent behaviors in user defined & native classes #89350

Open
lethefrost opened this issue Mar 10, 2024 · 3 comments · Fixed by #97374 · May be fixed by #103212 or #103213

Comments

@lethefrost
Copy link

lethefrost commented Mar 10, 2024

Tested versions

  • Reproducible in 4.2.1.stable, 4.2.stable
  • 4.2.1.stable outputs error message Attempt to call function 'null::<function name> (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:

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

@tool
extends EditorScript

func _run() -> void:
	var my_instance := MyClass.new()
	
	print(MyClass) # <GDScript#-9223365398814983298>
	print(my_instance) # <RefCounted#-9223364908316299205>
	
	
	# ------------- 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

@tool
extends EditorScript

func _run() -> void:
	var node_instance := Node.new()
	
	print(Node) # <GDScriptNativeClass#-9223372023818878356>
	print(node_instance) # <Node#20613024514636>
	
	
	# ------------- 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

	# 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

@lethefrost lethefrost changed the title Callable instances created from classes' static method have suspect behaviors, and inconsistent in user defined & native classes Callable obtained from classes (static and non-static methods) have suspect and inconsistent behaviors in user defined & native classes Mar 10, 2024
@dalexeev
Copy link
Member

@rune-scape #97374 says "partially fixes #89350". Should this be re-opened?

@rune-scape
Copy link
Contributor

yes, my PR only adresses the callables from static methods on native classes, "print_orphan_nodes" in Node and others are unaffected
im not sure about the cause of those ones

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment