Browse Source

Merge pull request #1377 from dsnopek/gdextension-register-virtual-method

Allow GDExtensions to register virtual methods and call them on scripts (godot-cpp support)
David Snopek 1 year ago
parent
commit
5fcc43e54d

+ 132 - 0
binding_generator.py

@@ -70,6 +70,136 @@ def generate_wrappers(target):
         f.write(txt)
 
 
+def generate_virtual_version(argcount, const=False, returns=False):
+    s = """#define GDVIRTUAL$VER($RET m_name $ARG)\\
+	StringName _gdvirtual_##m_name##_sn = #m_name;\\
+	template <bool required>\\
+	_FORCE_INLINE_ bool _gdvirtual_##m_name##_call($CALLARGS) $CONST {\\
+		if (::godot::internal::gdextension_interface_object_has_script_method(_owner, &_gdvirtual_##m_name##_sn)) { \\
+			GDExtensionCallError ce;\\
+			$CALLSIARGS\\
+			$CALLSIBEGIN::godot::internal::gdextension_interface_object_call_script_method(_owner, &_gdvirtual_##m_name##_sn, $CALLSIARGPASS, $CALLSIRETPASS, &ce);\\
+			if (ce.error == GDEXTENSION_CALL_OK) {\\
+				$CALLSIRET\\
+				return true;\\
+			}\\
+		}\\
+		if (required) {\\
+			ERR_PRINT_ONCE("Required virtual method " + get_class() + "::" + #m_name + " must be overridden before calling.");\\
+			$RVOID\\
+		}\\
+		return false;\\
+	}\\
+	_FORCE_INLINE_ bool _gdvirtual_##m_name##_overridden() const {\\
+		return godot::internal::gdextension_interface_object_has_script_method(_owner, &_gdvirtual_##m_name##_sn); \\
+	}\\
+	_FORCE_INLINE_ static MethodInfo _gdvirtual_##m_name##_get_method_info() {\\
+		MethodInfo method_info;\\
+		method_info.name = #m_name;\\
+		method_info.flags = $METHOD_FLAGS;\\
+		$FILL_METHOD_INFO\\
+		return method_info;\\
+	}
+
+"""
+
+    sproto = str(argcount)
+    method_info = ""
+    if returns:
+        sproto += "R"
+        s = s.replace("$RET", "m_ret,")
+        s = s.replace("$RVOID", "(void)r_ret;")  # If required, may lead to uninitialized errors
+        method_info += "method_info.return_val = GetTypeInfo<m_ret>::get_class_info();\\\n"
+        method_info += "\t\tmethod_info.return_val_metadata = GetTypeInfo<m_ret>::METADATA;"
+    else:
+        s = s.replace("$RET ", "")
+        s = s.replace("\t\t\t$RVOID\\\n", "")
+
+    if const:
+        sproto += "C"
+        s = s.replace("$CONST", "const")
+        s = s.replace("$METHOD_FLAGS", "METHOD_FLAG_VIRTUAL | METHOD_FLAG_CONST")
+    else:
+        s = s.replace("$CONST ", "")
+        s = s.replace("$METHOD_FLAGS", "METHOD_FLAG_VIRTUAL")
+
+    s = s.replace("$VER", sproto)
+    argtext = ""
+    callargtext = ""
+    callsiargs = ""
+    callsiargptrs = ""
+    if argcount > 0:
+        argtext += ", "
+        callsiargs = f"Variant vargs[{argcount}] = {{ "
+        callsiargptrs = f"\t\t\tconst Variant *vargptrs[{argcount}] = {{ "
+    for i in range(argcount):
+        if i > 0:
+            argtext += ", "
+            callargtext += ", "
+            callsiargs += ", "
+            callsiargptrs += ", "
+        argtext += f"m_type{i + 1}"
+        callargtext += f"m_type{i + 1} arg{i + 1}"
+        callsiargs += f"Variant(arg{i + 1})"
+        callsiargptrs += f"&vargs[{i}]"
+        if method_info:
+            method_info += "\\\n\t\t"
+        method_info += f"method_info.arguments.push_back(GetTypeInfo<m_type{i + 1}>::get_class_info());\\\n"
+        method_info += f"\t\tmethod_info.arguments_metadata.push_back(GetTypeInfo<m_type{i + 1}>::METADATA);"
+
+    if argcount:
+        callsiargs += " };\\\n"
+        callsiargptrs += " };"
+        s = s.replace("$CALLSIARGS", callsiargs + callsiargptrs)
+        s = s.replace("$CALLSIARGPASS", f"(const GDExtensionConstVariantPtr *)vargptrs, {argcount}")
+    else:
+        s = s.replace("\t\t\t$CALLSIARGS\\\n", "")
+        s = s.replace("$CALLSIARGPASS", "nullptr, 0")
+
+    if returns:
+        if argcount > 0:
+            callargtext += ", "
+        callargtext += "m_ret &r_ret"
+        s = s.replace("$CALLSIBEGIN", "Variant ret;\\\n\t\t\t")
+        s = s.replace("$CALLSIRETPASS", "&ret")
+        s = s.replace("$CALLSIRET", "r_ret = VariantCaster<m_ret>::cast(ret);")
+    else:
+        s = s.replace("$CALLSIBEGIN", "")
+        s = s.replace("$CALLSIRETPASS", "nullptr")
+        s = s.replace("\t\t\t\t$CALLSIRET\\\n", "")
+
+    s = s.replace(" $ARG", argtext)
+    s = s.replace("$CALLARGS", callargtext)
+    if method_info:
+        s = s.replace("$FILL_METHOD_INFO", method_info)
+    else:
+        s = s.replace("\t\t$FILL_METHOD_INFO\\\n", method_info)
+
+    return s
+
+
+def generate_virtuals(target):
+    max_versions = 12
+
+    txt = """/* THIS FILE IS GENERATED DO NOT EDIT */
+#ifndef GDEXTENSION_GDVIRTUAL_GEN_H
+#define GDEXTENSION_GDVIRTUAL_GEN_H
+
+"""
+
+    for i in range(max_versions + 1):
+        txt += f"/* {i} Arguments */\n\n"
+        txt += generate_virtual_version(i, False, False)
+        txt += generate_virtual_version(i, False, True)
+        txt += generate_virtual_version(i, True, False)
+        txt += generate_virtual_version(i, True, True)
+
+    txt += "#endif // GDEXTENSION_GDVIRTUAL_GEN_H\n"
+
+    with open(target, "w", encoding="utf-8") as f:
+        f.write(txt)
+
+
 def get_file_list(api_filepath, output_dir, headers=False, sources=False):
     api = {}
     files = []
