Binding System
Overview
What are bindings? Think of bindings as translators between your C++ code and the rest of Godot. When you write a C++ method, Godot doesn’t automatically know it exists or how to call it. Bindings are the registration process that tells Godot “here’s a C++ function, here’s its name, here are its parameters, and here’s how to call it from GDScript or the editor.”
Why bindings are essential: Without bindings, your C++ code would be invisible to Godot. Bindings make your C++ classes appear as native Godot classes that can be used in GDScript, show up in the editor’s inspector, and participate in Godot’s signal system.
The binding system in godot-cpp creates the bridge between your C++ code and the Godot engine. It handles method registration, type conversion, parameter marshalling, and enables seamless interaction between GDExtension code and GDScript. Every class, method, property, and signal must be properly bound to be accessible from the engine.
Why Bindings Are Needed
---
config:
theme: 'base'
curve: 'straight'
themeVariables:
darkMode: true
clusterBkg: '#22272f62'
clusterBorder: '#6a6f77ff'
clusterTextColor: '#6a6f77ff'
lineColor: '#C1C4CAAA'
background: '#262B33'
primaryColor: '#2b4268ff'
primaryTextColor: '#C1C4CAff'
primaryBorderColor: '#6a6f77ff'
primaryLabelBkg: '#262B33'
secondaryColor: '#425f5fff'
secondaryBorderColor: '#8c9c81ff'
secondaryTextColor: '#C1C4CAff'
tertiaryColor: '#4d4962ff'
tertiaryBorderColor: '#8983a5ff'
tertiaryTextColor: '#eeeeee55'
nodeTextColor: '#C1C4CA'
defaultLinkColor: '#C1C4CA'
edgeLabelBackground: '#262B33'
edgeLabelBorderColor: '#C1C4CA'
labelTextColor: '#C1C4CA'
errorBkgColor: '#724848ff'
errorTextColor: '#C1C4CA'
flowchart:
curve: 'basis'
nodeSpacing: 50
rankSpacing: 50
subGraphTitleMargin:
top: 15
bottom: 15
left: 15
right: 15
---
flowchart LR
subgraph GDSCRIPT["GDScript/Editor"]
A[Script Code] --> B[String-based Access]
end
subgraph BINDING["Binding Layer"]
C[Name Resolution] --> D[Type Checking]
D --> E[Parameter Marshalling]
E --> F[Method Invocation]
end
subgraph CPP["C++ Extension"]
G[Native Methods] --> H[Type-Safe Code]
end
B -->|Runtime Lookup| C
F -->|Direct Call| G
linkStyle default stroke:#C1C4CAaa,stroke-width:2px,color:#C1C4CA
style A fill:#7a7253ff,stroke:#c7c19bff,stroke-width:2px,color:#C1C4CA,rx:8,ry:8
style B fill:#7a6253ff,stroke:#c7ac9bff,stroke-width:2px,color:#C1C4CA,rx:8,ry:8
style C fill:#2b4268ff,stroke:#779DC9ff,stroke-width:2px,color:#C1C4CA,rx:8,ry:8
style D fill:#4d4962ff,stroke:#8983a5ff,stroke-width:2px,color:#C1C4CA,rx:8,ry:8
style E fill:#3a3f47ff,stroke:#6a6f77ff,stroke-width:2px,color:#C1C4CA,rx:8,ry:8
style F fill:#2b4268ff,stroke:#779DC9ff,stroke-width:2px,color:#C1C4CA,rx:8,ry:8
style G fill:#425f5fff,stroke:#8c9c81ff,stroke-width:2px,color:#C1C4CA,rx:8,ry:8
style H fill:#425f5fff,stroke:#8c9c81ff,stroke-width:2px,color:#C1C4CA,rx:8,ry:8
What Are Bindings
The bridge between worlds: Bindings are the metadata and infrastructure that allow Godot’s dynamic scripting system to interact with your static C++ code. They include information about parameter types, return values, method names, and function pointers, plus the code that converts between C++ types and Godot’s universal “Variant” type.
Definition and Purpose
Bindings are the metadata and function pointer mappings that allow the Godot engine to interact with C++ code. They serve several critical purposes:
- Name Resolution: Map string names (from GDScript/editor) to C++ methods
- Type Safety: Validate parameter types at call boundaries
- Marshalling: Convert between Variant and C++ types
- Documentation: Provide method signatures for the editor
- Reflection: Enable runtime introspection of classes
Binding Components
// A binding consists of several components:
struct MethodBinding {
StringName name; // Method name visible to engine
MethodBind *method_bind; // Function pointer wrapper
MethodInfo method_info; // Parameter and return type info
Vector<Variant> default_arguments; // Optional default values
uint32_t flags; // Method characteristics
};
// Example of what gets generated for each bound method:
class ExampleBinding {
public:
// Your C++ method
int calculate(float a, float b = 1.0f) {
return static_cast<int>(a * b);
}
// Generated binding infrastructure:
static MethodBind *bind_calculate;
static void _bind_methods() {
// Creates MethodBind with type information
bind_calculate = ClassDB::bind_method(
D_METHOD("calculate", "a", "b"), // Name and parameters
&ExampleBinding::calculate, // Function pointer
DEFVAL(1.0f) // Default argument
);
// MethodBind stores:
// - Function pointer: &ExampleBinding::calculate
// - Return type: Variant::INT
// - Parameter types: [Variant::FLOAT, Variant::FLOAT]
// - Parameter names: ["a", "b"]
// - Default values: [null, 1.0]
}
};
Binding Architecture
How the binding system is organized: At the heart of the binding system is ClassDB - a global registry that stores information about all registered classes. When you call _bind_methods(), you’re adding entries to this database. During runtime, when GDScript calls a method, ClassDB looks up the binding information and routes the call to your C++ code.
ClassDB Structure
The ClassDB is the central registry for all class information:
// Simplified ClassDB structure
class ClassDB {
private:
static HashMap<StringName, ClassInfo> classes;
struct ClassInfo {
StringName name;
StringName parent_name;
ClassInfo *parent_ptr;
// Method bindings
HashMap<StringName, MethodBind*> method_map;
// Property bindings
HashMap<StringName, PropertyInfo> property_map;
HashMap<StringName, PropertySetGet> property_setget;
// Signal definitions
HashMap<StringName, MethodInfo> signal_map;
// Virtual methods
HashMap<StringName, GDExtensionClassCallVirtual> virtual_methods;
// Class creation function
GDExtensionClassCreationInfo creation_info;
};
public:
template<class T>
static void register_class() {
ClassInfo info;
info.name = T::get_class_static();
info.parent_name = T::get_parent_class_static();
// Initialize class
T::initialize_class();
// Call _bind_methods to register everything
T::_bind_methods();
classes[info.name] = info;
}
};
Binding Lifecycle
---
config:
theme: 'base'
themeVariables:
darkMode: true
background: '#262B33'
primaryColor: '#2b4268ff'
primaryTextColor: '#C1C4CA'
primaryBorderColor: '#779DC9'
lineColor: '#C1C4CA'
actorBkg: '#2b4268ff'
actorBorder: '#779DC9'
actorTextColor: '#C1C4CA'
actorLineColor: '#779DC9'
activationBorderColor: '#c7ac9bff'
activationBkgColor: '#7a6253ff'
sequenceNumberColor: '#C1C4CA'
noteBkgColor: '#3a3f47ff'
noteTextColor: '#C1C4CA'
noteBorderColor: '#6a6f77ff'
labelBoxBkgColor: '#425f5fff'
labelBoxBorderColor: '#8c9c81ff'
labelTextColor: '#C1C4CA'
loopTextColor: '#C1C4CA'
altTextColor: '#C1C4CA'
messageBkgColor: '#262B33'
messageTextColor: '#C1C4CA'
---
sequenceDiagram
participant Init as Initialization
participant ClassDB as ClassDB
participant Bind as _bind_methods
participant Engine as Godot Engine
Init->>ClassDB: register_class<MyClass>()
ClassDB->>Bind: Call MyClass::_bind_methods()
Bind->>ClassDB: bind_method("my_method")
Bind->>ClassDB: ADD_PROPERTY("my_property")
Bind->>ClassDB: ADD_SIGNAL("my_signal")
ClassDB->>Engine: Register with GDExtension API
Engine-->>ClassDB: Class registered
Note over Engine: Class now available in GDScript
Method Binding
Making your C++ functions callable: Method binding is the process of registering your C++ functions so they can be called from GDScript, connected to signals, or invoked by the editor. The binding system automatically handles converting GDScript’s dynamic arguments to your C++ parameter types and converting your return value back.
Basic Method Binding
class MyClass : public Node {
GDCLASS(MyClass, Node)
protected:
static void _bind_methods() {
// Simple method binding
ClassDB::bind_method(D_METHOD("simple_method"), &MyClass::simple_method);
// Method with parameters
ClassDB::bind_method(D_METHOD("add", "a", "b"), &MyClass::add);
// Method with default arguments
ClassDB::bind_method(D_METHOD("multiply", "a", "b"),
&MyClass::multiply, DEFVAL(2.0));
// Static method binding
ClassDB::bind_static_method("MyClass",
D_METHOD("static_helper", "input"),
&MyClass::static_helper);
}
public:
void simple_method() {
print_line("Called from GDScript!");
}
int add(int a, int b) {
return a + b;
}
float multiply(float a, float b = 2.0f) {
return a * b;
}
static String static_helper(const String &input) {
return "Processed: " + input;
}
};
Advanced Method Binding
class AdvancedBinding : public Resource {
GDCLASS(AdvancedBinding, Resource)
protected:
static void _bind_methods() {
// Method with complex return type
MethodBind *bind = ClassDB::bind_method(
D_METHOD("get_data_structure"),
&AdvancedBinding::get_data_structure
);
// Set method flags
bind->set_flag(METHOD_FLAG_CONST, true);
// Vararg method (variable arguments)
{
MethodInfo mi("format_string", PropertyInfo(Variant::STRING, ""));
mi.arguments.push_back(PropertyInfo(Variant::STRING, "format"));
mi.flags = METHOD_FLAG_VARARG;
ClassDB::bind_vararg_method(METHOD_FLAGS_DEFAULT,
"format_string",
&AdvancedBinding::format_string,
mi);
}
// Method with enum parameter
ClassDB::bind_method(D_METHOD("set_mode", "mode"),
&AdvancedBinding::set_mode);
// Register enum for the method
BIND_ENUM_CONSTANT(MODE_NORMAL);
BIND_ENUM_CONSTANT(MODE_ADVANCED);
BIND_ENUM_CONSTANT(MODE_EXPERT);
}
public:
enum Mode {
MODE_NORMAL,
MODE_ADVANCED,
MODE_EXPERT
};
Dictionary get_data_structure() const {
Dictionary dict;
dict["key"] = "value";
dict["number"] = 42;
return dict;
}
Variant format_string(const Variant **p_args, int p_argcount,
Callable::CallError &r_error) {
if (p_argcount < 1) {
r_error.error = Callable::CallError::CALL_ERROR_TOO_FEW_ARGUMENTS;
r_error.argument = 1;
return Variant();
}
String format = *p_args[0];
String result = format;
for (int i = 1; i < p_argcount; i++) {
result = result.replace("{" + itos(i-1) + "}",
p_args[i]->stringify());
}
return result;
}
void set_mode(Mode p_mode) {
// Handle enum parameter
}
};
Method Binding Internals
// How method binding works internally
template<class T, class R, class... Args>
class MethodBindT : public MethodBind {
R (T::*method)(Args...);
public:
MethodBindT(R (T::*p_method)(Args...)) : method(p_method) {}
virtual Variant call(Object *p_object, const Variant **p_args,
int p_arg_count, Callable::CallError &r_error) override {
T *instance = Object::cast_to<T>(p_object);
// Validate argument count
if (p_arg_count != sizeof...(Args)) {
r_error.error = Callable::CallError::CALL_ERROR_TOO_FEW_ARGUMENTS;
return Variant();
}
// Convert Variants to native types and call
return call_with_variant_args(instance, method, p_args);
}
private:
template<std::size_t... Is>
R call_with_variant_args_helper(T *p_instance, const Variant **p_args,
std::index_sequence<Is...>) {
// Convert each Variant to the expected type
return (p_instance->*method)(
VariantCaster<typename std::tuple_element<Is, std::tuple<Args...>>::type>
::cast(*p_args[Is])...
);
}
R call_with_variant_args(T *p_instance, const Variant **p_args) {
return call_with_variant_args_helper(p_instance, p_args,
std::index_sequence_for<Args...>{});
}
};
Property Binding
Exposing variables to the editor and GDScript: Property binding makes your C++ member variables visible in the editor’s inspector and accessible from GDScript. Unlike simple variables, bound properties can have validation, hints for the editor (like sliders or color pickers), and automatic save/load behavior.
Property Registration
class PropertyExample : public Node {
GDCLASS(PropertyExample, Node)
private:
int health = 100;
float speed = 5.0f;
String player_name = "Player";
Ref<Texture2D> icon;
Color tint_color = Color(1, 1, 1, 1);
Vector3 spawn_position;
protected:
static void _bind_methods() {
// Basic property binding
ClassDB::bind_method(D_METHOD("set_health", "health"),
&PropertyExample::set_health);
ClassDB::bind_method(D_METHOD("get_health"),
&PropertyExample::get_health);
ADD_PROPERTY(PropertyInfo(Variant::INT, "health"),
"set_health", "get_health");
// Property with hint
ClassDB::bind_method(D_METHOD("set_speed", "speed"),
&PropertyExample::set_speed);
ClassDB::bind_method(D_METHOD("get_speed"),
&PropertyExample::get_speed);
ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "speed",
PROPERTY_HINT_RANGE, "0.0,100.0,0.1"),
"set_speed", "get_speed");
// String property with placeholder
ClassDB::bind_method(D_METHOD("set_player_name", "name"),
&PropertyExample::set_player_name);
ClassDB::bind_method(D_METHOD("get_player_name"),
&PropertyExample::get_player_name);
ADD_PROPERTY(PropertyInfo(Variant::STRING, "player_name",
PROPERTY_HINT_PLACEHOLDER_TEXT,
"Enter player name..."),
"set_player_name", "get_player_name");
// Resource property
ClassDB::bind_method(D_METHOD("set_icon", "icon"),
&PropertyExample::set_icon);
ClassDB::bind_method(D_METHOD("get_icon"),
&PropertyExample::get_icon);
ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "icon",
PROPERTY_HINT_RESOURCE_TYPE, "Texture2D"),
"set_icon", "get_icon");
// Color property
ClassDB::bind_method(D_METHOD("set_tint_color", "color"),
&PropertyExample::set_tint_color);
ClassDB::bind_method(D_METHOD("get_tint_color"),
&PropertyExample::get_tint_color);
ADD_PROPERTY(PropertyInfo(Variant::COLOR, "tint_color"),
"set_tint_color", "get_tint_color");
// Vector3 property with suffix
ClassDB::bind_method(D_METHOD("set_spawn_position", "position"),
&PropertyExample::set_spawn_position);
ClassDB::bind_method(D_METHOD("get_spawn_position"),
&PropertyExample::get_spawn_position);
ADD_PROPERTY(PropertyInfo(Variant::VECTOR3, "spawn_position",
PROPERTY_HINT_NONE, "",
PROPERTY_USAGE_DEFAULT, "suffix:m"),
"set_spawn_position", "get_spawn_position");
// Property groups for organization
ADD_GROUP("Player Stats", "stat_");
ADD_PROPERTY(PropertyInfo(Variant::INT, "stat_level"),
"set_level", "get_level");
ADD_PROPERTY(PropertyInfo(Variant::INT, "stat_experience"),
"set_experience", "get_experience");
ADD_SUBGROUP("Combat", "combat_");
ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "combat_damage"),
"set_damage", "get_damage");
}
public:
// Setters and getters
void set_health(int p_health) {
health = CLAMP(p_health, 0, 100);
// Notify editor of change
notify_property_list_changed();
}
int get_health() const { return health; }
void set_speed(float p_speed) {
speed = p_speed;
}
float get_speed() const { return speed; }
// Validation in setter
void set_player_name(const String &p_name) {
if (p_name.is_empty()) {
player_name = "Player";
} else {
player_name = p_name;
}
}
String get_player_name() const { return player_name; }
};
Dynamic Properties
class DynamicProperties : public Resource {
GDCLASS(DynamicProperties, Resource)
private:
Dictionary custom_properties;
bool advanced_mode = false;
protected:
static void _bind_methods() {
ClassDB::bind_method(D_METHOD("set_advanced_mode", "enabled"),
&DynamicProperties::set_advanced_mode);
ClassDB::bind_method(D_METHOD("get_advanced_mode"),
&DynamicProperties::get_advanced_mode);
ADD_PROPERTY(PropertyInfo(Variant::BOOL, "advanced_mode"),
"set_advanced_mode", "get_advanced_mode");
}
// Dynamic property list based on state
virtual void _get_property_list(List<PropertyInfo> *p_list) const override {
// Add properties based on mode
if (advanced_mode) {
p_list->push_back(PropertyInfo(Variant::FLOAT, "advanced_setting_1",
PROPERTY_HINT_RANGE, "0,1,0.01"));
p_list->push_back(PropertyInfo(Variant::INT, "advanced_setting_2",
PROPERTY_HINT_ENUM, "Option1,Option2,Option3"));
}
// Add dynamic properties from dictionary
for (const KeyValue<Variant, Variant> &E : custom_properties) {
String name = E.key;
p_list->push_back(PropertyInfo(Variant::NIL, name,
PROPERTY_HINT_NONE, "",
PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_SCRIPT_VARIABLE));
}
}
// Handle dynamic property access
virtual bool _set(const StringName &p_name, const Variant &p_value) override {
String name = p_name;
if (name.begins_with("advanced_setting_")) {
// Handle advanced settings
custom_properties[name] = p_value;
return true;
}
// Check if it's a custom property
if (custom_properties.has(name)) {
custom_properties[name] = p_value;
return true;
}
return false;
}
virtual bool _get(const StringName &p_name, Variant &r_ret) const override {
String name = p_name;
if (custom_properties.has(name)) {
r_ret = custom_properties[name];
return true;
}
return false;
}
public:
void set_advanced_mode(bool p_enabled) {
advanced_mode = p_enabled;
notify_property_list_changed(); // Refresh property list
}
bool get_advanced_mode() const { return advanced_mode; }
};
Signal Binding
Declaring events your class can emit: Signal binding registers the signals (events) that your class can emit. This tells Godot what parameters the signal carries and allows other objects to connect to your signals through the editor or GDScript. Signals enable loose coupling between game systems.
Signal Registration and Usage
class SignalExample : public Node {
GDCLASS(SignalExample, Node)
protected:
static void _bind_methods() {
// Simple signal
ADD_SIGNAL(MethodInfo("health_depleted"));
// Signal with parameters
ADD_SIGNAL(MethodInfo("health_changed",
PropertyInfo(Variant::INT, "old_health"),
PropertyInfo(Variant::INT, "new_health")));
// Signal with complex parameter
ADD_SIGNAL(MethodInfo("item_collected",
PropertyInfo(Variant::OBJECT, "item",
PROPERTY_HINT_RESOURCE_TYPE, "Item"),
PropertyInfo(Variant::INT, "quantity")));
// Signal that returns a value
MethodInfo damage_taken;
damage_taken.name = "damage_taken";
damage_taken.return_val = PropertyInfo(Variant::BOOL, "handled");
damage_taken.arguments.push_back(PropertyInfo(Variant::INT, "amount"));
damage_taken.arguments.push_back(PropertyInfo(Variant::INT, "damage_type"));
ADD_SIGNAL(damage_taken);
}
private:
int health = 100;
public:
void take_damage(int amount, int damage_type) {
int old_health = health;
health -= amount;
// Emit signal and check return value
bool handled = emit_signal("damage_taken", amount, damage_type);
if (!handled) {
// Default damage handling
health = Math::max(0, health);
}
// Emit health changed signal
emit_signal("health_changed", old_health, health);
if (health <= 0) {
emit_signal("health_depleted");
}
}
void collect_item(const Ref<Item> &item, int quantity) {
emit_signal("item_collected", item, quantity);
}
};
Virtual Method Binding
Allowing GDScript to override your C++ methods: Virtual method binding enables GDScript classes that inherit from your C++ class to override specific methods. This is how you create extensible base classes in C++ that can be customized through scripting without recompiling.
Registering Virtual Methods
class VirtualMethodExample : public Node {
GDCLASS(VirtualMethodExample, Node)
protected:
static void _bind_methods() {
// Bind virtual methods that can be overridden in GDScript
GDVIRTUAL_BIND(_ready);
GDVIRTUAL_BIND(_process, "delta");
GDVIRTUAL_BIND(_physics_process, "delta");
// Custom virtual method
GDVIRTUAL_BIND(_on_damage_received, "amount", "source");
// Virtual method with return value
GDVIRTUAL_BIND(_calculate_damage, "base_damage", "multiplier");
}
public:
// Built-in virtual methods
virtual void _ready() override {
// C++ implementation
print_line("VirtualMethodExample ready");
// Call GDScript override if exists
GDVIRTUAL_CALL(_ready);
}
virtual void _process(double delta) override {
// Check if GDScript has override
bool overridden = false;
GDVIRTUAL_IS_OVERRIDDEN_PTR(ptr, _process);
if (ptr) {
overridden = true;
GDVIRTUAL_CALL(_process, delta);
}
if (!overridden) {
// Default C++ behavior
default_process(delta);
}
}
// Custom virtual methods
void receive_damage(int amount, Node *source) {
// Call virtual method
GDVIRTUAL_CALL(_on_damage_received, amount, source);
// Can also check return value
int calculated_damage = 0;
if (GDVIRTUAL_CALL(_calculate_damage, amount, 1.5f, calculated_damage)) {
// GDScript provided calculation
apply_damage(calculated_damage);
} else {
// Use default calculation
apply_damage(amount);
}
}
private:
void default_process(double delta) {
// Default implementation
}
void apply_damage(int amount) {
// Apply the damage
}
// Virtual method declarations for GDScript
GDVIRTUAL0(_ready)
GDVIRTUAL1(_process, double)
GDVIRTUAL1(_physics_process, double)
GDVIRTUAL2(_on_damage_received, int, Node*)
GDVIRTUAL2R(int, _calculate_damage, int, float)
};
Type Conversion and Marshalling
Translating between C++ and GDScript types: GDScript uses a universal “Variant” type that can hold any value, while C++ uses specific types like int, String, and Vector3. The marshalling system automatically converts between these representations when calling bound methods, handling the complexity of type safety and validation.
Variant Conversion
// Type conversion between Variant and C++ types
class TypeConversion {
public:
static void demonstrate_conversions() {
// From C++ to Variant
int cpp_int = 42;
Variant var_int = cpp_int; // Automatic conversion
String cpp_string = "Hello";
Variant var_string = cpp_string;
Vector3 cpp_vec(1, 2, 3);
Variant var_vec = cpp_vec;
// From Variant to C++
Variant variant_value = 3.14;
float cpp_float = variant_value; // Automatic conversion
// Safe conversion with type checking
if (variant_value.get_type() == Variant::FLOAT) {
float safe_float = variant_value.operator float();
}
// Array conversion
Array godot_array;
godot_array.append(1);
godot_array.append("two");
godot_array.append(Vector2(3, 4));
// Dictionary conversion
Dictionary dict;
dict["key"] = "value";
dict["number"] = 42;
Variant var_dict = dict;
}
};
// Custom type marshalling
template<typename T>
struct VariantCaster {
static T cast(const Variant &p_variant);
};
// Specialization for custom type
template<>
struct VariantCaster<MyCustomType> {
static MyCustomType cast(const Variant &p_variant) {
// Convert from Variant to MyCustomType
if (p_variant.get_type() == Variant::DICTIONARY) {
Dictionary dict = p_variant;
MyCustomType result;
result.value = dict.get("value", 0);
result.name = dict.get("name", "");
return result;
}
return MyCustomType();
}
};
Parameter Marshalling
class ParameterMarshalling : public Object {
GDCLASS(ParameterMarshalling, Object)
protected:
static void _bind_methods() {
// Method with multiple parameter types
ClassDB::bind_method(D_METHOD("complex_method", "number", "text",
"position", "data"),
&ParameterMarshalling::complex_method);
}
public:
void complex_method(int number, const String &text,
const Vector3 &position, const Dictionary &data) {
// Parameters are automatically marshalled from Variants
}
// Manual parameter marshalling for varargs
Variant manual_marshalling(const Variant **p_args, int p_argcount,
Callable::CallError &r_error) {
// Validate argument count
if (p_argcount < 2) {
r_error.error = Callable::CallError::CALL_ERROR_TOO_FEW_ARGUMENTS;
r_error.argument = 2;
r_error.expected = 2;
return Variant();
}
// Manual type checking and conversion
if (p_args[0]->get_type() != Variant::INT) {
r_error.error = Callable::CallError::CALL_ERROR_INVALID_ARGUMENT;
r_error.argument = 0;
r_error.expected = Variant::INT;
return Variant();
}
int first_arg = *p_args[0];
String second_arg = p_args[1]->operator String();
// Process arguments
return process_varargs(first_arg, second_arg);
}
private:
Variant process_varargs(int a, const String &b) {
return a + b.to_int();
}
};
Binding Generation Process
How Godot’s built-in classes get their bindings: Most of Godot’s built-in classes (like Node, Resource, etc.) have their bindings automatically generated from the engine’s API definition. The binding generator reads this API information and creates the C++ wrapper classes you use in godot-cpp, complete with proper method signatures and parameter types.
How Bindings Are Generated
The binding generator (binding_generator.py) creates C++ bindings from Godot’s API:
# Conceptual binding generation process
def generate_class_binding(class_api):
"""
Generates C++ binding code for a Godot class
"""
code = []
# Generate class declaration
code.append(f"class {class_api.name} : public {class_api.inherits} {{")
code.append(f" GDCLASS({class_api.name}, {class_api.inherits})")
# Generate method bindings
for method in class_api.methods:
# Generate method declaration
return_type = variant_to_cpp_type(method.return_type)
params = ", ".join(f"{variant_to_cpp_type(p.type)} {p.name}"
for p in method.arguments)
code.append(f" {return_type} {method.name}({params});")
# Generate property accessors
for prop in class_api.properties:
cpp_type = variant_to_cpp_type(prop.type)
code.append(f" void set_{prop.name}({cpp_type} value);")
code.append(f" {cpp_type} get_{prop.name}() const;")
code.append("};")
return "\n".join(code)
Generated Binding Example
// Auto-generated binding for Node3D class
class Node3D : public Node {
GDCLASS(Node3D, Node)
protected:
static void _bind_methods();
public:
// Generated method bindings
void set_position(const Vector3 &p_position);
Vector3 get_position() const;
void set_rotation(const Vector3 &p_rotation);
Vector3 get_rotation() const;
void translate(const Vector3 &p_offset);
void rotate(const Vector3 &p_axis, real_t p_angle);
// Generated property bindings
static void _bind_methods() {
ClassDB::bind_method(D_METHOD("set_position", "position"),
&Node3D::set_position);
ClassDB::bind_method(D_METHOD("get_position"),
&Node3D::get_position);
ADD_PROPERTY(PropertyInfo(Variant::VECTOR3, "position"),
"set_position", "get_position");
// ... more bindings
}
};
// Implementation calls through to engine
void Node3D::set_position(const Vector3 &p_position) {
internal::_call_builtin_method_ptr<void>(_method_set_position,
_owner, &p_position);
}
Vector3 Node3D::get_position() const {
return internal::_call_builtin_method_ptr<Vector3>(_method_get_position,
_owner, nullptr);
}
Performance Considerations
Understanding the cost of bound calls: Every time you call a bound method (either from GDScript to C++ or vice versa), there’s overhead for type conversion and virtual dispatch. While this overhead is usually negligible, it becomes important in performance-critical code like inner loops or frequently called update methods.
Binding Overhead
class PerformanceOptimization : public Node {
GDCLASS(PerformanceOptimization, Node)
private:
// Cache frequently used method binds
static MethodBind *cached_method_bind;
protected:
static void _bind_methods() {
// Store method bind for direct access
cached_method_bind = ClassDB::bind_method(
D_METHOD("frequently_called"),
&PerformanceOptimization::frequently_called
);
}
public:
// Direct C++ call: ~1-2 CPU cycles
void direct_call() {
frequently_called();
}
// Through binding: ~50-100 CPU cycles
void bound_call() {
call("frequently_called");
}
// Optimized bound call with cached MethodBind
void optimized_bound_call() {
const Variant *args = nullptr;
Callable::CallError error;
cached_method_bind->call(this, args, 0, error);
}
void frequently_called() {
// Method implementation
}
// Batch operations to reduce binding overhead
void batch_operation(const Array &items) {
// Single binding call with multiple items
// More efficient than multiple individual calls
for (int i = 0; i < items.size(); i++) {
process_item_internal(items[i]);
}
}
private:
void process_item_internal(const Variant &item) {
// Internal processing without binding overhead
}
};
MethodBind *PerformanceOptimization::cached_method_bind = nullptr;
Best Practices for Performance
class BindingBestPractices : public Node {
GDCLASS(BindingBestPractices, Node)
protected:
static void _bind_methods() {
// 1. Use const methods when possible
ClassDB::bind_method(D_METHOD("get_data"),
&BindingBestPractices::get_data);
ClassDB::get_method("BindingBestPractices", "get_data")->
set_flag(METHOD_FLAG_CONST, true);
// 2. Avoid frequent property updates
// Batch them when possible
ClassDB::bind_method(D_METHOD("update_multiple_properties", "data"),
&BindingBestPractices::update_multiple_properties);
// 3. Use signals sparingly for high-frequency events
// Consider callbacks or direct method calls instead
}
public:
// Const method - can be optimized by compiler
Dictionary get_data() const {
return cached_data;
}
// Batch property updates
void update_multiple_properties(const Dictionary &data) {
// Update all at once, single notification
for (const KeyValue<Variant, Variant> &E : data) {
set(E.key, E.value);
}
notify_property_list_changed(); // Single notification
}
// Use internal methods for hot paths
virtual void _process(double delta) override {
// Hot path - avoid binding overhead
internal_update(delta);
// Only use bindings when necessary
if (should_emit_signal) {
emit_signal("update_complete");
should_emit_signal = false;
}
}
private:
Dictionary cached_data;
bool should_emit_signal = false;
void internal_update(double delta) {
// Performance-critical code without binding overhead
}
};
Conclusion
The binding system is the crucial infrastructure that enables GDExtension to integrate seamlessly with Godot. It provides type safety, automatic marshalling, and reflection capabilities while maintaining reasonable performance. Understanding how bindings work, when they’re invoked, and their performance characteristics is essential for writing efficient GDExtension code. The system’s flexibility allows for everything from simple method exposure to complex dynamic property systems, making it possible to create fully-featured engine extensions that feel native to Godot users.