Skip to content

Commit

Permalink
Allow GDExtensions to register virtual methods and call them on scripts
Browse files Browse the repository at this point in the history
  • Loading branch information
dsnopek committed Jan 30, 2024
1 parent 36847f6 commit a969e47
Show file tree
Hide file tree
Showing 14 changed files with 310 additions and 1 deletion.
132 changes: 132 additions & 0 deletions binding_generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = []
Expand All @@ -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"]):
Expand Down Expand Up @@ -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"]:
Expand Down
54 changes: 54 additions & 0 deletions gdextension/gdextension_interface.h
Original file line number Diff line number Diff line change
Expand Up @@ -381,6 +381,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_info;
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);
Expand Down Expand Up @@ -2268,6 +2280,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 method exists.
*/
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 */

/**
Expand Down Expand Up @@ -2483,6 +2523,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
Expand Down
31 changes: 31 additions & 0 deletions include/godot_cpp/classes/wrapped.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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>

Expand Down Expand Up @@ -107,6 +108,26 @@ class Wrapped {
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);
Expand Down Expand Up @@ -445,4 +466,14 @@ public:
// 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 changes: 3 additions & 0 deletions include/godot_cpp/core/class_db.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,10 @@ class ClassDB {
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);

Expand Down
2 changes: 2 additions & 0 deletions include/godot_cpp/core/object.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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); }
Expand Down
11 changes: 11 additions & 0 deletions include/godot_cpp/core/property_info.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions include/godot_cpp/godot.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down
41 changes: 41 additions & 0 deletions src/core/class_db.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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>

Expand Down Expand Up @@ -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_info = (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_info[i] = p_method.arguments[i]._to_gdextension();
mi.arguments_metadata[i] = p_method.arguments_metadata[i];
}
} else {
mi.arguments_info = 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_info[i].name = (GDExtensionStringNamePtr)&p_arg_names[i];
}
}

internal::gdextension_interface_classdb_register_extension_class_virtual_method(internal::library, &p_class, &mi);

if (mi.arguments_info) {
memfree(mi.arguments_info);
}
if (mi.arguments_metadata) {
memfree(mi.arguments_metadata);
}
}

void ClassDB::initialize_class(const ClassInfo &p_cl) {
}

Expand Down
Loading

0 comments on commit a969e47

Please sign in to comment.