@@ -81,6 +211,7 @@ def get_file_list(api_filepath, output_dir, headers=False, sources=False):
     source_gen_folder = Path(output_dir) / "gen" / "src"
 
     files.append(str((core_gen_folder / "ext_wrappers.gen.inc").as_posix()))
+    files.append(str((core_gen_folder / "gdvirtual.gen.inc").as_posix()))
 
     for builtin_class in api["builtin_classes"]:
         if is_pod_type(builtin_class["name"]):
@@ -204,6 +335,7 @@ def generate_builtin_bindings(api, output_dir, build_config):
     source_gen_folder.mkdir(parents=True, exist_ok=True)
 
     generate_wrappers(core_gen_folder / "ext_wrappers.gen.inc")
+    generate_virtuals(core_gen_folder / "gdvirtual.gen.inc")
 
     # Store types beforehand.
     for builtin_api in api["builtin_classes"]:

+ 60 - 1
gdextension/gdextension_interface.h

@@ -364,13 +364,18 @@ typedef struct {
 	GDExtensionClassMethodPtrCall ptrcall_func;
 	uint32_t method_flags; // Bitfield of `GDExtensionClassMethodFlags`.
 
-	/* If `has_return_value` is false, `return_value_info` and `return_value_metadata` are ignored. */
+	/* If `has_return_value` is false, `return_value_info` and `return_value_metadata` are ignored.
+	 *
+	 * @todo Consider dropping `has_return_value` and making the other two properties match `GDExtensionMethodInfo` and `GDExtensionClassVirtualMethod` for consistency in future version of this struct.
+	 */
 	GDExtensionBool has_return_value;
 	GDExtensionPropertyInfo *return_value_info;
 	GDExtensionClassMethodArgumentMetadata return_value_metadata;
 
 	/* Arguments: `arguments_info` and `arguments_metadata` are array of size `argument_count`.
 	 * Name and hint information for the argument can be omitted in release builds. Class name should always be present if it applies.
+	 *
+	 * @todo Consider renaming `arguments_info` to `arguments` for consistency in future version of this struct.
 	 */
 	uint32_t argument_count;
 	GDExtensionPropertyInfo *arguments_info;
@@ -381,6 +386,18 @@ typedef struct {
 	GDExtensionVariantPtr *default_arguments;
 } GDExtensionClassMethodInfo;
 
+typedef struct {
+	GDExtensionStringNamePtr name;
+	uint32_t method_flags; // Bitfield of `GDExtensionClassMethodFlags`.
+
+	GDExtensionPropertyInfo return_value;
+	GDExtensionClassMethodArgumentMetadata return_value_metadata;
+
+	uint32_t argument_count;
+	GDExtensionPropertyInfo *arguments;
+	GDExtensionClassMethodArgumentMetadata *arguments_metadata;
+} GDExtensionClassVirtualMethodInfo;
+
 typedef void (*GDExtensionCallableCustomCall)(void *callable_userdata, const GDExtensionConstVariantPtr *p_args, GDExtensionInt p_argument_count, GDExtensionVariantPtr r_return, GDExtensionCallError *r_error);
 typedef GDExtensionBool (*GDExtensionCallableCustomIsValid)(void *callable_userdata);
 typedef void (*GDExtensionCallableCustomFree)(void *callable_userdata);
@@ -2268,6 +2285,34 @@ typedef GDExtensionObjectPtr (*GDExtensionInterfaceObjectGetInstanceFromId)(GDOb
  */
 typedef GDObjectInstanceID (*GDExtensionInterfaceObjectGetInstanceId)(GDExtensionConstObjectPtr p_object);
 
+/**
+ * @name object_has_script_method
+ * @since 4.3
+ *
+ * Checks if this object has a script with the given method.
+ *
+ * @param p_object A pointer to the Object.
+ * @param p_method A pointer to a StringName identifying the method.
+ *
+ * @returns true if the object has a script and that script has a method with the given name. Returns false if the object has no script.
+ */
+typedef GDExtensionBool (*GDExtensionInterfaceObjectHasScriptMethod)(GDExtensionConstObjectPtr p_object, GDExtensionConstStringNamePtr p_method);
+
+/**
+ * @name object_call_script_method
+ * @since 4.3
+ *
+ * Call the given script method on this object.
+ *
+ * @param p_object A pointer to the Object.
+ * @param p_method A pointer to a StringName identifying the method.
+ * @param p_args A pointer to a C array of Variant.
+ * @param p_argument_count The number of arguments.
+ * @param r_return A pointer a Variant which will be assigned the return value.
+ * @param r_error A pointer the structure which will hold error information.
+ */
+typedef void (*GDExtensionInterfaceObjectCallScriptMethod)(GDExtensionObjectPtr p_object, GDExtensionConstStringNamePtr p_method, const GDExtensionConstVariantPtr *p_args, GDExtensionInt p_argument_count, GDExtensionUninitializedVariantPtr r_return, GDExtensionCallError *r_error);
+
 /* INTERFACE: Reference */
 
 /**
@@ -2483,6 +2528,20 @@ typedef void (*GDExtensionInterfaceClassdbRegisterExtensionClass2)(GDExtensionCl
  */
 typedef void (*GDExtensionInterfaceClassdbRegisterExtensionClassMethod)(GDExtensionClassLibraryPtr p_library, GDExtensionConstStringNamePtr p_class_name, const GDExtensionClassMethodInfo *p_method_info);
 
+/**
+ * @name classdb_register_extension_class_virtual_method
+ * @since 4.3
+ *
+ * Registers a virtual method on an extension class in ClassDB, that can be implemented by scripts or other extensions.
+ *
+ * Provided struct can be safely freed once the function returns.
+ *
+ * @param p_library A pointer the library received by the GDExtension's entry point function.
+ * @param p_class_name A pointer to a StringName with the class name.
+ * @param p_method_info A pointer to a GDExtensionClassMethodInfo struct.
+ */
+typedef void (*GDExtensionInterfaceClassdbRegisterExtensionClassVirtualMethod)(GDExtensionClassLibraryPtr p_library, GDExtensionConstStringNamePtr p_class_name, const GDExtensionClassVirtualMethodInfo *p_method_info);
+
 /**
  * @name classdb_register_extension_class_integer_constant
  * @since 4.1

+ 31 - 0
include/godot_cpp/classes/wrapped.hpp

@@ -36,6 +36,7 @@
 #include <godot_cpp/core/property_info.hpp>
 
 #include <godot_cpp/templates/list.hpp>
+#include <godot_cpp/templates/vector.hpp>
 
 #include <godot_cpp/godot.hpp>
 
@@ -107,6 +108,26 @@ public:
 	GodotObject *_owner = nullptr;
 };
 
+_FORCE_INLINE_ void snarray_add_str(Vector<StringName> &arr) {
+}
+
+_FORCE_INLINE_ void snarray_add_str(Vector<StringName> &arr, const StringName &p_str) {
+	arr.push_back(p_str);
+}
+
+template <class... P>
+_FORCE_INLINE_ void snarray_add_str(Vector<StringName> &arr, const StringName &p_str, P... p_args) {
+	arr.push_back(p_str);
+	snarray_add_str(arr, p_args...);
+}
+
+template <class... P>
+_FORCE_INLINE_ Vector<StringName> snarray(P... p_args) {
+	Vector<StringName> arr;
+	snarray_add_str(arr, p_args...);
+	return arr;
+}
+
 namespace internal {
 
 GDExtensionPropertyInfo *create_c_property_list(const ::godot::List<::godot::PropertyInfo> &plist_cpp, uint32_t *r_size);
@@ -445,4 +466,14 @@ private:
 // Don't use this for your classes, use GDCLASS() instead.
 #define GDEXTENSION_CLASS(m_class, m_inherits) GDEXTENSION_CLASS_ALIAS(m_class, m_class, m_inherits)
 
+#define GDVIRTUAL_CALL(m_name, ...) _gdvirtual_##m_name##_call<false>(__VA_ARGS__)
+#define GDVIRTUAL_CALL_PTR(m_obj, m_name, ...) m_obj->_gdvirtual_##m_name##_call<false>(__VA_ARGS__)
+
+#define GDVIRTUAL_REQUIRED_CALL(m_name, ...) _gdvirtual_##m_name##_call<true>(__VA_ARGS__)
+#define GDVIRTUAL_REQUIRED_CALL_PTR(m_obj, m_name, ...) m_obj->_gdvirtual_##m_name##_call<true>(__VA_ARGS__)
+
+#define GDVIRTUAL_BIND(m_name, ...) ::godot::ClassDB::add_virtual_method(get_class_static(), _gdvirtual_##m_name##_get_method_info(), ::godot::snarray(__VA_ARGS__));
+#define GDVIRTUAL_IS_OVERRIDDEN(m_name) _gdvirtual_##m_name##_overridden()
+#define GDVIRTUAL_IS_OVERRIDDEN_PTR(m_obj, m_name) m_obj->_gdvirtual_##m_name##_overridden()
+
 #endif // GODOT_WRAPPED_HPP

+ 3 - 0
include/godot_cpp/core/class_db.hpp

@@ -165,7 +165,10 @@ public:
 	static void add_property(const StringName &p_class, const PropertyInfo &p_pinfo, const StringName &p_setter, const StringName &p_getter, int p_index = -1);
 	static void add_signal(const StringName &p_class, const MethodInfo &p_signal);
 	static void bind_integer_constant(const StringName &p_class_name, const StringName &p_enum_name, const StringName &p_constant_name, GDExtensionInt p_constant_value, bool p_is_bitfield = false);
+	// Binds an implementation of a virtual method defined in Godot.
 	static void bind_virtual_method(const StringName &p_class, const StringName &p_method, GDExtensionClassCallVirtual p_call);
+	// Add a new virtual method that can be implemented by scripts.
+	static void add_virtual_method(const StringName &p_class, const MethodInfo &p_method, const Vector<StringName> &p_arg_names = Vector<StringName>());
 
 	static MethodBind *get_method(const StringName &p_class, const StringName &p_method);
 

+ 2 - 0
include/godot_cpp/core/object.hpp

@@ -68,6 +68,8 @@ struct MethodInfo {
 	int id = 0;
 	std::vector<PropertyInfo> arguments;
 	std::vector<Variant> default_arguments;
+	GDExtensionClassMethodArgumentMetadata return_val_metadata;
+	std::vector<GDExtensionClassMethodArgumentMetadata> arguments_metadata;
 
 	inline bool operator==(const MethodInfo &p_method) const { return id == p_method.id; }
 	inline bool operator<(const MethodInfo &p_method) const { return id == p_method.id ? (name < p_method.name) : (id < p_method.id); }

+ 11 - 0
include/godot_cpp/core/property_info.hpp

@@ -80,6 +80,17 @@ struct PropertyInfo {
 		p_info->usage = usage;
 		*(reinterpret_cast<StringName *>(p_info->class_name)) = class_name;
 	}
+
+	GDExtensionPropertyInfo _to_gdextension() const {
+		return {
+			(GDExtensionVariantType)type,
+			name._native_ptr(),
+			class_name._native_ptr(),
+			hint,
+			hint_string._native_ptr(),
+			usage,
+		};
+	}
 };
 
 } // namespace godot

+ 3 - 0
include/godot_cpp/godot.hpp

@@ -165,6 +165,8 @@ extern "C" GDExtensionInterfaceObjectGetClassName gdextension_interface_object_g
 extern "C" GDExtensionInterfaceObjectCastTo gdextension_interface_object_cast_to;
 extern "C" GDExtensionInterfaceObjectGetInstanceFromId gdextension_interface_object_get_instance_from_id;
 extern "C" GDExtensionInterfaceObjectGetInstanceId gdextension_interface_object_get_instance_id;
+extern "C" GDExtensionInterfaceObjectHasScriptMethod gdextension_interface_object_has_script_method;
+extern "C" GDExtensionInterfaceObjectCallScriptMethod gdextension_interface_object_call_script_method;
 extern "C" GDExtensionInterfaceCallableCustomCreate gdextension_interface_callable_custom_create;
 extern "C" GDExtensionInterfaceCallableCustomGetUserData gdextension_interface_callable_custom_get_userdata;
 extern "C" GDExtensionInterfaceRefGetObject gdextension_interface_ref_get_object;
@@ -177,6 +179,7 @@ extern "C" GDExtensionInterfaceClassdbGetMethodBind gdextension_interface_classd
 extern "C" GDExtensionInterfaceClassdbGetClassTag gdextension_interface_classdb_get_class_tag;
 extern "C" GDExtensionInterfaceClassdbRegisterExtensionClass2 gdextension_interface_classdb_register_extension_class2;
 extern "C" GDExtensionInterfaceClassdbRegisterExtensionClassMethod gdextension_interface_classdb_register_extension_class_method;
+extern "C" GDExtensionInterfaceClassdbRegisterExtensionClassVirtualMethod gdextension_interface_classdb_register_extension_class_virtual_method;
 extern "C" GDExtensionInterfaceClassdbRegisterExtensionClassIntegerConstant gdextension_interface_classdb_register_extension_class_integer_constant;
 extern "C" GDExtensionInterfaceClassdbRegisterExtensionClassProperty gdextension_interface_classdb_register_extension_class_property;
 extern "C" GDExtensionInterfaceClassdbRegisterExtensionClassPropertyIndexed gdextension_interface_classdb_register_extension_class_property_indexed;

+ 41 - 0
src/core/class_db.cpp

@@ -32,6 +32,7 @@
 
 #include <godot_cpp/core/error_macros.hpp>
 #include <godot_cpp/godot.hpp>
+#include <godot_cpp/templates/vector.hpp>
 
 #include <godot_cpp/core/memory.hpp>
 
@@ -337,6 +338,46 @@ void ClassDB::bind_virtual_method(const StringName &p_class, const StringName &p
 	type.virtual_methods[p_method] = p_call;
 }
 
+void ClassDB::add_virtual_method(const StringName &p_class, const MethodInfo &p_method, const Vector<StringName> &p_arg_names) {
+	std::unordered_map<StringName, ClassInfo>::iterator type_it = classes.find(p_class);
+	ERR_FAIL_COND_MSG(type_it == classes.end(), String("Class '{0}' doesn't exist.").format(Array::make(p_class)));
+
+	GDExtensionClassVirtualMethodInfo mi;
+	mi.name = (GDExtensionStringNamePtr)&p_method.name;
+	mi.method_flags = p_method.flags;
+	mi.return_value = p_method.return_val._to_gdextension();
+	mi.return_value_metadata = p_method.return_val_metadata;
+	mi.argument_count = p_method.arguments.size();
+	if (mi.argument_count > 0) {
+		mi.arguments = (GDExtensionPropertyInfo *)memalloc(sizeof(GDExtensionPropertyInfo) * mi.argument_count);
+		mi.arguments_metadata = (GDExtensionClassMethodArgumentMetadata *)memalloc(sizeof(GDExtensionClassMethodArgumentMetadata) * mi.argument_count);
+		for (int i = 0; i < mi.argument_count; i++) {
+			mi.arguments[i] = p_method.arguments[i]._to_gdextension();
+			mi.arguments_metadata[i] = p_method.arguments_metadata[i];
+		}
+	} else {
+		mi.arguments = nullptr;
+		mi.arguments_metadata = nullptr;
+	}
+
+	if (p_arg_names.size() != mi.argument_count) {
+		WARN_PRINT("Mismatch argument name count for virtual method: " + String(p_class) + "::" + p_method.name);
+	} else {
+		for (int i = 0; i < p_arg_names.size(); i++) {
+			mi.arguments[i].name = (GDExtensionStringNamePtr)&p_arg_names[i];
+		}
+	}
+
+	internal::gdextension_interface_classdb_register_extension_class_virtual_method(internal::library, &p_class, &mi);
+
+	if (mi.arguments) {
+		memfree(mi.arguments);
+	}
+	if (mi.arguments_metadata) {
+		memfree(mi.arguments_metadata);
+	}
+}
+
 void ClassDB::initialize_class(const ClassInfo &p_cl) {
 }
 

+ 6 - 0
src/godot.cpp

@@ -171,6 +171,8 @@ GDExtensionInterfaceObjectGetClassName gdextension_interface_object_get_class_na
 GDExtensionInterfaceObjectCastTo gdextension_interface_object_cast_to = nullptr;
 GDExtensionInterfaceObjectGetInstanceFromId gdextension_interface_object_get_instance_from_id = nullptr;
 GDExtensionInterfaceObjectGetInstanceId gdextension_interface_object_get_instance_id = nullptr;
+GDExtensionInterfaceObjectHasScriptMethod gdextension_interface_object_has_script_method = nullptr;
+GDExtensionInterfaceObjectCallScriptMethod gdextension_interface_object_call_script_method = nullptr;
 GDExtensionInterfaceCallableCustomCreate gdextension_interface_callable_custom_create = nullptr;
 GDExtensionInterfaceCallableCustomGetUserData gdextension_interface_callable_custom_get_userdata = nullptr;
 GDExtensionInterfaceRefGetObject gdextension_interface_ref_get_object = nullptr;
@@ -183,6 +185,7 @@ GDExtensionInterfaceClassdbGetMethodBind gdextension_interface_classdb_get_metho
 GDExtensionInterfaceClassdbGetClassTag gdextension_interface_classdb_get_class_tag = nullptr;
 GDExtensionInterfaceClassdbRegisterExtensionClass2 gdextension_interface_classdb_register_extension_class2 = nullptr;
 GDExtensionInterfaceClassdbRegisterExtensionClassMethod gdextension_interface_classdb_register_extension_class_method = nullptr;
+GDExtensionInterfaceClassdbRegisterExtensionClassVirtualMethod gdextension_interface_classdb_register_extension_class_virtual_method = nullptr;
 GDExtensionInterfaceClassdbRegisterExtensionClassIntegerConstant gdextension_interface_classdb_register_extension_class_integer_constant = nullptr;
 GDExtensionInterfaceClassdbRegisterExtensionClassProperty gdextension_interface_classdb_register_extension_class_property = nullptr;
 GDExtensionInterfaceClassdbRegisterExtensionClassPropertyIndexed gdextension_interface_classdb_register_extension_class_property_indexed = nullptr;
@@ -408,6 +411,8 @@ GDExtensionBool GDExtensionBinding::init(GDExtensionInterfaceGetProcAddress p_ge
 	LOAD_PROC_ADDRESS(object_cast_to, GDExtensionInterfaceObjectCastTo);
 	LOAD_PROC_ADDRESS(object_get_instance_from_id, GDExtensionInterfaceObjectGetInstanceFromId);
 	LOAD_PROC_ADDRESS(object_get_instance_id, GDExtensionInterfaceObjectGetInstanceId);
+	LOAD_PROC_ADDRESS(object_has_script_method, GDExtensionInterfaceObjectHasScriptMethod);
+	LOAD_PROC_ADDRESS(object_call_script_method, GDExtensionInterfaceObjectCallScriptMethod);
 	LOAD_PROC_ADDRESS(callable_custom_create, GDExtensionInterfaceCallableCustomCreate);
 	LOAD_PROC_ADDRESS(callable_custom_get_userdata, GDExtensionInterfaceCallableCustomGetUserData);
 	LOAD_PROC_ADDRESS(ref_get_object, GDExtensionInterfaceRefGetObject);
@@ -420,6 +425,7 @@ GDExtensionBool GDExtensionBinding::init(GDExtensionInterfaceGetProcAddress p_ge
 	LOAD_PROC_ADDRESS(classdb_get_class_tag, GDExtensionInterfaceClassdbGetClassTag);
 	LOAD_PROC_ADDRESS(classdb_register_extension_class2, GDExtensionInterfaceClassdbRegisterExtensionClass2);
 	LOAD_PROC_ADDRESS(classdb_register_extension_class_method, GDExtensionInterfaceClassdbRegisterExtensionClassMethod);
+	LOAD_PROC_ADDRESS(classdb_register_extension_class_virtual_method, GDExtensionInterfaceClassdbRegisterExtensionClassVirtualMethod);
 	LOAD_PROC_ADDRESS(classdb_register_extension_class_integer_constant, GDExtensionInterfaceClassdbRegisterExtensionClassIntegerConstant);
 	LOAD_PROC_ADDRESS(classdb_register_extension_class_property, GDExtensionInterfaceClassdbRegisterExtensionClassProperty);
 	LOAD_PROC_ADDRESS(classdb_register_extension_class_property_indexed, GDExtensionInterfaceClassdbRegisterExtensionClassPropertyIndexed);

+ 5 - 0
test/project/example.gd

@@ -0,0 +1,5 @@
+extends Example
+
+func _do_something_virtual(p_name, p_value):
+	custom_signal.emit(p_name, p_value)
+	return "Implemented"

+ 4 - 0
test/project/main.gd

@@ -241,6 +241,10 @@ func _ready():
 	assert_equal(new_example_ref.was_post_initialized(), true)
 	assert_equal(example.test_post_initialize(), true)
 
+	# Test a virtual method defined in GDExtension and implemented in script.
+	assert_equal(example.test_virtual_implemented_in_script("Virtual", 939), "Implemented")
+	assert_equal(custom_signal_emitted, ["Virtual", 939])
+
 	exit_with_status()
 
 func _on_Example_custom_signal(signal_name, value):

+ 3 - 1
test/project/main.tscn

@@ -1,11 +1,13 @@
-[gd_scene load_steps=2 format=3 uid="uid://dmx2xuigcpvt4"]
+[gd_scene load_steps=3 format=3 uid="uid://dmx2xuigcpvt4"]
 
 [ext_resource type="Script" path="res://main.gd" id="1_qesh5"]
+[ext_resource type="Script" path="res://example.gd" id="2_jju25"]
 
 [node name="Node" type="Node"]
 script = ExtResource("1_qesh5")
 
 [node name="Example" type="Example" parent="."]
+script = ExtResource("2_jju25")
 
 [node name="ExampleMin" type="ExampleMin" parent="Example"]
 layout_mode = 0

+ 11 - 0
test/src/example.cpp

@@ -230,6 +230,9 @@ void Example::_bind_methods() {
 	ClassDB::bind_method(D_METHOD("callable_bind"), &Example::callable_bind);
 	ClassDB::bind_method(D_METHOD("test_post_initialize"), &Example::test_post_initialize);
 
+	GDVIRTUAL_BIND(_do_something_virtual, "name", "value");
+	ClassDB::bind_method(D_METHOD("test_virtual_implemented_in_script"), &Example::test_virtual_implemented_in_script);
+
 	ClassDB::bind_static_method("Example", D_METHOD("test_static", "a", "b"), &Example::test_static);
 	ClassDB::bind_static_method("Example", D_METHOD("test_static2"), &Example::test_static2);
 
@@ -626,3 +629,11 @@ void Example::_input(const Ref<InputEvent> &event) {
 		emit_custom_signal(String("_input: ") + key_event->get_key_label(), key_event->get_unicode());
 	}
 }
+
+String Example::test_virtual_implemented_in_script(const String &p_name, int p_value) {
+	String ret;
+	if (GDVIRTUAL_CALL(_do_something_virtual, p_name, p_value, ret)) {
+		return ret;
+	}
+	return "Unimplemented";
+}

+ 4 - 0
test/src/example.h

@@ -24,6 +24,7 @@
 #include <godot_cpp/variant/variant.hpp>
 
 #include <godot_cpp/core/binder_common.hpp>
+#include <godot_cpp/core/gdvirtual.gen.inc>
 
 using namespace godot;
 
@@ -181,6 +182,9 @@ public:
 	// Virtual function override (no need to bind manually).
 	virtual bool _has_point(const Vector2 &point) const override;
 	virtual void _input(const Ref<InputEvent> &event) override;
+
+	GDVIRTUAL2R(String, _do_something_virtual, String, int);
+	String test_virtual_implemented_in_script(const String &p_name, int p_value);
 };
 
 VARIANT_ENUM_CAST(Example::Constants);