How to Use Rust with Godot (gdext)

You use Rust with Godot by leveraging the `gdext` crate (formerly `godot-rust`), which generates safe, idiomatic Rust bindings for the Godot engine via the GDExtension API.

When Godot needs a speed boost

You're building a game. Godot's editor is fantastic. The scene tree, the inspector, the visual workflow. It all clicks. Then you hit the bottleneck. Your pathfinding algorithm is too slow. Your entity manager is drowning in dynamic casts. You want the type safety of Rust to catch bugs before they crash the game, but you don't want to abandon the workflow you love.

You don't have to choose. gdext lets you write Rust code that plugs directly into Godot as if it were a native script. You compile Rust into a shared library, drop it into your project, and Godot treats your Rust structs as first-class Nodes. The editor sees them. The inspector edits their properties. The scene tree manages their lifecycle. You get Rust's performance and safety without losing Godot's visual tools.

How gdext bridges the gap

Godot exposes a C API called GDExtension. This API allows external languages to talk to the engine. Writing bindings against a C API is tedious. You have to manage memory manually, convert types carefully, and handle lifecycle events manually. One mistake and you get a segfault or a memory leak.

gdext automates this. It uses Rust's procedural macros to generate the glue code. You annotate your structs and traits, and gdext produces the C functions Godot expects. It also provides safe wrappers around Godot types. Instead of raw pointers, you get Gd<T>, a handle that tracks the Godot object. Instead of manual type conversion, you get macros and traits that convert i32 to Variant and back.

Think of gdext like a universal adapter for a power outlet. Godot is the wall socket. Rust is your device. The adapter handles the voltage conversion and pin mapping so you can just plug in and use power. You don't need to know how the wiring works inside the wall.

Minimal example: A Rust node

Start by initializing a project with cargo-gdext. This tool generates the correct Cargo.toml and directory structure. It saves you from wrestling with build scripts and manifest files.

use godot::prelude::*;

/// A simple node that logs when it enters the scene.
#[derive(GodotClass)]
#[class(init, base=Node2D)]
struct MyNode {
    // The base field holds the parent Godot object.
    // Composition replaces inheritance in Rust.
    #[base]
    base: Base<Node2D>,
}

#[godot_api]
impl INode2D for MyNode {
    // Called when the node enters the scene tree.
    // Use this for initialization that requires other nodes.
    fn ready(&mut self) {
        info!("MyNode is ready!");
    }

    // Called every frame.
    // _delta is the time in seconds since the last frame.
    fn process(&mut self, _delta: f64) {
        info!("Processing frame in Rust!");
    }
}

The #[derive(GodotClass)] macro tells gdext to generate the registration code. The #[class(init, base=Node2D)] attribute configures the class. init means Godot can instantiate this node. base links it to Godot's Node2D. The #[base] attribute marks the field that wraps the parent object. Rust doesn't support multiple inheritance, so you compose your node by holding a Base<T> field.

The #[godot_api] attribute marks the implementation block. gdext scans this block and generates the C functions for the methods. You implement traits like INode2D to hook into the lifecycle. ready runs when the node is added to the scene. process runs every frame.

Run cargo gdext build to compile the extension. The command produces a shared library in the correct directory. Run cargo gdext run to launch the Godot editor with the extension loaded. The editor sees MyNode as a new class. You can drag it into the scene tree.

Match the gdext crate version to your Godot engine version. A mismatch causes linker errors that look like magic. Check the release notes.

Realistic example: Properties and signals

Real nodes need data. gdext lets you expose Rust fields as Godot properties. You can edit these properties in the inspector. You can also connect signals between Rust and GDScript.

use godot::prelude::*;

/// A node that tracks a score and exposes it to the editor.
#[derive(GodotClass)]
#[class(init, base=Node)]
struct ScoreKeeper {
    #[base]
    base: Base<Node>,

    // #[var] exposes the field as a runtime property.
    // GDScript and other Rust nodes can read and write this.
    #[var]
    score: i32,

    // #[export] makes the field visible in the Godot inspector.
    // You can set the initial value in the editor.
    #[export]
    max_score: i32,
}

#[godot_api]
impl IScoreKeeper for ScoreKeeper {
    fn ready(&mut self) {
        // Initialize state if needed.
        // #[export] fields are set from the editor before ready.
        self.score = 0;
    }

