How to create a Godot Rust GDExtension

Using the Rustlang GDExtension bindings for Godot 4 we are now able to create interfaces to Rust code from GDScript.

This means that we may bundle a library file such as a DLL with our game to give it more speed or access to features that would otherwise not be available.

For example, interaction with hardware such as COM ports or data streams.

This article will explain the basics of how to set up a GDExtension and compile Rust code to work with it to produce an Extension library file.

Project Structure

Start by creating a Godot project. Then create a Rust project from the command line from the root of the Godot project.

For example: cargo new "my-extension" --lib and then rename the newly created directory to rust.

Then you can go git init to set up a Git repository to track your changes.

Also, add an empty .gdignore file to the Rust directory to tell Godot not to import any of the Rust files.

Then configure the .gitignore file to ignore Rust debug and build files.

## Godot 4+ specific ignores
.godot/

## Rust specific ignores
# From compiled files and executables
rust/debug/
rust/target/

# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
rust/Cargo.lock

# These are backup files generated by rustfmt
rust/**/*.rs.bk

# MSVC Windows builds of rustc generate these, which store debugging information
rust/*.pdb

And then make some changes to the Cargo.toml file.

[package]
name = "my-extension" # Appears in the filename of the compiled dynamic library.
version = "0.1.0"
edition = "2021"

[lib]
crate-type = ["cdylib"]  # Compile this crate to a dynamic C library.

[dependencies]
godot = { git = "https://github.com/godot-rust/gdext", branch = "master" }

To compile the extension, cd to the rust directory and type cargo build.

This produces an extension library file with the file extension relevant to your operating system in the target/debug directory such as my-extension.dll or my-extension.so.

You may also compile a release build without debugging overhead with cargo build --release which appears in the target/release directory.

Wiring up Godot to Rust

In your text editor, create a .gdextension file with contents containing the paths to your extension library files, the minimum version of Godot that you support, and a symbol representing the entry point function such as _init but specified as: gdext_rust_init.

Example .gdextension file:

[configuration]
entry_symbol = "gdext_rust_init"
compatibility_minimum = 4.1

[libraries]
linux.debug.x86_64 = "res://rust/target/debug/libmy-extension.so"
linux.release.x86_64 = "res://rust/target/release/libmy-extension.so"
windows.debug.x86_64 = "res://rust/target/debug/my-extension.dll"
windows.release.x86_64 = "res://rust/target/release/my-extension.dll"

The libraries listed are the files that you have built. Also, they are in the scope of the res: path so that Godot will see them when running the game.

When starting Godot Engine, a second file should be generated: res://.godot/extension_list.cfg this lists paths to your .gdextension files and may be manually created.

If you find any other odd files that may have been generated and your project doesn’t work, then delete those files and try again.

So that is the awkward stuff out of the way and we can start coding the actual extension!

Coding a Rust Extension for Godot

The code will be written inside the lib.rs file.

I will base this on the code example from https://godot-rust.github.io/book/intro/index.html and then expand on the material described there.

This is a code template to set up the main struct for an extension with a given type name. It also sets up the code to work with Godot’s C++ functions:

use godot::prelude::*;

struct MyExtension;

#[gdextension]
unsafe impl ExtensionLibrary for MyExtension {}

We want to set up a Class based on one of the Objects or Nodes available in the API. The most basic one is refcounted which allows us to add properties and methods to it, and our intstance will be freed by Godot when it goes out of use.

In this example, I am using a Node2D to get access to methods such as _process that become active when the Node is added to the Scene. So you will find your new Node in the tree of Nodes in the Editor and will add it to your scene manually or do it via code.

use godot::prelude::*;
use godot::engine::Node2D;

#[gdextension]
unsafe impl ExtensionLibrary for GameNode {}

#[derive(GodotClass)]
#[class(base=Node2D)]
struct GameNode {
    energy: f64,
    lives: i32,
    msg: String,

    #[base]
    base: Base<Node2D>
}

The #[derive] attribute registers the GameNode as a class in the Godot engine.

We added some properties (energy, lives and msg). The #[base] attribute declares the base property so that we may access the base node instance with self.

Now we may implement methods to override Godot methods or add new methods. Each Engine class has an I{classname} trait which comes with virtual functions for that specific class.

The init function corresponds to Godot’s _init function and is used to initialize our Node.

use godot::prelude::*;
use godot::engine::Node2D;
use godot::engine::INode2D;

#[gdextension]
unsafe impl ExtensionLibrary for GameNode {}

#[derive(GodotClass)]
#[class(base=Node2D)]
struct GameNode {
    energy: f64,
    lives: i32,
    msg: String,

    #[base]
    base: Base<Node2D>
}

#[godot_api]
impl INode2D for GameNode {
    fn init(base: Base<Node2D>) -> Self {
        godot_print!("Hello, world!"); // Prints to the Godot console
        
        // The constructor for the node that gets returned
        Self {
            energy: 1000.0,
            lives: 10,
            msg,
            base
        }
    }

    // Implement one of the API virtual functions. We don't prefix underscore _
    fn process(&mut self, delta: f64) {
        self.energy -= delta;
    }
}

Add our API functions and signals to the node:

#[godot_api]
impl GameNode {
    #[signal]
    fn got_message();

    #[func]
    pub fn send_message(&mut self, text: GString) {
        self.msg = text.to_string();
        self.base.emit_signal("got_message".into(), &[text.to_variant()]);
    }
}

The above code demonstrates how to add a public method and output a signal.

Notice how we convert the String from Godot format (GString) to Rust (String) and then back to Godot (Variant).

The method may be called from GDScript, and the signal connected to from GDScript. Notice how the signal function does not need to include the parameters that are returned (seems odd to me).

Here is our GDScript code attached to the node in the scene of the game:

extends Node2D

var game_node

func _ready():
	game_node = GameNode.new()
    add_child(game_node)
	game_node.got_message.connect(got_message)
	game_node.send_message("Hello!")

func got_message(msg):
	print("Got message " + msg)

One more thing: after making changes to the Rust code, and recompiling the Extension Library, you probably need to restart the Godot Engine for the changes to take effect.

Godot Rust resource links:

More Articles