GDExtension & C# Interop

| | GDScript | C# (.NET) | GDExtension (C++, Rust, ...) | |-|----------|-----------|------------------------------| | Setup | None | .NET SDK, .NET build of Godot | Compiler toolchain, bindings | | Iteration | Instant hot reload | Compile step | Compile ste…

38.1 Choosing a language

| | GDScript | C# (.NET) | GDExtension (C++, Rust, ...) | |-|----------|-----------|------------------------------| | Setup | None | .NET SDK, .NET build of Godot | Compiler toolchain, bindings | | Iteration | Instant hot reload | Compile step | Compile step, restart for some changes | | Performance | Good | Faster for heavy logic | Native speed | | Platforms | All | Desktop + mobile (no web export in 4.x so far) | All you compile for | | Best for | Gameplay, UI, glue | Large codebases, .NET ecosystem | Engine-level systems, heavy algorithms, libraries |

You can mix languages in one project (GDScript + GDExtension works everywhere; C# requires the .NET build).

38.2 GDExtension

GDExtension loads native shared libraries at runtime without recompiling the engine. Classes registered by the extension appear exactly like built-in classes: they show in the *Create Node* dialog, can be extended by GDScript, and have docs.

C++ with godot-cpp (sketch):

The .gdextension file tells Godot which library to load:

Build with SCons (scons platform=windows target=template_debug). Using it from GDScript is seamless:

Other bindings: godot-rust (gdext) for Rust, plus community bindings for Swift, Zig, Go, Nim, and others. Popular GDExtension addons: Terrain3D, LimboAI, Jolt (before integration), Steam (GodotSteam).

// gd_example.h
#include <godot_cpp/classes/sprite2d.hpp>
namespace godot {
class GDExample : public Sprite2D {
	GDCLASS(GDExample, Sprite2D)
	double speed = 1.0;
protected:
	static void _bind_methods();
public:
	void set_speed(double p_speed) { speed = p_speed; }
	double get_speed() const { return speed; }
	void _process(double delta) override;
};
}

// gd_example.cpp
void GDExample::_bind_methods() {
	ClassDB::bind_method(D_METHOD("set_speed", "speed"), &GDExample::set_speed);
	ClassDB::bind_method(D_METHOD("get_speed"), &GDExample::get_speed);
	ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "speed"), "set_speed", "get_speed");
}
[configuration]
entry_symbol = "example_library_init"
compatibility_minimum = "4.3"

[libraries]
windows.debug.x86_64 = "res://bin/libgdexample.windows.template_debug.x86_64.dll"
windows.release.x86_64 = "res://bin/libgdexample.windows.template_release.x86_64.dll"
linux.debug.x86_64 = "res://bin/libgdexample.linux.template_debug.x86_64.so"
macos.debug = "res://bin/libgdexample.macos.template_debug.framework"
var ex := GDExample.new()
ex.speed = 3.0
add_child(ex)

38.3 C# interop

Keep cross-language boundaries small and well-defined (e.g., a C# simulation library used by GDScript gameplay).

// Health.cs
using Godot;

public partial class Health : Node
{
	[Signal] public delegate void DiedEventHandler();
	[Export] public int MaxHealth { get; set; } = 100;
	public int Current { get; private set; } = 100;

	public void Damage(int amount)
	{
		Current = Mathf.Max(Current - amount, 0);
		if (Current == 0) EmitSignal(SignalName.Died);
	}
}
# GDScript calling C#: members use the C# (PascalCase) names
var health = $Health                # node with Health.cs attached
health.Damage(10)
print(health.Current)
health.Died.connect(_on_died)       # C# signals are regular signals
var cs_script := load("res://Health.cs")
var h = cs_script.new()
// C# calling GDScript: use dynamic calls
var gdNode = GetNode("Player");
gdNode.Call("take_damage", 10);
int hp = (int)gdNode.Get("health");
gdNode.Connect("died", Callable.From(OnPlayerDied));
var script = GD.Load<GDScript>("res://utils.gd");
var obj = (GodotObject)script.New();