    /// Adds points to the score.
    /// Public methods in #[godot_api] are callable from GDScript.
    fn add_points(&mut self, amount: i32) {
        self.score += amount;

        // Clamp the score to the maximum.
        if self.score > self.max_score {
            self.score = self.max_score;
        }

        // Emit a signal so UI can update.
        // var! converts Rust values to Godot Variants.
        self.base_mut().emit_signal("score_changed", &[var!(self.score)]);
    }
}

The #[var] attribute exposes a field as a property. Godot can serialize this property. Other scripts can access it. The #[export] attribute makes the field visible in the inspector. You can set max_score in the editor, and the value flows into Rust.

The add_points method is public and inside #[godot_api]. Godot exposes this method to GDScript. You can call score_keeper.add_points(10) from a GDScript button handler. The var! macro converts the i32 to a Variant for the signal. Signals bridge Rust and GDScript seamlessly.

The community convention is to use #[export] for editor-visible fields and #[var] for runtime properties. You can combine them. Use #[export] for configuration. Use #[var] for state that scripts manipulate.

Treat #[export] fields as configuration. Don't mutate them in code. Use #[var] for mutable state.

Walking through the lifecycle

When Godot loads your project, it loads the shared library. The library's entry point registers your classes. Godot learns about ScoreKeeper. It sees the properties, methods, and signals. The inspector updates. You can drag ScoreKeeper into the scene.

When the scene loads, Godot instantiates ScoreKeeper. It calls the init function generated by #[class(init)]. This function creates the Rust struct. Then Godot calls ready. Your Rust code runs. You can access other nodes, connect signals, and initialize state.

When the frame updates, Godot calls process if you implemented it. Your Rust code runs. When the node is removed, Godot calls the destructor. Rust drops the value. Memory is freed.

gdext handles the reference counting and type erasure behind the scenes. You write idiomatic Rust. The compiler checks your types. The borrow checker ensures you don't hold references across yields or signals.

Recent versions of gdext support hot reload. You can edit Rust code, run cargo gdext build, and the changes appear in the running editor without restarting. This keeps the iteration loop fast.

Trust gdext to handle the FFI. Your job is the Rust logic.

Pitfalls and compiler errors

Rust's borrow checker protects you, but it fights you when you try to mutate the scene tree while iterating it.

You might try to iterate over children and modify them.

// This code does not compile.
for child in self.base().get_children() {
    // get_children borrows base immutably.
    // set_position requires a mutable borrow.
    child.set_position(Vector2::ZERO);
}

The compiler rejects this with E0502 (cannot borrow as mutable because it is also borrowed as immutable). get_children returns an iterator that borrows the base node. You can't mutate the base or its children while the iterator is alive.

Collect the children first.

// Collect children to break the borrow.
let children = self.base().get_children();
for child in children {
    child.set_position(Vector2::ZERO);
}

Collecting copies the handles. The iterator drops. You can mutate the children safely.

Holding a Gd<Node> doesn't keep the node alive. Nodes are owned by the scene tree. If the scene tree removes the node, the handle becomes invalid. Dereferencing an invalid handle causes a panic or undefined behavior.

Check is_instance_valid() before using a Gd<Node>.

if let Some(node) = gd_node.try_read() {
    // Safe to use node.
} else {
    // Node was freed.
}

gdext provides try_read() and try_write() for safe access. These methods return Option if the handle is invalid. Use them instead of direct dereferencing.

Version drift is another trap. Godot updates its API. gdext must update to match. Using an old gdext with a new Godot version results in missing symbols or ABI mismatches. The linker fails with cryptic errors.

Pin your gdext version to the Godot version you're targeting. Update both together.

Treat Gd<T> like a file handle. It might be closed by the time you try to read it. Check validity.

Decision: Rust vs GDScript vs C++

Use gdext when you need Rust's safety guarantees for complex game logic, physics simulations, or data-heavy systems.

Use gdext when you want to share Rust libraries between your game and other Rust services, like a backend server or a toolchain.

Use gdext when you're building a large project where the borrow checker will save you from runtime bugs that GDScript can't catch.

Reach for GDScript when you're prototyping quickly or writing simple UI logic where performance is not a concern.

Reach for GDScript when you want the fastest iteration loop and don't need the strictness of a compiled language.

Reach for C++ GDExtension when you have an existing C++ codebase or need to interface with legacy libraries that lack Rust bindings.

Pick gdext over manual FFI when you want the compiler to generate bindings and handle type conversions automatically.

Start with GDScript. Move to Rust when you hit a wall. The editor supports both.

Where to go next