| name | gdext |
| description | Working with godot-rust/gdext (Rust GDExtension bindings for Godot) in the stage-godot crate. Use when writing or modifying Rust code that compiles to the GDExtension library. |
gdext — Rust GDExtension for Godot
This skill covers the godot-rust/gdext crate used in crates/stage-godot. That crate compiles to a cdylib loaded by Godot at runtime.
Cargo.toml Setup
[lib]
crate-type = ["cdylib"]
[dependencies]
godot = { version = "0.4", features = ["api-4-5", "experimental-godot-api", "lazy-function-tables"] }
Entry Point
Every gdext library needs exactly one entry point:
use godot::init::*;
struct StageExtension;
#[gdextension]
unsafe impl ExtensionLibrary for StageExtension {}
The #[gdextension] macro:
- Exports the symbol
gdext_rust_init (must match entry_symbol in .gdextension file)
- Auto-registers all
#[derive(GodotClass)] classes — no manual registration
- Is
unsafe because it's FFI initialization
Defining Classes
use godot::prelude::*;
#[derive(GodotClass)]
#[class(base=Node)]
pub struct StageCollector {
base: Base<Node>,
poll_interval: u32,
}
#[godot_api]
impl INode for StageCollector {
fn init(base: Base<Node>) -> Self {
Self { base, poll_interval: 1 }
}
fn ready(&mut self) {
}
fn physics_process(&mut self, _delta: f64) {
self.poll();
}
}
#[godot_api]
impl StageCollector {
#[func]
pub fn get_visible_nodes(&self) -> Array<Dictionary> {
}
#[func]
pub fn poll(&mut self) {
}
}
Base class choice matters:
Base<Node> — scene tree access, has _process/_physics_process, no rendering
Base<Node3D> / Base<Node2D> — has transform in world space
Base<RefCounted> — no scene tree, lightweight data container
Base<Object> — bare minimum, no scene tree
Accessing Base/Self
self.base().get_tree()
self.base().get_parent()
self.base().get_name()
self.base_mut().add_child(node.upcast())
self.base_mut().emit_signal("my_signal".into(), &[])
Scene Tree Traversal
let node = self.base().try_get_node_as::<CharacterBody3D>("enemies/scout_02");
if let Some(enemy) = node {
let pos = enemy.get_global_position();
}
let enemy = self.base().get_node_as::<CharacterBody3D>("enemies/scout_02");
let tree = self.base().get_tree().expect("not in scene tree");
let root = tree.get_root().expect("no root");
let parent = self.base().get_node_as::<Node>("enemies");
let count = parent.get_child_count();
for i in 0..count {
let child = parent.get_child(i).expect("child exists");
let name = child.get_name();
let class = child.get_class();
}
Reading Properties from Any Node
let node = self.base().get_node_as::<Node>("some/node");
let health: Variant = node.upcast::<Object>().get("health".into());
let health_int: i64 = health.to::<i64>();
let body = self.base().get_node_as::<CharacterBody3D>("player");
let velocity = body.get_velocity();
let on_floor = body.is_on_floor();
let properties = node.upcast::<Object>().get_property_list();
Properties and Exports
#[derive(GodotClass)]
#[class(base=Node)]
struct MyClass {
#[export]
speed: f32,
#[var]
internal_id: u32,
base: Base<Node>,
cached_data: Vec<u8>,
}
#[export] implies #[var]. Use #[export] when the human needs to configure it in the editor, #[var] when GDScript needs it but not the inspector.
Signals
#[godot_api]
impl MyClass {
#[signal]
fn data_collected(frame: i64, node_count: i32);
}
self.base_mut().emit_signal(
"data_collected".into(),
&[frame.to_variant(), count.to_variant()],
);
Editor Classes — #[class(tool)]
For code that runs inside the editor (not just in-game):
#[derive(GodotClass)]
#[class(tool, init, base=EditorPlugin)]
struct MyEditorPlugin {
base: Base<EditorPlugin>,
}
#[godot_api]
impl IEditorPlugin for MyEditorPlugin {
fn enter_tree(&mut self) { }
fn exit_tree(&mut self) { }
}
CRITICAL GODOT LIMITATION: GDScript cannot inherit from a GDExtension-derived EditorPlugin (godot#85268). In Stage, we solve this with the hybrid pattern:
- GDExtension provides
StageCollector, StageTCPServer, StageRecorder as plain Node subclasses
- GDScript
plugin.gd is the actual EditorPlugin and instantiates those Rust classes
- Do NOT make GDExtension classes inherit
EditorPlugin
StringName — Cache for Performance
StringName construction is expensive (FFI + interning). Cache at module level:
use godot::builtin::StringName;
use std::sync::OnceLock;
fn sn_health() -> &'static StringName {
static NAME: OnceLock<StringName> = OnceLock::new();
NAME.get_or_init(|| StringName::from("health"))
}
let val = node.get(sn_health().clone());
For property names used in tight loops (frame capture, bulk collection), always cache.
Threading — Main Thread Only for Godot APIs
Godot's scene tree and object APIs are not thread-safe. Gd<T> is neither Send nor Sync.
Rules:
- All scene tree access in
_physics_process, _ready, _process (main thread callbacks) — safe
- Do NOT pass
Gd<T> to std::thread::spawn
- For background computation: collect raw data (positions, Strings, primitives) → send via channel → process in thread → send results back → apply on main thread
- TCP I/O: use non-blocking sockets polled from
_physics_process, or Godot's StreamPeerTCP
fn physics_process(&mut self, _delta: f64) {
let snapshot = self.collect_snapshot();
let _ = self.tx.try_send(snapshot);
self.tcp_server.poll();
}
Calling Methods on Nodes
let node: Gd<Node> = self.base().get_node_as("some/node").upcast();
let result: Variant = node.call("take_damage".into(), &[50i32.to_variant()]);
let mut body = self.base().get_node_as::<CharacterBody3D>("player");
body.set_global_position(Vector3::new(5.0, 0.0, -3.0));
Converting Between Types
let node3d: Gd<Node3D> = ...;
let node: Gd<Node> = node3d.upcast();
let obj: Gd<Object> = node3d.upcast();
let node: Gd<Node> = ...;
let body: Option<Gd<CharacterBody3D>> = node.try_cast::<CharacterBody3D>();
let v: Variant = 42i32.to_variant();
let i: i32 = v.to::<i32>();
let pos: Vector3 = Vector3::new(1.0, 2.0, 3.0);
let arr = [pos.x, pos.y, pos.z];
Common Gotchas
bind() vs bind_mut(): When you hold a Gd<T>, you can't directly call methods. Use bind() for &self access or bind_mut() for &mut self:
let collector: Gd<StageCollector> = ...;
let count = collector.bind().get_node_count();
collector.bind_mut().poll();
init vs manual init: #[class(init)] generates a default init(). Without it, you must implement fn init(base: Base<T>) -> Self in the I{Base} trait impl.
get_node_as panics on wrong type: If the node exists but is a different class, it panics. Use try_get_node_as and handle None.
Properties vs methods for Godot built-ins: Godot's CharacterBody3D.velocity is a property, accessed via get_velocity() / set_velocity() in gdext, not as a field.
.gdextension file must match entry symbol:
[configuration]
entry_symbol = "gdext_rust_init"
compatibility_minimum = "4.5"
reloadable = true
[libraries]
linux.debug.x86_64 = "res://addons/stage/bin/linux/libstage_godot.so"
linux.release.x86_64 = "res://addons/stage/bin/linux/libstage_godot.so"
macos.debug.arm64 = "res://addons/stage/bin/macos/libstage_godot.dylib"
macos.release.arm64 = "res://addons/stage/bin/macos/libstage_godot.dylib"
windows.debug.x86_64 = "res://addons/stage/bin/windows/stage_godot.dll"
windows.release.x86_64 = "res://addons/stage/bin/windows/stage_godot.dll"