Introduction

Welcome to the godot-rust book! This is a work-in-progress user guide for gdext, the Rust binding for Godot 4.

If you're new to Rust, before getting started, it is highly recommended that you familiarize yourself with concepts outlined in the officially maintained Rust Book.

To read the book about gdnative (Godot 3 binding), follow this link.

The purpose of godot-rust

Godot is a batteries-included game engine that fosters a productive and fun gamedev workflow. It ships GDScript as a built-in scripting language and also provides official support for C++ and C# bindings. Its GDExtension mechanism allows more languages to be integrated, in our case Rust.

Rust brings a modern, robust and performant experience to game development. If you are interested in scalability, strong type systems or just enjoy Rust as a language, you may consider using it with Godot, to combine the best of both worlds.

About this project

godot-rust is a community-developed open source project. It is maintained independently of Godot itself, but we are in close contact with engine developers, to foster a steady exchange of ideas. This has allowed us to address a lot of Rust's needs upstream, but also led to improvements of the engine itself in several cases.

Currently supported features

For an up-to-date overview of implementation status, consult issue #24.

Terminology

To avoid confusion, here is an explanation of names and technologies you may encounter over the course of this book:

  • godot-rust: The entire project, encompassing Rust bindings for Godot 3 and 4, as well as related efforts (book, community, etc.).
  • GDExtension: C API provided by Godot 4.
  • GDNative: C API provided by Godot 3.
  • gdext (lowercase): the Rust binding for GDExtension (Godot 4) -- what this book focuses on.
  • gdnative (lowercase): the Rust binding for GDNative (Godot 3).
  • Extension: An extension is a dynamic C library, developed by any language binding (Rust, C++, Swift, ...). It uses the GDExtension API and can be loaded by Godot 4.

GDExtension API: what's new

This section briefly mentions the difference between the native interfaces in Godot 3 and 4 from a functional point of view, without going into Rust.

While the underlying FFI (foreign function interface) layer has been completely rewritten, a lot of concepts remain the same from a user point of view. In particular, Godot's approach with a node-based scene graph, composed of classes in an inheritance relation, has not changed.

That said, there are some notable differences:

  1. No more native scripts

    With GDNative, Rust classes could be registered as native scripts. These scripts are attached to nodes in order to enhance their functionality, analogous to how GDScript scripts could be attached. GDExtension on the other hand directly supports Rust types as engine classes, see also next point.

    Keep this in mind when porting GDScript code to Rust: instead of replacing the GDScript with a native script, you need to change the node type to a Rust class that inherits the node.

  2. First-class citizen types

    In Godot 3, user-defined native classes had lots of limitations in the editor: type annotations were not fully supported, they could not easily be used as custom resources, etc. With GDExtension, user-defined classes in Rust behave much closer to GDScript classes.

  3. Always-on

    There is no differentiation between "tool" and "normal" scripts anymore, as it was the case in GDNative. Rust logic runs as soon as the Godot editor launches, but gdext explicitly changes this behavior. By default, all virtual callbacks (ready, process etc.) are not invoked in editor mode. This behavior can be configured when implementing the ExtensionLibrary trait.

  4. No recompilation while editor is open

    Prior to Godot 4.2, it was not possible to recompile a Rust library and let changes take effect when the game is launched from the editor. This has recently been implemented though, see issue #66231.

**

Getting Started

This chapter guides you through the process of setting up gdext and developing your first application with it.

Note

To read this book, we assume intermediate Rust knowledge. If you are new to Rust, reading the Rust Book first is highly encouraged. You won't need to know 100% of the language, but you should know basic concepts (type system, generics, traits, borrow checking, safety).

Some familiarity with Godot is also necessary, although it is possible to learn gdext together with Godot. However, we won't reiterate basic Godot concepts -- so if you choose that approach, we recommend to read the official Godot tutorial in parallel.

In addition to this book, you can use the following resources to learn more about the project:

Setup

Before we can start writing Rust code, we need to install a few tools.

Godot Engine

While you can write Rust code without the Godot engine, we highly recommend to install Godot for quick feedback loops. For the rest of the tutorial, we assume that you have Godot 4 installed and available either:

  • in your PATH as godot4,
  • or an environment variable called GODOT4_BIN, containing the path to the Godot executable.

Godot from pre-built binaries

Binaries of Godot 4 can be downloaded from the official website.
For beta and older versions, you can also check the download archive.

Installing Godot via command-line

# --- Linux ---
# For Ubuntu or Debian-based distros
apt install godot

# For Fedora/RHEL
dnf install godot

# Distro-independent through Flatpak
flatpak install flathub org.godotengine.Godot


# --- Windows ---
# Windows installations can be made through WinGet
winget install --id=GodotEngine.GodotEngine -e


# --- macOS ---
brew install godot

Other Godot versions

If you plan to target Godot versions different from the latest stable release, please read Selecting a Godot version.

Rust

rustup is the preferred way to install the Rust toolchain. It includes the compiler, standard library, Cargo (the package manager) as well as tools like rustfmt or clippy. Visit the website to download binaries or installers for your platform. Alternatively, you can install it via command-line.

Installing rustup via command-line

# Linux (distro-independent)
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

# Windows
winget install --id=Rustlang.Rustup -e

# macOS
brew install rustup

After installation of rustup and the stable toolchain, you can verify that they are working:

$ rustc --version
rustc 1.74.1 (a28077b28 2023-12-04)

LLVM

Tip

In general, you do NOT need to install LLVM.

This was necessary in the past due to bindgen, which depends on LLVM. However, we now provide pre-built artifacts, so that most users can simply add the Cargo dependency and start immediately. This also significantly reduces initial compile times, as bindgen was quite heavyweight with its many transitive dependencies.

You will still need LLVM if you plan to use the custom-godot feature, for example if you have a forked version of Godot or custom modules. To just use a different API version of Godot, you do not need LLVM though; see Selecting a Godot version.

LLVM binaries can be downloaded from llvm.org. Once installed, you can check whether LLVM's clang compiler is available:

clang -v

Hello World

This page shows you how to develop your own small extension library and load it from Godot. The tutorial is heavily inspired by Creating your first script from the official Godot documentation. It is recommended to follow that alongside this tutorial, in case you're interested how certain GDScript concepts map to Rust.

Table of contents

Directory setup

We assume the following file structure, with separate directories for the Godot and Rust parts:

project_dir
│
├── .git/
│
├── godot/
│   ├── .godot/
│   ├── HelloWorld.gdextension
│   └── project.godot
│
└── rust/
    ├── Cargo.toml
    ├── src/
    │   └── lib.rs
    └── target/
        └── debug/

Create a Godot project

We assume a Godot version of 4.1 or later. Feel free to download the latest stable one. You can download in-development ones, but we do not provide official support for those, so we recommend stable ones.

Open the Godot project manager and create a new Godot 4 project in the godot/ subfolder. Add a Sprite2D to the center of a new scene. We recommend that you follow the Official tutorial and stop at the point where it asks you to create a script.

Run your scene to make sure everything is working. Save the changes and consider versioning each step of the tutorial in Git.

Create a Rust crate

To make a new crate with cargo, open your terminal, navigate to your desired folder and then type:

cargo new "{YourCrate}" --lib

where {YourCrate} will be used as a placeholder for a crate name of your choice. To fit with the file structure, we choose rust as the crate name. --lib is used to create a library (not an executable), but there is some extra configuration that the crate requires.

Open Cargo.toml and modify it as follows:

[package]
name = "rust_project" # Appears in the filename of the compiled dynamic library.
version = "0.1.0"     # You can leave version and edition as-is for now.
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" }

The cdylib crate type is not very common in Rust. Instead of building an application (bin) or a library to be utilized by other Rust code (lib), we create a dynamic library, exposing an interface in the C programming language. This dynamic library is loaded by Godot at runtime, through the GDExtension interface.

Main crate

The main crate of gdext is called godot. At this point, it is still hosted on GitHub; in the future, it will be published to crates.io. To fetch the latest changes, you can regularly run a cargo update (possibly breaking). Keep your Cargo.lock file under version control, so that it's easy to revert updates.

To compile each iteration of the extension as you write code, you can use cargo as you normally do with any other Rust project:

cargo build

This should output to {YourCrate}/target/debug/ at least one variation of a compiled library depending on your setup. As an example, a Rust crate hello on Linux would be compiled to libhello.so:

$ cargo build
   Compiling godot4-prebuilt v0.0.0 
       (https://github.com/godot-rust/godot4-prebuilt?branch=4.1.1#fca6897d)
   Compiling proc-macro2 v1.0.69
   [...]
   Compiling godot v0.1.0 (https://github.com/godot-rust/gdext?branch=master#66df8f47)
   Compiling hello v0.1.0 (/path/to/hello)
    Finished dev [unoptimized + debuginfo] target(s) in 1m 46s

$ ls -l
╭───┬──────────────────────────┬──────╮
│ # │           name           │ type │
├───┼──────────────────────────┼──────┤
│ 0 │ target/debug/build       │ dir  │
│ 1 │ target/debug/deps        │ dir  │
│ 2 │ target/debug/examples    │ dir  │
│ 3 │ target/debug/incremental │ dir  │
│ 4 │ target/debug/libhello.d  │ file │
│ > │ target/debug/libhello.so │ file │
╰───┴──────────────────────────┴──────╯

Wire up Godot with Rust

.gdextension file

This file tells Godot how to load your compiled Rust extension. It contains the path to the dynamic library, as well as the entry point (function) to initialize it with.

First, add an empty .gdextension file anywhere in your godot subfolder. In case you're familiar with Godot 3, this is the equivalent of .gdnlib. In this case, we create res://HelloWorld.gdextension inside the godot subfolder and fill it as follows:

[configuration]
entry_symbol = "gdext_rust_init"
compatibility_minimum = 4.1
reloadable = true

[libraries]
linux.debug.x86_64 =     "res://../rust/target/debug/lib{YourCrate}.so"
linux.release.x86_64 =   "res://../rust/target/release/lib{YourCrate}.so"
windows.debug.x86_64 =   "res://../rust/target/debug/{YourCrate}.dll"
windows.release.x86_64 = "res://../rust/target/release/{YourCrate}.dll"
macos.debug =            "res://../rust/target/debug/lib{YourCrate}.dylib"
macos.release =          "res://../rust/target/release/lib{YourCrate}.dylib"
macos.debug.arm64 =      "res://../rust/target/debug/lib{YourCrate}.dylib"
macos.release.arm64 =    "res://../rust/target/release/lib{YourCrate}.dylib"

The [configuration] section should be copied as-is.

  • Key entry_symbol refers to the entry point function that gdext exposes. We choose "gdext_rust_init", which is gdext's default (but can be configured if needed).
  • Key compatibility_minimum specifies the minimum version of Godot required by your extension to work. Opening the project with a version of Godot lower than this will prevent your extension from running.
    • If you build a plugin to be used by others, set this as low as possible for maximum ecosystem compatibility. This might however limit the features you can use.
  • Key reloadable specifies that the editor should reload the extension when the editor window loses and regains focus. See Godot issue #80284 for more details.
    • If Godot is crashing, you may want to try turning off or removing this setting.

The [libraries] section should be updated to match the paths of your dynamic Rust libraries.

  • The keys on the left are the build targets of the Godot project.
  • The values on the right are the file paths to your dynamic library.
    • The res:// prefix represents the path to files relative to your Godot directory, regardless of where your HelloWorld.gdextension file is. You can learn more about Godot's resource paths here.
    • If you remember the file structure, the godot and rust directories are siblings, so we need to go up one level to reach rust.
  • You can add configurations for as many platforms as you like, if you plan to export your project to those later. At the very least, you need to have your current OS in debug mode.

Tip

You can also employ the use of symbolic links and git submodules and then treat those as regular folders and files. Godot reads those just fine too!

Export paths

When exporting your project, you need to use paths inside res://.
Outside paths like .. are not supported.

Custom Rust targets

If you specify your cargo compilation target via the --target flag or a .cargo/config.toml file, the rust library will be placed in a path name that includes target architecture, and the .gdextension library paths will need to match. For example, for M1 Macs (macos.debug.arm64 and macos.release.arm64), the path would be "res://../rust/target/aarch64-apple-darwin/debug/lib{YourCrate}.dylib".

extension_list.cfg

A second file res://.godot/extension_list.cfg should be generated once you open the Godot editor for the first time. This file lists all extension registered within your project. If the file does not exist, you can also manually create it, simply containing the Godot path to your .gdextension file:

res://HelloWorld.gdextension

Your first Rust extension

.gdignore

If you do not follow the recommended gdext project directory setup of having separate rust/ and godot/ directories and instead place your rust source directly within your Godot project, then please consider adding a .gdignore file at the root folder of your Rust code. This avoids cases where the Rust Compiler may produce a file in your rust folder with an ambiguous extension such as .obj, which the Godot Editor may inappropriately attempt to import, resulting in an error and preventing you from building your project.

Rust entry point

As mentioned earlier, our compiled C library needs to expose an entry point to Godot: a C function that can be called through the GDExtension. Setting this up requires quite some low-level FFI code, which gdext abstracts for you.

In your lib.rs, replace the template with the following:

#![allow(unused)]
fn main() {
use godot::prelude::*;

struct MyExtension;

#[gdextension]
unsafe impl ExtensionLibrary for MyExtension {}
}

There are multiple things going on here:

  1. Place the prelude module from the godot crate into scope. This module contains the most common symbols in the gdext API.
  2. Define a struct called MyExtension. This is just a type tag without data or methods, you can name it however you like.
  3. Implement the ExtensionLibrary trait for our type, and mark it with the #[gdextension] attribute.

The last point declares the actual GDExtension entry point, and the proc-macro attribute takes care of the low-level details.

Creating a Rust class

Now, let's write Rust code to define a class that can be used in Godot.

Every class inherits an existing Godot-provided class (its base class or just base). Rust does not natively support inheritance, but the gdext API emulates it to a certain extent.

Class declaration

In this example, we declare a class called Player, which inherits Sprite2D (a node type):

#![allow(unused)]
fn main() {
use godot::prelude::*;
use godot::engine::Sprite2D;

#[derive(GodotClass)]
#[class(base=Sprite2D)]
struct Player {
    speed: f64,
    angular_speed: f64,

    base: Base<Sprite2D>
}
}

Let's break this down.

  1. The gdext prelude contains the most common symbols. Less frequent classes are located in the engine module.

  2. The #[derive] attribute registers Player as a class in the Godot engine. See API docs for details about #[derive(GodotClass)].

  3. The optional #[class] attribute configures how the class is registered. In this case, we specify that Player inherits Godot's Sprite2D class. If you don't specify the base key, the base class will implicitly be RefCounted, just as if you omitted the extends keyword in GDScript.

  4. We define two fields speed and angular_speed for the logic. These are regular Rust fields, no magic involved. More about their use later.

  5. The Base<T> type is used for the base field, which allows self to access the base instance (via composition, as Rust does not have native inheritance). This enables two methods that can be accessed as self.base() and self.base_mut() on your type (through an extension trait).

    • T must match the declared base class. For example, #[class(base=Sprite2D)] implies Base<Sprite2D>.
    • The name can be freely chosen, but base is a common convention.
    • You do not have to declare this field. If it is absent, you cannot access the base object from within self. This is often not a problem, e.g. in data bundles inheriting RefCounted.

Correct node type

When adding an instance of your Player class to the scene, make sure to select node type Player and not its base Sprite2D. Otherwise, your Rust logic will not run. We will guide you to make that change to your scene later, when you're ready to test it.

If Godot fails to load a Rust class (e.g. due to an error in your extension), it may silently replace it with its base class. Use version control (git) to check for unwanted changes in .tscn files.

Method declaration

Now let's add some logic. We start with overriding the init method, also known as the constructor. This corresponds to GDScript's _init() function.

#![allow(unused)]
fn main() {
use godot::engine::ISprite2D;

#[godot_api]
impl ISprite2D for Player {
    fn init(base: Base<Sprite2D>) -> Self {
        godot_print!("Hello, world!"); // Prints to the Godot console
        
        Self {
            speed: 400.0,
            angular_speed: std::f64::consts::PI,
            base,
        }
    }
}
}

Again, those are multiple pieces working together, let's go through them one by one.

  1. #[godot_api] - this lets gdext know that the following impl block is part of the Rust API to expose to Godot. This attribute is required here; accidentally forgetting it will cause a compile error.

  2. impl ISprite2D - each of the engine classes has a I{ClassName} trait, which comes with virtual functions for that specific class, as well as general-purpose functionality such as init (the constructor) or to_string (String conversion). The trait has no required methods.

  3. The init constructor is an associated function ("static method" in other languages) that takes the base instance as argument and returns a constructed instance of Self. While the base is usually just forwarded, the constructor is the place to initialize all your other fields. In this example, we assign initial values 400.0 and PI.

Now that initialization is sorted out, we can move on to actual logic. We would like to continuously rotate the sprite, and thus override the process() method. This corresponds to GDScript's _process(). If you need a fixed framerate, use physics_process() instead.

#![allow(unused)]
fn main() {
use godot::engine::ISprite2D;

#[godot_api]
impl ISprite2D for Player {
    fn init(base: Base<Sprite2D>) -> Self { /* as before */ }

    fn physics_process(&mut self, delta: f64) {
        // In GDScript, this would be: 
        // rotation += angular_speed * delta
        
        let radians = (self.angular_speed * delta) as f32;
        self.base_mut().rotate(radians);
        // The 'rotate' method requires a f32, 
        // therefore we convert 'self.angular_speed * delta' which is a f64 to a f32
    }
}
}

GDScript uses property syntax here; Rust requires explicit method calls instead. Also, access to base class methods -- such as rotate() in this example -- is done via base() and base_mut() methods.

Direct field access

Do not use the self.base field directly. Use self.base() or self.base_mut() instead, otherwise you won't be able to access and call the base class methods.

This is a point where you can see the result. Compile your code and launch the Godot editor. Right click on your Sprite2D in the scene tree, and choose "Change Type..." Find and choose the Player node type, which will be a child of Sprite2D in the Change Type dialog that appears.

Now, save your changes, and run the scene. The sprite should rotate at a constant speed.

rotating sprite

Tip

Launching the Godot application

While it's possible to open the Godot editor and press the launch button every time you made a change in Rust, this is not the most efficient workflow. Unfortunately there is a GDExtension limitation that prevents recompilation while the editor is open (at least on Windows systems -- it tends to work better on Linux and macOS).

However, if you don't need to modify anything in the editor itself, you can launch Godot from the command-line or even your IDE. Check out the [command-line tutorial][godot-command-line] for more information.

We now add a translation component to the sprite, following the upstream tutorial.

#![allow(unused)]
fn main() {
use godot::engine::ISprite2D;

#[godot_api]
impl ISprite2D for Player {
    fn init(base: Base<Sprite2D>) -> Self { /* as before */ }

    fn physics_process(&mut self, delta: f64) {
        // GDScript code:
        //
        // rotation += angular_speed * delta
        // var velocity = Vector2.UP.rotated(rotation) * speed
        // position += velocity * delta
        
        let radians = (self.angular_speed * delta) as f32;
        self.base_mut().rotate(radians);

        let rotation = self.base().get_rotation();
        let velocity = Vector2::UP.rotated(rotation) * self.speed as f32;
        self.base_mut().translate(velocity * delta as f32);
        
        // or verbose: 
        // let this = self.base_mut();
        // this.set_position(
        //     this.position() + velocity * delta as f32
        // );
    }
}
}

The result should be a sprite that rotates with an offset.

rotating translated sprite

Custom Rust APIs

Say you want to add some functionality to your Player class, which can be called from GDScript. For this, you have a separate impl block, again annotated with #[godot_api]. However, this time we are using an inherent impl (i.e. without a trait name).

Concretely, we add a function to increase the speed, and a signal to notify other objects of the speed change.

#![allow(unused)]
fn main() {
#[godot_api]
impl Player {
    #[func]
    fn increase_speed(&mut self, amount: f64) {
        self.speed += amount;
        self.base_mut().emit_signal("speed_increased".into(), &[]);
    }

    #[signal]
    fn speed_increased();
}
}

#[godot_api] takes again the role of exposing the API to the Godot engine. But there are also two new attributes:

  • #[func] exposes a function to Godot. The parameters and return types are mapped to their corresponding GDScript types.
  • #[signal] declares a signal. A signal can be emitted with the emit_signal method (which every Godot class provides, since it is inherited from Object).

API attributes typically follow the GDScript keyword names: class, func, signal, export, var, ...

That's it for the Hello World tutorial! The following chapters will go into more detail about the various features that gdext provides.

Using the Godot API

In this chapter, you will learn how to interact with the Godot engine from Rust code. After introducing you to builtins and objects, we will delve into engine API calls and discuss gdext-specific idioms surrounding them.

If you are interested in exposing your own Rust symbols to the engine and to GDScript code, check out the chapter Registering Rust symbols. It is however strongly recommended to read this chapter first, as it introduces vital concepts.

Built-in types

The so-called "built-in types" or just "builtins" are the basic types that Godot provides. Notably, these are not classes. See also basic built-in types in Godot.

Table of contents

List of types

Here is an exhaustive list of all built-in types, by category. We use the GDScript names; below, we explain how they map to Rust.

Simple types

  • Boolean: bool
  • Numeric: int, float

Composite types

  • Variant (able to hold anything): Variant
  • String types: String, StringName, NodePath
  • Ref-counted containers: Array (Array[T]), Dictionary
  • Packed arrays: Packed*Array for following element types:
    Byte, Int32, Int64, Float32, Float64, Vector2, Vector3, Color, String
  • Functional: Callable, Signal

Geometric types

  • Vectors: Vector2, Vector2i, Vector3, Vector3i, Vector4, Vector4i
  • Bounding boxes: Rect2, Rect2i, AABB
  • Matrices: Transform2D, Transform3D, Basis, Projection
  • Rotation: Quaternion
  • Geometric objects: Plane

Miscellaneous

  • Color: Color
  • Resource ID: RID

Rust mapping

Rust types in the gdext API represent the corresponding Godot types in the closest way possible. They are used in parameter and return type position of API functions, for example.

Most builtins have a 1:1 equivalent (e.g. Vector2f, Color etc.). The following list highlights some noteworthy mappings:

GDScript typeRust typeRust example expression
inti64-12345
floatf643.14159
realreal (either f32 or f64)real!(3.14159)
StringGString"Some string".into()
StringNameStringName"MyClass".into()
NodePathNodePathNodes/MyNode".into()
Array[T]Array<T>array![1, 2, 3]
ArrayVariantArray
or Array<Variant>
varray![1, "two", true]
DictionaryDictionarydict!{"key": "value"}
AABBAabbAabb::new(pos, size)
ObjectGd<Object>Object::new_alloc()
SomeClassGd<SomeClass>Resource::new_gd()
SomeClass (nullable)Option<Gd<SomeClass>>None
Variant (also implicit)VariantVariant::nil()

Note that Godot does not have nullability information in its class API yet. This means that we have to conservatively assume that objects can be null, and thus use Option<Gd<T>> instead of Gd<T> for object return types. This often needs unnecessary unwrapping.

Nullable types are being looked into on Godot side. If there is no upstream solution for a while, we may consider our own workarounds, but it may come with manual annotation of many APIs.

String types

Godot provides three string types: String (GString in Rust), StringName, and NodePath. GString is used as a general-purpose string, while StringName is often used for identifiers like class or action names. The idea is that StringName is cheap to construct and compare.1

These types all support From traits to convert to/from Rust String, and from &str. You can thus use "some_string".into() expressions. If you need more explicit typing, use StringName::from("some_string").

StringName in particular provides a direct conversion from C-string literals such as c"string", introduced in Rust 1.77. This can be used for static C-strings, i.e. ones that remain allocated for the entire program lifetime. Don't use them for short-lived ones.

Arrays and dictionaries

Godot's linear collection type is Array<T>. It is generic over its element type T, which can be one of the supported Godot types (generally anything that can be represented by Variant). A special type VariantArray is provided as an alias for Array<Variant>, which is used when the element type is dynamically typed.

Dictionary is a key-value store, where both keys and values are Variant. Godot does currently not support generic dictionaries, although this feature is under discussion.

Arrays and dictionaries can be constructed using three macros:

#![allow(unused)]
fn main() {
let a = array![1, 2, 3];          // Array<i64>
let b = varray![1, "two", true];  // Array<Variant>
let c = dict!{"key": "value"};    // Dictionary
}

Their API is similar, but not identical to Rust's standard types Vec and HashMap. An important difference is that Array and Dictionary are reference-counted, which means that clone() will not create an independent copy, but another reference to the same instance. Furthermore, since internal elements are stored as variants, they are not accessible by reference, which is why the [] operator (Index/IndexMut traits) is absent.

#![allow(unused)]
fn main() {
let a = array![0, 11, 22];

assert_eq!(a.len(), 3);
assert_eq!(a.get(1), Some(11));  // Note: by value, not Some(&11).
assert_eq!(a.at(1), 11);         // Panics on out-of-bounds.

let mut b = a.clone();   // Increment reference-count.
b.set(2, 33);            // Modify new ref.
assert_eq!(a.at(2), 33); // Original array has changed.

b.clear();
assert_eq!(b, Array::new()); // new() creates an empty array.
}
#![allow(unused)]
fn main() {
let c = dict! {
    "str": "hello",
    "int": 42,
    "bool": true,
};

assert_eq!(c.len(), 3);
assert_eq!(c.get("str"), Some(&"hello".into()));

let mut d = c.clone();            // Increment reference-count.
d.insert("float", 3.14);          // Modify new ref.
assert!(c.contains_key("float")); // Original dict has changed.
}

To iterate, you can use iter_shared(). This method works almost like iter() on Rust collections, but the name highlights that you do not have unique access to the collection during iteration, since there might exist another reference. This also means it's your responsibility to ensure that the collection is not modified in unintended ways during iteration (which should be safe, but may lead to data inconsistencies).

#![allow(unused)]
fn main() {
let a = array!["one", "two", "three"];
let d = dict!{"one": 1, "two": 2.0, "three": Vector3::ZERO};

for elem in a.iter_shared() {
    println!("Element: {elem}");
}bu

for (key, value) in d.iter_shared() {
    println!("Key: {key}, value: {value}");
}
}

Packed arrays

Packed*Array types are used for storing elements space-efficiently in contiguous memory ("packed"). The * stands for the element type, e.g. PackedByteArray or PackedVector3Array.

#![allow(unused)]
fn main() {
// Create from slices.
let bytes = PackedByteArray::from(&[0x0A, 0x0B, 0x0C]);
let ints = PackedInt32Array::from(&[1, 2, 3]);

// Access as Rust shared/mutable slices.
let bytes_slice: &[u8] = b.as_slice();
let ints_slice: &mut [i32] = i.as_mut_slice();
}

Unlike Array, packed arrays use copy-on-write instead of reference counting. When you clone a packed array, you get a new independent instance. Cloning is cheap as long as you don't modify either instance. Once you use a write operation (anything with &mut self), the packed array will allocate its own memory and copy the data.



Footnotes

1

When constructing StringName from &str or String, the conversion is rather expensive, since it has to go through a conversion to UTF-32. As Rust recently introduced C-string literals (c"hello"), we can now directly construct from them. This is more efficient, but keeps memory allocated until shutdown, so don't use it for rarely used temporaries. See API docs and issue #531 for more information.

Objects

This chapter covers the most central mechanism of the Rust bindings -- one that will accompany you from the Hello-World example to a sophisticated Rust game.

We're talking about objects and the way they integrate into the Godot engine.

Table of contents

Terminology

To avoid confusion, whenever we talk about objects, we mean instances of Godot classes. This amounts to Object (the hierarchy's root) and all classes inheriting directly or indirectly from it: Node, Resource, RefCounted, etc.

In particular, the term "class" also includes user-provided types that are declared using #[derive(GodotClass)], even if Rust technically calls them structs. In the same vein, inheritance refers to the conceptual relation ("Player inherits Sprite2D"), not any technical language implementation.

Objects do not include built-in types such as Vector2, Color, Transform3D, Array, Dictionary, Variant etc. These types, although sometimes called "built-in classes", are not real classes, and we generally do not refer to their instances as objects.

Inheritance

Inheritance is a central concept in Godot. You likely know it already from the node hierarchy, where derived classes add specific functionality. This concept extends to Rust classes, with inheritance being emulated via composition.

Each Rust class has a Godot base class.

  • Typically, a base class is a node type, i.e. it (indirectly) inherits from the class Node. This makes it possible to attach instances of the class to the scene tree. Nodes are manually managed, so you need to either add them to the scene tree or free them manually.
  • If not explicitly specified, the base class is RefCounted. This is useful to move data around, without interacting with the scene tree. "Data bundles" (collection of multiple fields without much logic) should generally use RefCounted.
  • Object is the root of the inheritance tree. It is rarely used directly, but it is the base class of Node and RefCounted. Use it only when you really need it; it requires manual memory management and is harder to handle.

Inheriting custom base classes

You cannot inherit other Rust classes or user-defined classes declared in GDScript.

To create relations between Rust classes, use composition and traits. The library still undergoes some exploration in this area, so best practices for absracting over Rust classes might change in the future.

The Gd smart pointer

Gd<T> is the type you will encounter the most when working with gdext.

It is also the most powerful and versatile type that the library provides.

In particular, its responsibilities include:

  • Holding references to all Godot objects, whether they are engine types like Node2D or your own #[derive(GodotClass)] structs in Rust.
  • Tracking memory management of types that are reference-counted.
  • Safe access to user-defined Rust objects through interior mutability.
  • Detecting destroyed objects and preventing UB (double-free, dangling pointer, etc.).
  • Providing FFI conversions between Rust and engine representations, for engine-provided and user-exposed APIs.

A few practical examples (don't worry if you don't fully understand them yet, they will be explained later on):

  1. Retrieve a node relative to current -- type inferred as Gd<Node3D>:

    #![allow(unused)]
    fn main() {
    // retrieve Gd<Node3D>.
    let child = self.base().get_node_as::<Node3D>("Child");
    }
  2. Load a scene and instantiate it as a RigidBody2D:

    #![allow(unused)]
    fn main() {
    // mob_scene is declared as a field of type Gd<PackedScene>.
    self.mob_scene = load("res://Mob.tscn");
    // instanced is of type Gd<RigidBody2D>.
    let mut instanced = self.mob_scene.instantiate_as::<RigidBody2D>();
    }
  3. A signal handler for the body_entered signal of a Node3D in your custom class:

    #![allow(unused)]
    fn main() {
    #[godot_api]
    impl Player {
        #[func]
        fn on_body_entered(&mut self, body: Gd<Node3D>) {
            // Body holds the reference to the Node3D object that triggered the signal.
        }
    }
    }

Object management and lifetime

When working with Godot objects, it is important to understand how long they live and how or when they are destroyed.

Construction

Not all classes in Godot are constructible; for example, singletons do not provide a constructor.

For all others, the constructor's name depends on the memory management of the class:

  • For reference-counted classes, the constructor is called new_gd (e.g. TcpServer::new_gd())
  • For manually managed classes, it is called new_alloc (e.g. Node2D::new_alloc()).

The new_gd() and new_alloc() functions are imported via extension traits NewGd and NewAlloc, respectively. Those always return the type Gd<Self>. If you type :: after a class name, your IDE should suggest the correct constructor for it.

Instance API

Once alive, Godot objects can be accessed to interact with the engine.

Functionality to query and manage the object's lifetime is directly available on the Gd<T> type. Examples include:

  • instance_id() to obtain Godot's object ID.
  • clone() to create a new reference to the same object.
  • free() to manually destroy objects.
  • == and != to compare objects for identity.

Conversions

You can up- and downcast objects if they stand in an inheritance relation. gdext will statically ensure that the cast makes sense.

Downcasts are done via cast::<U>(). If the cast fails, the method will panic. You can also use try_cast::<U>() to get a Result.

#![allow(unused)]
fn main() {
let node: Gd<Node> = ...;

// "I know this downcast will succeed" -> use cast().
let node2d = node.cast::<Node2D>();
// Alternative syntax:
let node2d: Gd<Node2D> = node.cast();

// Fallible downcast -> use try_cast().
let sprite = node.try_cast::<Sprite2D>();
match sprite {
    Ok(sprite) => { /* access converted Gd<Sprite2D> */ },
    Err(node) => { /* access previous Gd<Node> */ },
}
}

Upcasts are always infallible. You can use upcast::<U>() to consume the value.

#![allow(unused)]
fn main() {
let node2d: Gd<Node2D> = ...;
let node = node2d.upcast::<Node>();
// or, equivalent:
let node: Gd<Node> = node2d.upcast();
}

If you just need a reference, use upcast_ref() or upcast_mut().

#![allow(unused)]
fn main() {
let node2d: Gd<Node2D> = ...;
let node: &Node = node2d.upcast_ref();

let mut refc: Gd<RefCounted> = ...;
let obj: &mut Object = refc.upcast_mut();
}

Destruction

Reference-counted classes, instantiated via new_gd(), are automatically destroyed when the last reference goes out of scope. This includes references that have been shared with the Godot engine (e.g. held by GDScript code).

Classes instantiated via new_alloc() require manual memory management. This means that you either have to explicitly call Gd::free() or let a Godot method such as Node::queue_free() take care of it.

Safety around the dead

Accessing destroyed objects is a common source of bugs in Godot, and can occasionally cause undefined behavior (UB). Not so in godot-rust! We have designed the Gd<T> type to be safe even in the presence of mistakes.

If you try to access a destroyed object, the Rust code will panic. There are also APIs to query for validity, although we generally recommend to fix bugs rather than defensive programming.

Conclusion

Objects are a central concept in the Rust bindings. They represent instances of Godot classes, both engine- and user-defined. We have seen how to construct, manage and destroy them. The next chapter will go into calling Godot functions.

Calling functions

In general, the gdext library maps Godot functions in a way that feels as idiomatic as possible in Rust. Sometimes, signatures differ from GDScript, and this page will go into such differences.

Table of contents

Godot classes

Godot classes are located in the godot::engine module. Some often-used ones like Node, RefCounted, Node3D etc. are additionally re-exported in godot::prelude.

The majority of Godot's functionality is exposed via functions inside classes. Please don't hesitate to check out the API docs.

Godot functions

For methods, the first parameter is the receiver, i.e. the object on which the method is called.

The Rust API infers the mutability information from the GDExtension API and uses either &self or &mut self accordingly. Note that this is informational only and bears no safety implications, but it can help you make code more expressive.

#![allow(unused)]
fn main() {
// Call with &self receiver.
let node = Node::new_alloc();
let path = node.get_path();

// Call with &mut self receiver.
let mut node = Node::new_alloc();
let other: Gd<Node> = ...;
node.add_child(other);
}

Associated functions (called "static" in GDScript) are invoked on the type itself.

#![allow(unused)]
fn main() {
Node::print_orphan_nodes();
}

Singletons

Singleton classes (not to be confused with autoloads, which are sometimes called singletons, too) provide a singleton() function to access the one true instance. Methods are then invoked on that instance:

#![allow(unused)]
fn main() {
let input = Input::singleton();
let jump = input.is_action_pressed("jump");
let mouse_pos = input.get_mouse_position();

// Mutable actions need mut:
let mut input = input;
input.set_mouse_mode(MouseMode::CAPTURED);
}

There are discussions about providing methods directly on the singleton type instead of requiring the singleton() call. This would however lose the mutability information, among a few other things.

Default parameters

GDScript supports default values for parameters. If no argument is passed, then the default value is used. As an example, we can use AcceptDialog.add_button(). The GDScript signature is:

Button add_button(String text, bool right=false, String action="")

So you can call it in the following ways from GDScript:

var dialog = AcceptDialog.new()
var button0 = dialog.add_button("Yes")
var button1 = dialog.add_button("Yes", true)
var button2 = dialog.add_button("Yes", true, "confirm")

In Rust, we still have a base method AcceptDialog::add_button(), which takes no default arguments. It can be called in the usual way:

#![allow(unused)]
fn main() {
let dialog = AcceptDialog::new_alloc();
let button = dialog.add_button("Yes".into());
}

Because Rust does not support default parameters, we have to emulate the other calls differently. We decided to use the builder pattern.

Builder methods in gdext receive the _ex suffix. Such a method takes all required parameters, like the base method. It returns a builder object, which offers methods to set the optional parameters by their name. Eventually, a done() method concludes the builder and returns the result of the Godot function call.

For our example, we have the AcceptDialog::add_button_ex() method. These two calls are exactly equivalent:

#![allow(unused)]
fn main() {
let button = dialog.add_button("Yes".into());
let button = dialog.add_button_ex("Yes".into()).done();
}

You can additionally pass optional arguments using methods on the builder object. Just specify the arguments you need. The nice thing here is that you can use any order, and skip any parameters -- unlike GDScript, where you can only skip ones at the end.

#![allow(unused)]
fn main() {
// Equivalent in GDScript: dialog.add_button("Yes", true, "")
let button = dialog.add_button_ex("Yes".into())
    .right(true)
    .done();

// GDScript: dialog.add_button("Yes", false, "confirm")
let button = dialog.add_button_ex("Yes".into())
    .action("confirm".into())
    .done();

// GDScript: dialog.add_button("Yes", true, "confirm")
let button = dialog.add_button_ex("Yes".into())
    .right(true)
    .action("confirm".into())
    .done();
}

Dynamic calls

Sometimes, you want to invoke functions that are not exposed in the Rust API. These could be functions you wrote inside custom GDScript code, or methods from other GDExtensions.

When you don't have the static information available, you can use Godot's reflection APIs. Godot provides Object.call() among others, which is exposed in two ways in Rust.

If you expect a call to succeed (since you know the GDScript code you wrote), use Object::call(). This method will panic if the call fails, providing a detailed message.

#![allow(unused)]
fn main() {
let node = get_node_as::<Node2D>("path/to/MyScript");

// Declare arguments as a slice of variants.
let args = &["string".to_variant(), 42.to_variant()];

// Call the method dynamically.
let val: Variant = node.call("my_method", args);

// Convert to a known type (may panic; try_to() doensn't).
let vec2 = val.to::<Vector2>();
}

If instead you want to handle the failure case, use Object::try_call(). This method returns a Result with the result or a CallError error.

#![allow(unused)]
fn main() {
let result: Result<Variant, CallError> = node.try_call("my_method", args);

match result {
    Ok(val) => {
        let vec2 = val.to::<Vector2>();
        // ...
    }
    Err(err) => {
        godot_print!("Error calling method: {}", err);
    }
}
}

Registering Rust symbols

This chapter teaches how you make your own Rust code available to Godot. You do this by registering individual symbols (classes, functions etc.) in the engine.

Starting with class registration, the chapter then goes into the details of registering functions, properties, signals and constants.

Proc-macro API

The proc-macro API is currently the only way to register Rust symbols. A variety of procedural macros (derive and attribute macros) are provided to decorate your Rust items, such as structs or impl blocks. Behind the scenes, these macros generate the necessary glue code to register each item with Godot.

The library is designed in a way that you can use all your existing knowledge and simply extend it with macro syntax, rather than having to learn a completely new way of doing things. We try to avoid foreign DSLs (domain-specific languages) and instead build on top of Rust's existing syntax.

This approach does a respectable job at limiting the amount of boilerplate code you have to write, and thus makes it much easier for you to focus on the important bits. For example, you will rarely have to repeat yourself more than necessary or register one thing in multiple places (e.g. declare a method, mention it in another register method and then repeat its name yet again as a string literal).

"Exporting"

The term "exporting" is sometimes erroneously used. Please avoid talking about "exporting classes" or "exporting methods" if you mean "registering". This can often cause confusion, especially among beginners.

Export already has two well-defined meanings in the context of Godot:

  1. Exporting a property. This does not register the property with Godot, but renders it visible in the editor.

  2. Exporting projects, meaning bundling them for release.

    • The editor provides a UI to build release versions of your game or application, so they can run as a standalone executable. This process of building the executable is called "exporting".
    • See also Exporting projects.

Registering classes

Classes are the backbone of data modeling in Godot. If you want to build complex user-defined types in a type-safe way, you won't get around classes. Arrays, dictionaries and simple types only get you so far, and overusing them defeats the purpose of using a statically typed language.

Rust makes class registration straightforward. As mentioned before, Rust syntax is used as a baseline, with gdext-specific additions.

See also GDScript reference for classes.

Table of contents

Defining a Rust struct

In Rust, Godot classes are represented by structs. Structs are defined as usual and can contain any number of fields. To register them with Godot, you need to derive the GodotClass trait.

GodotClass trait

The GodotClass trait marks all classes known in Godot. It is already implemented for engine classes, for example Node or Resource. If you want to register your own classes, you need to implement GodotClass as well.

#[derive(GodotClass)] streamlines this process and takes care of all the boilerplate.
See API docs for detailed information.

Let's define a simple class named Monster:

#![allow(unused)]
fn main() {
#[derive(GodotClass)]
struct Monster {
    name: String,
    hitpoints: i32,
}
}

That's it. Immediately after compiling, this class becomes available in Godot through hot reloading (before Godot 4.2, after restart). It won't be very useful yet, but the above definition is enough to register Monster in the engine.

Auto-registration

#[derive(GodotClass)] automatically registers the class -- you don't need an explicit add_class() registration call or a central list mentioning all classes.

The proc-macro internally registers the class in such a list at startup time.

Selecting a base class

By default, the base class of a Rust class is RefCounted. This is consistent with GDScript when you omit the extends keyword.

RefCounted is quite useful for data bundles. As implied by the name, it allows sharing instances tracked by a reference counter; as such, you don't need to worry about memory management. Resource is a subclass of RefCounted and is useful for data that needs to be serialized to the filesystem.

However, if you want your class to be part of the scene tree, you need to use Node (or one of its derived classes) as a base class.

Here, we use a more concrete node type, Node3D. This is done by specifying #[class(base=Node3D)] on the struct definition:

#![allow(unused)]
fn main() {
#[derive(GodotClass)]
#[class(base=Node3D)]
struct Monster {
    name: String,
    hitpoints: i32,
}
}

The base field

Since Rust does not have inheritance, we need to use composition to achieve the same effect. gdext provides a Base<T> type, which lets us store the instance of the Godot superclass (base class) as a field in our Monster class.

#![allow(unused)]
fn main() {
#[derive(GodotClass)]
#[class(base=Node3D)]
struct Monster {
    name: String,
    hitpoints: i32,
    base: Base<Node3D>,
}
}

The important part is the Base<T> type. T must match the base class you specified in the #[class(base=...)] attribute. You can also use the associated type Self::Base for T.

When you declare a base field in your struct, the #[derive] procedural macro will automatically detect the Base<T> type.[^inference] This lets you access the Node API through provided methods self.base() and self.base_mut(), but more on this later.

Conclusion

You have learned how to define a Rust class and register it with Godot. You now know that different base classes exist and how to select one.

The next chapters cover functions and constructors.



[^inference] You can tweak the type detection using the #[hint] attribute, see the corresponding docs.

Registering functions

Functions are essential in any programming language to execute logic. The gdext library allows you to register functions, so that they can be called from the Godot engine and GDScript.

Registration of functions happens always inside impl blocks that are annotated with #[godot_api].

See also GDScript reference for functions.

Table of contents

Godot special functions

Interface traits

Each engine class comes with an associated trait, which has the same name but is prefixed with the letter I, for "Interface". The trait has no required functions, but you can override any functions to customize the behavior towards Godot.

Any impl block for the trait must be annotated with the #[godot_api] attribute macro.

godot_api macro

The attribute proc-macro #[godot_api] is applied to impl blocks and marks their items for registration. It takes no arguments.

See API docs for detailed information.

Functions provided by the interface trait (beginning with I) are called Godot special functions. These can be overridden and allow you to influence the behavior of an object. Most common is a hook into the lifecycle of your object, defining some logic that is run upon certain events like creation, scene-tree entering, or per-frame updates.

In our case, the Node3D comes with the INode3D trait. Here is a small selection of its lifecycle methods. For a complete list, see INode3D docs.

#![allow(unused)]
fn main() {
#[godot_api]
impl INode3D for Monster {
    // Instantiate the object.
    fn init(base: Base<Node3D>) -> Self { ... }
    
    // Called when the node is ready in the scene tree.
    fn ready(&mut self) { ... }
    
    // Called every frame.
    fn process(&mut self, delta: f64) { ... }
    
    // Called every physics frame.
    fn physics_process(&mut self, delta: f64) { ... }
    
    // String representation of the object.
    fn to_string(&self) -> GString { ... }
    
    // Handle user input.
    fn input(&mut self, event: Gd<InputEvent>) { ... }
    
    // Handle lifecycle notifications.
    fn on_notification(&mut self, what: Node3DNotification) { ... }
}
}

As you see, some methods take &mut self and some take &self, depending on whether they typically mutate the object or not. Some also have return values, which are passed back into the engine. For example, the GString returned from to_string() is used if you print an object in GDScript.

So let's implement to_string(), here again showing the class definition for quick reference.

#![allow(unused)]
fn main() {
#[derive(GodotClass)]
#[class(base=Node3D)]
struct Monster {
    name: String,
    hitpoints: i32
    
    base: Base<Node3D>,
}

#[godot_api]
impl INode3D for Monster {      
    fn to_string(&self) -> GString {
        let Self { name, hitpoints, .. } = &self;
        format!("Monster(name={name}, hp={hitpoints})").into()
    }
}
}

User-defined functions

Methods

Besides Godot special functions, you can register your own functions. You need to declare them inside an inherent impl block, also annotated with #[godot_api].

Each function needs a #[func] attribute to register it with Godot. You can omit #[func] as well, but functions defined like that are only visible to Rust code.

Let's add two methods to our Monster class: one that deals damage to the monster, and one that returns its name.

#![allow(unused)]
fn main() {
#[godot_api]
impl Monster {
    #[func]
    fn damage(&mut self, amount: i32) {
        self.hitpoints -= amount;
    }
    
    #[func]
    fn get_name(&self) -> GString {
        self.name.clone()
    }
}
}

The above methods are now available in GDScript. You can call them as follows:

var monster = Monster.new()
# ...
monster.damage(10)
print("A monster called ", monster.get_name())

As you see, the Rust types are automatically mapped to their GDScript counterparts. In this case, i32 becomes int and GString becomes String. Sometimes there are multiple possible mappings, e.g. Rust u16 would also be mapped to int in GDScript.

Associated functions

In addition to methods (taking &self or &mut self), you can also register associated functions (without a receiver). In GDScript, the latter are known as "static functions".

For example, we can add an associated function which generates a random monster name:

#![allow(unused)]
fn main() {
#[godot_api]
impl Monster {
    #[func]
    fn random_name() -> GString {
        // ...
    }
}
}

The above can then be called from GDScript as follows:

var name: String = Monster.random_name()

Of course, it is also possible to declare parameters.

Associated functions are sometimes useful for user-defined constructors, as we will see in the next chapter.

Conclusion

This page gave you an overview of registering functions with Godot:

  • Special methods that hook into the lifecycle of your object.
  • User-defined methods and associated functions to expose a Rust API to Godot.

These are just a few use cases, you are very flexible in how you design your interface between Rust and GDScript. In the next page, we will look into a special kind of functions: constructors.

Constructors

While Rust does not have constructors as a language feature (like C++ or C#), associated functions that return a new object are commonly called "constructors". We extend the term to include slightly deviating signatures, but conceptually constructors are always used to construct new objects.

Godot has a special constructor, which we call the Godot default constructor or simply init. This is comparable to the _init method in GDScript.

Table of contents

Default constructor

The constructor of any GodotClass object is called init in gdext. This constructor is necessary to instantiate the object in Godot. It is invoked by the scene tree or when you write Monster.new() in GDScript.

There are two options to define the constructor: let gdext generate it or define it manually. It is also possible to opt out of init if you don't need Godot to default-construct your object.

Library-generated init

You can use #[class(init)] to generate a constructor for you. This is limited to simple cases, and it calls Default::default() for each field (except the Base<T> one, which is correctly wired up with the base object).

#![allow(unused)]
fn main() {
#[derive(GodotClass)]
#[class(init, base=Node3D)]
struct Monster {
    name: String,          // initialized to ""
    hitpoints: i32,        // initialized to 0
    base: Base<Node3D>,    // wired up
}
}

To provide another default value, use #[init(default = value)]. This should only be used for simple cases, as it may lead to difficult-to-read code and error messages. This API may also still change.

#![allow(unused)]
fn main() {
#[derive(GodotClass)]
#[class(init, base=Node3D)]
struct Monster {
    name: String,          // initialized to ""
   
    #[init(default = 100)]
    hitpoints: i32,        // initialized to 100
    
    base: Base<Node3D>,    // wired up
}
}

Manually defined init

We can provide a manually-defined constructor by overriding the trait's associated function init:

#![allow(unused)]
fn main() {
#[derive(GodotClass)]
#[class(base=Node3D)] // No init here, since we define it ourselves.
struct Monster {
    name: String,
    hitpoints: i32,
    base: Base<Node3D>,
}

#[godot_api]
impl INode3D for Monster {
    fn init(base: Base<Node3D>) -> Self {
        Self {
            name: "Nomster".to_string(),
            hitpoints: 100,
            base,
        }
    }
}
}

As you can see, the init function takes a Base<Node3D> as its one and only parameter. This is the base class instance, which is typically just forwarded to its corresponding field in the struct, here base.

The init method always returns Self. You may notice that this is currently the only way to construct a Monster instance. As soon as your struct contains a base field, you can no longer provide your own constructor, as you can't provide a value for that field. This is by design and ensures that if you need access to the base, that base comes from Godot directly.

However, fear not: you can still provide all sorts of constructors, they just need to go through dedicated functions that internally call init. More on this in the next section.

Disabled init

You don't always need to provide a default constructor to Godot. Reasons to not have a constructor include:

  • Your class is not a node that should be added to the tree as part of a scene file.
  • You require custom parameters to be provided for your object invariants -- a default value is not meaningful.
  • You only need to construct objects from Rust code, not from GDScript or the Godot editor.

To disable the init constructor, you can use #[class(no_init)]:

#![allow(unused)]
fn main() {
#[derive(GodotClass)]
#[class(no_init, base=Node3D)]
struct Monster {
    name: String,
    hitpoints: i32,
    base: Base<Node3D>,
}
}

Not providing/generating an init method and forgetting to use #[class(no_init)] will result in a compile-time error.

Custom constructors

The default constructor init is not always useful, as it may leave objects in an incorrect state.

For example, a Monster will always have the same values for name and hitpoints upon construction, which may not be desired. Let's provide a more suitable constructor, which accepts those attributes as parameters.

#![allow(unused)]
fn main() {
// Default constructor from before.
#[godot_api]
impl INode3D for Monster {
    fn init(base: Base<Node3D>) -> Self { ... }
}

// New custom constructor.
#[godot_api]
impl Monster {
    #[func] // Note: the following is incorrect.
    fn from_name_hp(name: GString, hitpoints: i32) -> Self { 
        ...
    }
}
}

But now, how to fill in the blanks? Self requires a base object, how to obtain it? In fact, we cannot return Self here.

Passing around objects

When interacting with Godot from Rust, all objects (class instances) need to be transported inside the Gd smart pointer -- whether they appear as parameters or return types.

The return types of init and a few other gdext-provided functions are an exception, because the library requires at this point that you have a value of the raw object. You never need to return Self in your own defined #[func] functions.

For details, consult the chapter about objects or the Gd<T> API docs.

So we need to return Gd<Self> instead of Self.

Objects with a base field

If your class T contains a Base<...> field, you cannot create a standalone instance -- you must encapsulate it in Gd<T>. You can also not extract a T from a Gd<T> smart pointer anymore; since it has potentially been shared with the Godot engine, this would not be a safe operation.

To construct Gd<Self>, we can use Gd::from_init_fn(), which takes a closure. This closure accepts a Base object and returns an instance of Self. In other words, it has the same signature as init -- this presents an alternative way of constructing Godot objects, while allowing to pass in addition context.

The result of Gd::from_init_fn() is a Gd<Self> object, which can be directly returned by Monster::from_name_hp().

#![allow(unused)]
fn main() {
#[godot_api]
impl Monster {
    #[func]
    fn from_name_hp(name: GString, hitpoints: i32) -> Gd<Self> {
        // Function contains a single statement, the `Gd::from_init_fn()` call.
        
        Gd::from_init_fn(|base| {
            // Accept a base of type Base<Node3D> and directly forward it.
            Self {
                name: name.into(), // Convert GString -> String.
                hitpoints,
                base,
            }
        })
    }
}
}

That's it! The just added associated function is now registered in GDScript and effectively works as a constructor:

var monster = Monster.from_name_hp("Nomster", 100)

Objects without a base field

For classes that don't have a base field, you can simply use Gd::from_object() instead of Gd::from_init_fn().

This is often useful for data bundles, which don't define much logic but are an object-oriented way to bundle related data in a single type. Such classes are typically subclasses of RefCounted or Resource.

#![allow(unused)]
fn main() {
#[derive(GodotClass)]
#[class(no_init)] // We only provide a custom constructor.
// Since there is no #[class(base)] key, the base class will default to RefCounted.
struct MonsterConfig {
    color: Color,
    max_hp: i32,
    tex_coords: Vector2i,
}

#[godot_api]
impl MonsterConfig {
    // Not named 'new' since MonsterConfig.new() in GDScript refers to default. 
    #[func] 
    fn create(color: Color, max_hp: i32, tex_coords: Vector2i) -> Gd<Self> {
        Gd::from_object(Self {
            color,
            max_hp,
            tex_coords,
        })
    }
}
}

Destructors

You do not typically need to declare your own destructors, if you manage memory through RAII. If you do however need custom cleanup logic, simply declare the Drop trait for your type:

#![allow(unused)]
fn main() {
impl Drop for Monster {
    fn drop(&mut self) {
        godot_print!("Monster '{}' is being destroyed!", self.name);
    }
}
}

Drop::drop() is invoked as soon as Godot orders the destruction of your Gd<T> smart pointer -- either if it is manually freed, or if the last reference to it goes out of scope.

Conclusion

Constructors allow to initialize Rust classes in various ways. You can generate, implement, or disable the default constructor init, and you can provide as many custom constructors with different signatures as you like.

Registering properties

So far, you learned how to register classes and functions. This is already powerful enough to create simple applications with godot-rust, however you might want to give Godot more direct access to the state of your object.

This is where properties come into play. In Rust, properties are typically defined as fields of a struct.

See also GDScript reference for properties.

Table of contents

Registering variables

Previously, we defined a function Monster::get_name(). This works to fetch the name, but requires you to write obj.get_name() in GDScript. Sometimes, you do not need this extra encapsulation and would like to access the field directly.

The gdext library provides an attribute #[var] to annotate fields that should be exposed as variables. This works like the var keyword in GDScript.

Starting with the earlier struct declaration, we now add the #[var] attribute to the name field. We also change the type from String to GString, since this field is now directly interfacing Godot.

#![allow(unused)]
fn main() {
#[derive(GodotClass)]
#[class(init, base=Node3D)]
struct Monster {
    #[var]
    name: GString,
    hitpoints: i32,
}
}

The effect of this is that name is now registered as a property in Godot:

var monster = Monster.new()

# Write the property.
monster.name = "Orc"

# Read the property.
print(monster.name) # prints "Orc"

In GDScript, properties are syntactic sugar for function calls to getters and setters. You can also do so explicitly:

var monster = Monster.new()

# Write the property.
monster.set_name("Orc")

# Read the property.
print(monster.get_name()) # prints "Orc"

The #[var] attribute also takes parameters to customize whether both getters and setters are provided, and what their names are. You can also write Rust methods acting as getters and setters, if you have more involved logic. See the API documentation for details.

Visibility

Like #[func] functions, #[var] fields do not need to be pub. This separates visibility towards Godot and towards Rust.

In practice, you can still access #[var] fields from Rust, but via detours (e.g. Godot's reflection APIs). But this is then a deliberate choice; private fields are primarily preventing accidental mistakes or encapsulation breaches.

Exporting variables

The #[var] attribute exposes a field to GDScript, but does not display it in the Godot editor UI.

Making a property available to the editor is called exporting. Like the GDScript annotation @export, gdext provides exports through the #[export] attribute. You might see a pattern with naming here.

The following code not only makes the name field available to GDScript, but it also adds a property UI in the editor. This allows you to name every Monster instance individually, without any code!

#![allow(unused)]
fn main() {
#[derive(GodotClass)]
#[class(init, base=Node3D)]
struct Monster {
    #[export]
    name: GString,
    hitpoints: i32,
}
}

You may have noticed that there is no longer a #[var] attribute. This is because #[export] always implies #[var] -- the name is still accessible from GDScript like before.

You can also declare both attributes on the same field. This is in fact necessary as soon as you provide arguments to customize them.

Enums

You can export Rust enums as properties. An exported enum appears as a drop-down field in the editor, with all available options. In order to do that, you need to derive three traits:

  • GodotConvert to define how the type is converted from/to Godot.
  • Var to allow using it as a #[var] property, so it can be accessed from Godot.
  • Export to allow using it as a #[export] property, so it appears in the editor UI.

Godot does not have dedicated enum types, so you can map them either as integers (e.g. i64) or strings (GString). This can be configured using the via key of the #[godot] attribute.

Exporting an enum can be done as follows:

#![allow(unused)]
fn main() {
#[derive(GodotConvert, Var, Export)]
#[godot(via = GString)]
pub enum Planet {
    Earth, // first enumerator is default.
    Mars,
    Venus,
}

#[derive(GodotClass)]
#[class(base=Node)]
pub struct SpaceFarer {
    #[export]
    favorite_planet: Planet,
}
}

The above will show up as follows in the editor UI:

Exported enum in the Godot editor UI

Refactoring the Rust enum may impact already serialized scenes, so be mindful if you want to choose integers or strings as the underlying representation:

  • Integers enable renaming variants without breaking existing scenes, however new ones must be strictly added at the end, and existing ones cannot be removed or reordered.
  • Strings allow free reordering and removing (if unused) and make debugging easier. However, you cannot rename them, and they take slightly more space (only relevant if you have tens of thousands).

Of course, it is always possible to adjust existing scene files, but this involves manual search&replace and is generally error-prone.

Enums in GDScript

Enums are not first-class citizens in Godot. Even if you define them in GDScript, they are mostly syntactic sugar for constants. This declaration:

enum Planet {
    EARTH,
    VENUS,
    MARS,
}

@export var favorite_planet: Planet

is roughly the same as:

const EARTH = 0
const VENUS = 1
const MARS = 2

@export_enum("EARTH", "VENUS", "MARS") var favorite_planet = Planet.EARTH

However, the enum is not type-safe, you can just do this:

var p: Planet = 5

Furthermore, unless you initialize the constants with string values, you cannot retrieve their names, making debugging harder. There is no reflection either, such as "get number of enum values" or "iterate over all of them". If you have the choice, consider keeping enums in Rust.

Advanced usage

Both #[var] and #[export] attributes accept parameters to further customize how properties are registered in Godot. Consult the API documentation for details.

PackedArray mutability

Packed*Array types use copy-on-write semantics, meaning every new instance can be considered an independent copy. When a Rust-side packed array is registered as a property, GDScript will create a new instance of the array when you mutate it, making changes invisible to Rust code. There is a GitHub issue with more details.

Instead, use Array<T> or register designated #[func] methods that perform the mutation on Rust side.

Custom types with #[var] and #[export]

If you want to register properties of user-defined types, so they become accessible from GDScript code (#[var]) or additionally from the editor (#[export]), then you can implement the Var and Export traits, respectively.

These traits also come with derive macros, #[derive(Var)] and #[derive(Export)].

Performance

Enabling all sorts of types for Var and Export seems convenient, but keep in mind that your conversion functions are invoked every time the engine accesses the property, which may sometimes be behind the scenes. Especially for #[export] fields, interactions with the editor UI or serialization to/from scene files can cause a quite a bit of traffic.

As a general rule, try to stay close to Godot's own types, e.g. Array, Dictionary or Gd. These are reference-counted or simple pointers.

Registering signals

Signals currently have very limited support in gdext, through the #[signal] attribute. Consult its API documentation for details.

Signal registration will be completely reworked in the future, with breaking API changes.

As an alternative, you can use Godot's dynamic API to register signals. The Object class has methods connect() and emit_signal() that can be used to connect and emit signals, respectively.

See also GDScript reference for signals.

Registering constants

Constants can be used to share fixed values from Rust code to the Godot engine.

See also GDScript reference for constants.

Constant declaration

Constants are declared as const items in Rust, inside the inherent impl block of a class.

The attribute #[constant] makes it available to Godot.

#![allow(unused)]
fn main() {
#[godot_api]
impl Monster {
    #[constant]
    const DEFAULT_HP: i32 = 100;

    #[func]
    fn from_name_hp(name: GString, hitpoints: i32) -> Gd<Self> { ... }
}
}

Usage in GDScript would look as follows:

var nom = Monster.from_name_hp("Nomster", Monster.DEFAULT_HP)
var orc = Monster.from_name_hp("Orc", 200)

(This particular example might be better suited for default parameters once they are implemented, but it illustrates the point.)

Statics

static fields can currently not be registered as constants.

Virtual functions for scripts

The GDExtension API allows you to define virtual functions in Rust, which can be overridden in scripts attached to your objects.

Compatibility

This feature is available from Godot 4.3 onwards.
(This includes dev and nightly versions after 2024-02-13).

Table of contents

A motivating example

To stay with our Monster example, let's say we have different monster types and would like to customize their behavior. We can write the logic common to all monsters in Rust, and for quick prototyping use GDScript for the specific parts.

For example, we can experiment with two monsters: Orc and Goblin. Each of them comes with a different behavior, which is encoded in a respective GDScript file. The project structure might look like this:

project_dir/
│
├── godot/
│   ├── .godot/
│   ├── project.godot
│   ├── MonsterGame.gdextension
│   └── Scenes
│       ├── Monster.tscn
│       ├── Orc.gd
│       └── Goblin.gd
│
└── rust/
    ├── Cargo.toml
    └── src/
        ├── lib.rs
        └── monster.rs

The Monster.tscn encodes a simple scene with the node Monster (our Rust class inheriting Node3D) at the root. This node would be the one to attach scripts to.

Step by step

Rust default behavior

Let's start from this class definition:

#![allow(unused)]
fn main() {
use godot::prelude::*;

#[derive(GodotClass)]
#[class(init, base=Node3D)]
struct Monster {
    base: Base<Node3D>
}
}

We can now implement a Rust function to calculate the damage a monster deals per hit. Traditionally, we would write this:

#![allow(unused)]
fn main() {
#[godot_api]
impl Monster {
    #[func]
    fn damage(&self) -> i32 {
        10
    }
}
}

That method will always return 10, no matter what. To customize this behavior in scripts that are attached to the Monster node, we can define a virtual method in Rust, which can be overridden in GDScript. The Rust code is called the default implementation.

Early vs. late binding

Virtual (also called late-binding) means that dynamic dispatch is involved: the actual method to call is determined at runtime, depending on whether a script is attached to the Monster node -- and if yes, which one.

This stands in contrast to early-binding, which is resolved at compile time, using static dispatch.

While traditional Rust might use trait objects (dyn Trait) for late binding, godot-rust provides a more direct way. Making a method virtual is very easy: just add the virtual key to the #[func] attribute.

#![allow(unused)]
fn main() {
#[godot_api]
impl Monster {
    #[func(virtual)]
    fn damage(&self) -> i32 {
        10
    }
}
}

That's it. Your monster can now be customized in scripts.

Overriding in GDScript

In the GDScript files, you can now override the Rust damage method as _damage. The method is prefixed with an underscore, following Godot convention for virtual methods such as _ready or _process.

Here's an example for the Orc and Goblin scripts:

# Orc.gd
extends Monster

func _damage():
    return 20
# Goblin.gd
extends Monster

# Random damage between 5 and 15.
# Type annotations are possible, but not required.
func _damage() -> int:
    return randi() % 11 + 5

If your signature in GDScript does not match the Rust signature, Godot will cause an error.

Dynamic behavior

Now, let's call damage() in Rust code:

#![allow(unused)]
fn main() {
fn monster_attacks_player(monster: Gd<Monster>, player: Gd<Player>) {
    // Compute the damage.
    let damage_points: i32 = monster.bind().damage();

    // Apply the damage to the player.
    player.bind_mut().take_damage(damage_points);
}
}

What value does damage_points have in the above example?
The answer depends on the circumstances:

  • If the Monster node has no script attached, damage_points will be 10 (the default implementation in Rust).
  • If the Monster node has the Orc.gd script attached, damage_points will be 20.
  • If the Monster node has the Goblin.gd script attached, damage_points will be a random number between 5 and 15.

Trade-offs

You might ask: what's the point of all this, if one can achieve the same with a simple match statement?

And you're right; if a match in Rust is all you need, then use that. However, the script-based approach has a few advantages, especially when it comes to more complex scenarios than just computing a single damage number:

  • You can prepare a variety of scripts with different behaviors, e.g. for different levels or enemy AI behavior. In the Godot editor, you can then simply swap out scripts as needed, or have different Monster instances with different scripts, to compare them side-by-side.
  • Switching behaviors does not require recompiling Rust code. This can be useful if you work with game designers, modders or artists who are less familiar with Rust, but want to experiment nonetheless.

That said, if your compile times are short (gdext itself is quite lightweight) and you prefer having the logic in Rust, that is of course also a valid choice. To retain the option to quickly switch behaviors, you could use an #[export]'ed enum to select the behavior, and then dispatch on that in Rust.

Ultimately, #[func(virtual)] is just one extra tool that godot-rust offers among a variety of abstraction mechanisms. Since Godot's paradigm revolves heavily around attaching scripts to nodes, this feature integrates very well with the engine.

Limitations

Warning

Godot script virtual functions do not behave like OOP virtual functions in every aspect.
Make sure you understand the limitations.

In contrast to virtual methods from OOP languages (C++, C#, Java, Kotlin, PHP, ...), there are some important differences to be aware of.

  1. The default implementation is unreachable from Godot.

    In Rust, calling monster.bind().damage() will automatically look for script overrides, and fall back to the Rust default if no script is attached. In GDScript however, you cannot call the default implementation. Calling monster._damage() will fail without a script. The same is true for reflection calls from Rust (e.g. Object::call()).

    The _ prefix underlines that: ideally, you don't call virtual functions directly from scripts.

    To work around this, you can declare a separate #[func] fn default_damage() in Rust, which will be registered as a regular method and thus can be called from scripts. To keep Rust's convenient fallback behavior, just invoke default_damage() inside the Rust damage() method.

  2. No access to super methods.

    In OOP languages, you can call the base method from the overriding method, typically using super or base keywords.

    As a consequence of point 1), this default method is also not visible to the script overriding it. The same workaround can be used though.

  3. Limited re-entrancy.

    If you call a virtual method from Rust, it may dispatch to a script implementation. The Rust side holds either a shared (&self) or exclusive borrow (&mut self) to the object -- an implicit Gd::bind() or Gd::bind_mut() guard. If the script implementation then accesses the same object (e.g. by setting a #[var] property), panics can occur due to double-borrow errors.

    For now, you can work around this by declaring the method with #[func(gd_self, virtual)]. The gd_self requires the first parameter to be of type Gd<Self>, which avoids the bind call and thus the borrow.

We are observing how virtual functions are used by the community and plan to mitigate the limitations where possible. If you have any inputs, feel free to let us know!

Types of scripts

While this page focuses on GDScript, Godot also provides other scripting capabilities. Notably, C# can be used for scripting, if you run Godot with the Mono runtime.

The library also provides a dedicated trait ScriptInstance, which allows users to provide Rust-based "scripts". Consult its docs for detailed information.

You can also configure scripts entirely programmatically, using the engine::Script API and its inherited classes, such as engine::GDScript. This typically defeats the purpose of scripting, but is mentioned here for completeness.

Conclusion

In this chapter, we have seen how to define virtual functions in Rust, and how to override them in GDScript. This provides an additional integration layer between the two languages and allows to effortlessly experiment with swappable behaviors from the editor.

Toolchain

Beyond Rust, there are quite a few things that are handy to know when working with Godot. This chapter goes into more detail about them, covering topics such as versioning, compatibility or debugging.

Check out the subchapters for more information.

Compatibility and stability

The gdext library supports all stable Godot releases starting from Godot 4.0.

Compatibility with Godot

When developing extension libraries (or just "extensions"), you need to consider which engine version you want to target. There are two conceptually different versions:

  • API version is the version of GDExtension against which gdext (and the code of your extension) is compiled.
  • Runtime version is the version of Godot in which the library built with gdext is run.

The two versions can be different, but there are certain constraints (see below).

Philosophy

We take compatibility with the engine seriously, in an attempt to build an ecosystem of extensions that are interoperable with multiple Godot versions. Nothing is more annoying than updating the engine and recompiling 10 plugins/extensions.

This is sometimes difficult, because:

  • Godot may introduce subtle breaking changes of which we are not aware.
  • Some changes that are non-breaking in C++ and GDScript are breaking in Rust (e.g. providing a default value for a previously required parameter).
  • Using newer features needs to come with a fallback/polyfill for older Godot versions.

We run CI jobs against multiple Godot versions, to get a certain level of confidence that updates do not break compatibility. Nevertheless, the number of possible combinations is large and only growing, so we may miss certain issues. If you find incompatibilities or violations of the rules stated below, please let us know.

Current guarantees

Every extension developed with API version 4.0.x MUST be run with the same runtime version.

  • In particular, it is not possible to run an extension compiled with API version 4.0.x in Godot 4.1 or later. This is due to breaking changes in Godot's GDExtension API.

Starting from Godot 4.1 official release, extensions can be loaded by any Godot version, as long as runtime version >= API version.

  • You can run a 4.1 extension in Godot 4.1.1 or 4.2.
  • You cannot run a 4.2 extension in Godot 4.1.1.
  • This is subject to change depending on how the GDExtension API evolves and how many breaking changes we have to deal with.

Out of scope

We do not invest effort in maintaining compatibility with:

  1. Godot in-development versions, except for the latest master branch.
    • Note that we may take some time to catch up with the latest changes, so please don't report issues within a few days after upstream changes have landed.
  2. Non-stable releases (alpha, beta, RC).
  3. Third-party bindings or GDExtension APIs (C#, C++, Python, ...).
    • These may have their own versioning guarantees and release cycles; and there may be specific bugs to such an integration. If you find an issue with gdext and another binding, reproduce it in GDScript to make sure it's relevant for us.
    • We do however maintain compatibility with Godot, so if integrations go through the engine (e.g. Rust calls a method whose implementation is in C#), this should work.
  4. Godot with non-standard build flags (e.g. disabled modules).
  5. Godot forks or engines running third-party modules.

Rust API stability

We are still in a phase where a lot of gdext's foundation needs to be built and refined. As such, expect breaking changes. At the current stage, we believe that making APIs more ergonomic and accessible has priority over long-term stability. The alternative would be to lock us early into a design corner.

Note that many such breaking changes are externally motivated, for example:

  • GDExtension changes in a way that cannot be abstracted from the user.
  • There are subtleties in the type system or runtime guarantees that can be modeled in a better, safer way (e.g. typed arrays, RIDs).
  • We get feedback from game developers and other users stating that certain workflows are very cumbersome.

Once we get into a more stable feature set, we plan to release versions on crates.io and follow semantic versioning.

Selecting a Godot version

By default, gdext uses the latest stable release of Godot. This is desired in most cases, but it means that you cannot run your extension in an older Godot version. Furthermore, you cannot benefit from modified Godot versions (e.g. with custom modules).

If these are features you need, this page will walk you through the necessary steps. Read Compatibility and stability first and make sure you understand the concept of API and runtime versions.

Older stable releases

Building gdext against an older Godot API allows you to remain forward-compatible with all engine versions >= that version. (For Godot 4.0.x, == applies instead of >=.)

In a hypothetical example, building against API 4.1 allows you to run your extension in Godot 4.1.1, 4.1.2 or 4.2.

To choose a version (here 4.0), add the following to your top-level (workspace) Cargo.toml:

[patch."https://github.com/godot-rust/godot4-prebuilt".godot4-prebuilt]
git = "https://github.com//godot-rust/godot4-prebuilt"
branch = "4.0"

(If you're interested in the // workaround, see https://github.com/rust-lang/cargo/issues/5478).

Custom Godot versions

If you want to freely choose a Godot binary on your local machine from which the GDExtension API is generated, you can use the Cargo feature custom-godot. If enabled, this will look for a Godot binary in two locations, in this order:

  1. The environment variable GODOT4_BIN.
  2. The binary godot4 in your PATH.

Generated code inside the godot::engine and godot::builtin modules may now look different from stable releases. Note that we do not give any support or compatibility guarantees for custom-built GDExtension APIs.

Note that this requires the bindgen, as such you may need to install the LLVM toolchain. Consult the setup page for more information.

Setting GODOT4_BIN to a relative path

If you have multiple Godot workspaces on a machine, you may want a workspace-independent method of setting the GODOT4_BIN environment variable. This way, the matching Godot editor binary for that workspace is always used in the build process, without having to set GODOT4_BIN differently for each location.

You can do this by configuring Cargo to set GODOT4_BIN to a relative path for you, in .cargo/config.toml.

In the root of your Rust project, create .cargo/config.toml with the example content shown below, modifying the editor path as needed to find your binary. The path you set will be resolved relatively to the location of the .cargo directory.

[env]
GODOT4_BIN = { value = "../godot/bin/godot.linuxbsd.editor.x86_64", relative = true, force = true }

(If you want to override config.toml by setting GODOT4_BIN in your environment, remove force = true.)

Test your change by running cargo build.

See The Cargo Book for more information on customizing your build environment with config.toml.

Debugging

Extensions written in gdext can be debugged using LLDB, in a similar manner to other Rust programs. The primary difference is that LLDB will launch or attach to the Godot C++ executable: either the Godot editor or your custom Godot application. Godot then loads your extension (itself a dynamic library), and with it your Rust code.

The process for launching or attaching LLDB varies based on your IDE and platform. Unless you are using a debug version of Godot itself, you will only have symbols for stack frames in Rust code.

Launching with VS Code

Here is an example launch configuration for Visual Studio Code. Launch configurations should be added to ./.vscode/launch.json, relative to your project's root. This example assumes you have the CodeLLDB extension installed, which is common for Rust development.

{
    "configurations": [
        {
            "name": "Debug Project (Godot 4)",
            "type": "lldb", // type provided by CodeLLDB extension
            "request": "launch",
            "preLaunchTask": "rust: cargo build",
            "cwd": "${workspaceFolder}",
            "args": [
                "-e", // run editor (remove this to launch the scene directly)
                "-w", // windowed mode
            ],
            "linux": {
                "program": "/usr/local/bin/godot4",
            },
            "windows": {
                "program": "C:\\Program Files\\Godot\\Godot_v4.1.X.exe",
            },
            "osx": {
                // NOTE: on macOS the Godot.app needs to be manually re-signed 
                // to enable debugging (see below)
                "program": "/Applications/Godot.app/Contents/MacOS/Godot",
            }
        }
    ]
}

Debugging on macOS

Attaching a debugger to an executable that wasn't compiled locally (the Godot editor, in this example) requires special considerations on macOS due to its System Integrity Protection (SIP) security feature. Even though your extension is compiled locally, LLDB will be unable to attach to the Godot host process without manual re-signing.

In order to re-sign, simply create a file called editor.entitlements with the following contents. Be sure to use the editor.entitlements file below rather than the one from the Godot Docs, as it includes the required com.apple.security.get-task-allow key not currently present in Godot's instructions.

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist 
  PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
    <dict>
        <key>com.apple.security.cs.allow-dyld-environment-variables</key>
        <true/>
        <key>com.apple.security.cs.allow-jit</key>
        <true/>
        <key>com.apple.security.cs.allow-unsigned-executable-memory</key>
        <true/>
        <key>com.apple.security.cs.disable-executable-page-protection</key>
        <true/>
        <key>com.apple.security.cs.disable-library-validation</key>
        <true/>
        <key>com.apple.security.device.audio-input</key>
        <true/>
        <key>com.apple.security.device.camera</key>
        <true/>
        <key>com.apple.security.get-task-allow</key>
        <true/>
    </dict>
</plist>

Once this file is created, you can run

codesign -s - --deep --force --options=runtime \
    --entitlements ./editor.entitlements /Applications/Godot.app

in Terminal to complete the re-signing process. It is recommended to check this file into version control, since each developer needs to re-sign their local installation if you have a team. This process should only be necessary once per Godot installation though.

Export to Web

Web builds are a fair bit more difficult to get started with compared to native builds. This will be a complete guide on how to get things compiled. However, setting up a web server to host and share your game is considered out of scope of this guide, and is best explained elsewhere.

Warning

Web support with gdext is experimental and should be understood as such before proceeding.

Installation

Install a nightly build of rustc, the wasm32-unknown-emscripten target for rustc, and rust-src. The reason why nightly rustc is required is the unstable flag to build std (-Zbuild-std). Assuming that Rust was installed with rustup, this is quite simple.

rustup toolchain install nightly
rustup component add rust-src --toolchain nightly
rustup target add wasm32-unknown-emscripten --toolchain nightly

Next, install Emscripten. The simplest way to achieve this is to install emsdk from the git repo. We recommended version 3.1.39 for now.1

git clone https://github.com/emscripten-core/emsdk.git
cd emsdk
./emsdk install 3.1.39
./emsdk activate 3.1.39
source ./emsdk.sh     (or ./emsdk.bat on windows)

It would also be highly recommended to follow the instructions in the terminal to add emcc2 to your PATH. If not, it is necessary to manually source the emsdk.sh file in every new terminal prior to compilation. This is platform-specific.

Project Configuration

Enable the experimental-wasm feature on gdext in the Cargo.toml file. It is also recommended to enable the lazy-function-tables feature to avoid long compile times with release builds (this might be a bug and not necessary in the future). Edit the line to something like the following:

[dependencies.godot]
git = "https://github.com/godot-rust/gdext"
branch = "master"
features = ["experimental-wasm", "lazy-function-tables"]

If you do not already have a .cargo/config.toml file, do the following:

  • Create a .cargo directory at the same level as your Cargo.toml.
  • Inside that directory, create a config.toml file.

This file needs to contain the following:

[target.wasm32-unknown-emscripten]
rustflags = [
    "-C", "link-args=-sSIDE_MODULE=2",
    "-C", "link-args=-pthread", # was -sUSE_PTHREADS=1 in earlier emscripten versions
    "-C", "target-feature=+atomics,+bulk-memory,+mutable-globals",
    "-Zlink-native-libraries=no"
]

Edit the project's .gdextension file to include support for web exports. This file will probably be at godot/{YourCrate}.gdextension. The format will be similar to the following:

[libraries]
...
web.debug.wasm32 = "res://../rust/target/wasm32-unknown-emscripten/debug/{YourCrate}.wasm"
web.release.wasm32 = "res://../rust/target/wasm32-unknown-emscripten/release/{YourCrate}.wasm"

Compile the Project

Verify emcc is in the PATH. This can be as simple as doing the following:

emcc --version

Compile the code. It is necessary to both use the nightly compiler and specify to build std3, along with specifying the Emscripten target.

cargo +nightly build -Zbuild-std --target wasm32-unknown-emscripten

Godot editor setup

Add a web export in the Godot Editor. In the top menu bar, go to Project > Export... and configure it there. Make sure to turn on the Extensions Support checkbox.

Example of export screen

If instead, the bottom on the export popup contains this error in red:

No export template found at expected path:

Then click on Manage Export Templates next to the error message, and then on the next screen select Download and Install. See Godot tutorial for further information.

Running the webserver

Back at the main editor screen, there is an option to run the web debug build (not a release build) locally without needing to run an export or set up a web server. At the top right, choose Remote Debug > Run in Browser and it will automatically open up a web browser.

Location of built-in web server

Known Caveats

  • Godot 4.1.3+ or 4.2+ is necessary.
  • Only Chromium-based browsers (Chrome or Edge) appear to be supported by GDExtension at the moment; Firefox and Safari don't work yet. Info about browser support can be found here.

If your default browser is not Chromium-based, you will need to copy the URL (which is usually http://localhost:8060/tmp_js_export.html) and open it in a supported browser such as Google Chrome or Microsoft Edge.

Debugging

Currently, the only option for WASM debugging is this extension for Chrome. It adds support for breakpoints and a memory viewer into the F12 menu.



1

Note: Due to a bug with emscripten, the maximum version of emcc2 that can one compile Godot with is 3.1.39. gdext itself should be able to support the latest version of emcc, however, it may be a safer bet to stick to version 3.1.39.

2

emcc is the name of Emscripten's compiler.

3

The primary reason for this is it is necessary to compile with -sSHARED_MEMORY enabled. The shipped std does not, so building std is a requirement. Related info on about WASM support can be found here.

Recipes

Custom resources

With godot-rust, you are able to define custom Resource classes which are then available to the end user.

Editor plugins

EditorPlugin types are loaded during editor and runtime and are able to access the editor as well as the scene tree. This type follows the same functionality that a typical EditorPlugin class written in GDScript would, but crucially with access to the entire Rust ecosystem.

Engine singletons

An Engine Singleton is a class instance that is always globally available (following the Singleton pattern). However, it cannot access the SceneTree through any reliable means.

Custom icons

Adding custom icons to your classes is actually fairly simple!

Custom resources

Custom Resources are exposed to the end user to use within their development. Resources can store data that is easily edited from within the editor GUI. For example, you can create a custom AudioStream type that handles a new and interesting audio file type.

Registering a Resource

This workflow is similar to the Hello World example:

#![allow(unused)]
fn main() {
#[derive(GodotClass)]
#[class(tool, init, base=Resource)]
struct ResourceType {
    base: Base<Resource>,
}
}

It is important that similar to defining custom resources in GDScript, marking this class as a "tool class" is required to be usable within the editor.

The above resource does not export any variables. While not all resources require exported variables, most do.

The systems for registering functions, properties, and more are described in detail in the Registering Rust symbols section.

Editor plugins

Using EditorPlugin types is very similar to the process used when writing plugins in GDScript. Unlike GDScript plugins, godot-rust plugins are registered automatically and cannot be enabled/disabled in the Project Settings plugins pane.

Plugins written in GDScript are automatically disabled if they have a code error, but because Rust is a compiled language, you cannot introduce compile-time errors.

Creating an EditorPlugin

#![allow(unused)]
fn main() {
#[derive(GodotClass)]
#[class(tool, init, editor_plugin, base=EditorPlugin)]
struct MyEditorPlugin {
    base: Base<EditorPlugin>,
}

#[godot_api]
impl IEditorPlugin for MyEditorPlugin {
    fn enter_tree(&mut self) {
        // Perform typical plugin operations here.
    }

    fn exit_tree(&mut self) {
        // Perform typical plugin operations here.
    }
}
}

Since this is an EditorPlugin, it will be automatically added to the scene tree root. This means it can access the scene tree at runtime. Additionally, it is safe to access the EditorInterface singleton through this node, which allows adding different GUI elements to the editor directly. This can be helpful if you have an advanced GUI you want to implement.

Gameplay-only code

Use an is_editor_hint guard if you don't want some code executing during runtime of the game.

Read more information on guard clauses in computer science.

Engine singletons

It is important for you to understand the Singleton pattern to properly utilize this system.

Controversy

The "Singleton pattern" is often referred to as an anti-pattern, because it violates several good practices for clean, modular code. However, it is also a tool that can be used to solve certain design problems. As such, it is used internally by Godot, and is available to godot-rust users as well.

Read more about criticisms here.

An engine singleton is registered through godot::engine::Engine.

Custom engine singletons in Godot:

  • are Object types
  • are always accessible to GDScript and GDExtension languages
  • must be manually registered and unregistered in the InitLevel::Scene step

Godot provides many built-in singletons in its API. You can find a full list here.

Table of contents

Defining a singleton

Defining a singleton is the same as registering a custom class.

#![allow(unused)]
fn main() {
#[derive(GodotClass)]
#[class(init, base=Object)]
struct MyEditorSingleton {
    base: Base<Object>,
}

#[godot_api]
impl MyEditorSingleton {
    #[func]
    fn foo(&mut self) {}
}
}

Registering a singleton

Registering singletons is done during the InitLevel::Scene stage of initialization.

To achieve this, we can customize our init/shutdown routines by overriding ExtensionLibrary trait methods.

#![allow(unused)]
fn main() {
struct MyExtension;

#[gdextension]
unsafe impl ExtensionLibrary for MyExtension {
    fn on_level_init(level: InitLevel) {
        if level == InitLevel::Scene {
            // The StringName identifies your singleton and can be
            // used later to access it.
            Engine::singleton().register_singleton(
                StringName::from("MyEditorSingleton"),
                MyEditorSingleton::new_alloc().upcast(),
            );
        }
    }

    fn on_level_deinit(level: InitLevel) {
        if level == InitLevel::Scene {
            // Get the `Engine` instance and `StringName` for your singleton.
            let mut engine = Engine::singleton();
            let singleton_name = StringName::from("MyEditorSingleton");

            // We need to retrieve the pointer to the singleton object,
            // as it has to be freed manually - unregistering singleton 
            // doesn't do it automatically.
            let singleton = engine
                .get_singleton(singleton_name.clone())
                .expect("cannot retrieve the singleton");

            // Unregistering singleton and freeing the object itself is needed 
            // to avoid memory leaks and warnings, especially for hot reloading.
            engine.unregister_singleton(singleton_name);
            singleton.free();
        }
    }
}
}

Singletons inheriting from RefCounted

Use a manually-managed class as a base (often Object will be enough) for custom singletons to avoid prematurely freeing the object. If for any reason you need to have an instance of a reference-counted object registered as a singleton, this issue thread presents some possible workarounds.

Calling from GDScript

Now that your singleton is available (and once you've recompiled and reloaded), you should be able to access it from GDScript like so:

extends Node

func _ready() -> void:
    MyEditorSingleton.foo()

Calling from Rust

You may also want to access your singleton from Rust as well.

#![allow(unused)]
fn main() {
godot::engine::Engine::singleton()
    .get_singleton(StringName::from("MyEditorSingleton"));
}

For more information on this method, refer to the API docs.

Singletons and the SceneTree

Singletons cannot safely access the scene tree. At any given moment, they may exist without a scene tree being active. While it is technically possible to access the tree through hacky methods, it is highly recommended to use a custom EditorPlugin for this purpose. Creating an EditorPlugin allows for registering an "autoload singleton" which is a Node (or derived) type and is automatically loaded into the SceneTree by Godot when the game starts.

Custom node icons

By default, all your custom types will use the Node icon in the editor UI -- e.g. in the scene tree or when selecting a node to create. While this can be serviceable, you may want to add custom icons to distinguish node types, especially if you plan to distribute your extension to others.

All icons must be registered by their class name in your .gdextension file. For this, you can add a new icon section. Classes are keys and paths to SVG files are values.

[icons]

MyClass = "res://addons/your_extension/filename.svg"

Icon paths

The path is based off the res:// scheme, like other Godot resources. It is recommended to use Godot's convention of an addons folder, followed by the name of the addon.

Read more about the reasoning behind this in the Godot docs:

Formatting for custom icons

The Godot docs have a page dedicated to tools and resources for creating custom icons. The long and short of it is:

  • Use the SVG format.
  • Aspect ratio is a square, 16x16 units is the reference size.
  • Refer to the Godot icon colors mappings.
    • Use the light mode colors -- Godot only supports light-to-dark, but not dark-to-light color conversions.

Third-party article

The user QueenOfSquiggles wrote an alternative version of this article on her personal blog, which includes color previews for the light and dark themed colors.

Details on how to use her reference page is included here.

Contributing to gdext

This chapter provides deeper information for people who are interested in contributing to the library. In case you are simply using gdext, you can skip this chapter.

If you haven't already, please read the Contributing guidelines in the repository first. The rest of this chapter explains developer tools and workflows in more detail. Check out the respective subchapters.

Philosophy

Different gamedev projects have different goals, which determines how APIs are built and how they support various use cases.

Understanding the vision behind gdext allows users to:

  • decide whether the library is the right choice for them
  • comprehend design decisions that have influenced the library's status quo
  • contribute in ways that align with the project, thus saving time.

Mission statement

If the idea behind the godot-rust project had to be summarized in a single word, it would be:

Pragmatism

godot-rust offers an ergonomic, safe and efficient way to access Godot functionality from Rust.

It focuses on a productive workflow for the development of games and interactive applications.

In our case, pragmatism means that progress is driven by solutions to real-world problems, rather than theoretical purity. Engineering comes with trade-offs, and gdext in particular is rather atypical for a Rust project. As such, we may sometimes deviate from Rust best practices that may apply in a clean-room setting, but fall apart when exposed to the interaction with a C++ game engine.

At the end of the day, people use Godot and Rust to build games, simulations or other interactive applications. The library should be designed around this fact, and Rust should be a tool that helps us achieve this goal -- not an end in itself.

In many ways, we follow similar principles as the Godot engine.

Scope

gdext is primarily a binding to the Godot engine. A priority is to make Godot functionality accessible for Rust developers, in ways that exploit the strengths of the language, while minimizing the friction.

Since we are not building our own game engine, features need to be related to Godot. We aim to build a robust core for everyday workflows, while avoiding overly niche features. Integrations with other parts of the gamedev ecosystem (e.g. ECS, asset pipelines, GUI) are out of scope and best implemented as extensions.

API design principles

We envision the following core principles as a guideline for API design:

  1. Solution-oriented approach
    Every feature must solve a concrete problem that users or developers face.

    • We do not build solutions in search of problems. "Idiomatic Rust", "others also do it" or "it would be nice" are not good justifications :)
    • Priority is higher if more people are affected by a problem, or if the problem impacts a daily workflow more severely. In particular, this means that we can't spend much time on rarely used niche APIs, while there are game-breaking bugs in the core functionality.
    • We should always keep the big picture in mind. Rust makes it easy to get lost in irrelevant details. What matters is how a certain change helps end users.
  2. Simplicity
    Prefer self-explanatory, straightforward APIs.

    • Avoid abstractions that don't add value to the user. Do not over-engineer prematurely just because it's possible; follow YAGNI and avoid premature optimization.
    • Examples to avoid: traits that are not used polymorphically, type-state pattern, many generic parameters, layers of wrapper types/functions that simply delegate logic.
    • Sometimes, runtime errors are better than compile-time errors. Most users are building a game, where fast iteration is key. Use Option/Result when errors are recoverable, and panics when the user must fix their code. See also Ergonomics and panics.
  3. Maintainability
    Every line of code added must be maintained, potentially indefinitely.

    • Consider that it may not be you working with it in the future, but another contributor or maintainer, maybe a year from now.
    • Try to see the bigger picture -- how important is a specific feature in the overall library? How much detail is necessary? Balance the amount of code with its real-world impact for users.
    • Document non-trivial thought processes and design choices as inline // comments.
    • Document behavior, invariants and limitations in /// doc comments.
  4. Consistency
    As a user, having a uniform experience when using different parts of the library is important. This reduces the cognitive load of learning and using the library, requires less doc lookup and makes users more efficient.

    • Look at existing code and try to understand its patterns and conventions.
    • Before doing larger refactorings or changes of existing systems, get an understanding of the underlying design choices and discuss your plans.

See these as guidelines, not hard rules. If you are unsure, please don't hesitate to ask questions and discuss different ideas :)

Tip

We highly appreciate if contributors propose a rough design before spending large effort on implementation. This aligns ideas early and saves time on approaches that may not work.

Dev tools and testing

The library comes with a handful of tools and tricks to ease development. This page goes into different aspects of the contributing experience.

Local development

The script check.sh in the project root can be used to mimic a minimal version of CI locally. It's useful to run this before you commit, push or create a pull request:

./check.sh

At the time of writing, this will run formatting, clippy, unit tests and integration tests. More checks may be added in the future. Run ./check.sh --help to see all available options.

If you like, you can set this as a pre-commit hook in your local clone of the repository:

ln -sf check.sh .git/hooks/pre-commit

API Docs

Besides published docs, API documentation can also be generated locally using ./check.sh doc. Use dok instead of doc to open the page in the browser.

Unit tests

Because most of gdext interacts with the Godot engine, which is not available from the test executable, unit tests (using cargo test and the #[test] attribute) are pretty limited in scope. They are primarily used for Rust-only logic.

Unit tests also include doctests, which are Rust code snippets embedded in the documentation.

As additional flags might be needed, the preferred way to run unit tests is through the check.sh script:

./check.sh test

Integration tests

The itest directory contains a suite of integration tests. It is split into two directories: rust, containing the Rust code for the GDExtension library, and godot with the Godot project and GDScript tests.

Similar to #[test], the function annotated by #[itest] contains one integration test. There are multiple syntax variations:

#![allow(unused)]
fn main() {
// Use a Godot API and verify the results using assertions.
#[itest]
fn variant_nil() {
    let variant = Variant::nil();
    assert!(variant.is_nil());
}

// TestContext parameter gives access to a node in the scene tree.
#[itest]
fn do_sth_with_the_tree(ctx: &TestContext) {
    let tree: Gd<Node> = ctx.scene_tree.share();
    
    // If you don't need the scene, you can also construct free-standing nodes:
    let node: Gd<Node3D> = Node3D::new_alloc();
    // ...
    node.free(); // don't forget to free everything created by new_alloc().    
}

// Skip a test that's not yet ready.
#[itest(skip)]
fn not_executed() {
    // ...
}

// Focus on a one or a few tests.
// As soon as there is at least one #[itest(focus)], only focused tests are run.
#[itest(focus)]
fn i_need_to_debug_this() {
    // ...
}
}

You can run the integration tests like this:

./check.sh itest

Just like when compiling the crate, the GODOT4_BIN environment variable can be used to supply the path and filename of your Godot executable. Otherwise, a binary named godot4 in your PATH is used.

Formatting

rustfmt is used to format code. check.sh only warns about formatting issues, but does not fix them. To do that, run:

cargo fmt

Clippy

clippy is used for additional lint warnings not implemented in rustc. This, too, is best run through check.sh:

./check.sh clippy

Continuous Integration

If you want to have the full CI experience, you can experiment as much as you like on your own gdext fork, before submitting a pull request.

For this, navigate to the file .github/workflows/full-ci.yml and change the following lines:

on:
  push:
    branches:
      - staging
      - trying

to:

on:
  push:

This runs the entire CI pipeline to run on every push. You can then see the results in the Actions tab in your repository.

Don't forget to undo this before opening a PR! You may want to keep it in a separate commit named "UNDO" or similar.

Build configurations

real type

Certain types in Godot use either a single or double-precision float internally, such as Vector2. When working with these types, we use the real type instead of choosing either f32 or f64. As a result, our code is portable between Godot binaries compiled with precision=single and precision=double.

To run the testing suite with double-precision enabled you may add --double to a check.sh invocation:

./check.sh --double

Code and API conventions

Bikeshed auto-painting

In general, we try to automate as much as possible during CI. This ensures a consistent code style and avoids unnecessary work during pull request reviews.

In particular, we use the following tools:

In addition, we have unit tests (#[test]), doctests and Godot integration tests (#[itest]). See Dev tools for more information.

Technicalities

This section lists specific style conventions that have caused some confusion in the past. Following them is nice for consistency, but it's not the top priority of this project. Hopefully, we can automate some of them over time.

Formatting

rustfmt is the authority on formatting decisions. If there are good reasons to deviate from it, e.g. data-driven tables in tests, use #[rustfmt::skip]. rustfmt does not work very well with macro invocations, but such code should still follow rustfmt's formatting choices where possible.

Line width is 120-145 characters (mostly relevant for comments).
We use separators starting with // --- to visually divide sections of related code.

Code organization

  1. Anything that is not intended to be accessible by the user, but must be pub for technical reasons, should be marked as #[doc(hidden)].

  2. We do not use the prelude inside the project, except in examples and doctests.

  3. Inside impl blocks, we roughly try to follow the order:

    • Type aliases in traits (type)
    • Constants (const)
    • Constructors and associated functions
    • Public methods
    • Private methods (pub(crate), private, #[doc(hidden)])
  4. Inside files, there is no strict order yet, except use and mod at the top. Prefer to declare public-facing symbols before private ones.

  5. Use flat import statements. If multiple paths have different prefixes, put them on separate lines. Avoid self.

    #![allow(unused)]
    fn main() {
    // Good:
    use crate::module;
    use crate::module::{Type, function};
    use crate::module::nested::{Trait, some_macro};
    
    // Bad:
    use crate::module::{self, Type, function, nested::{Trait, some_macro}};
    }

Types

  1. Avoid tuple-enums enum E { Var(u32, u32) } and tuple-structs struct S(u32, u32) with more than 1 field. Use named fields instead.

  2. Derive order is #[derive(GdextTrait, ExternTrait, Default, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)].

    • GdextTrait is a custom derive defined by gdext itself (in any of the crates).
    • ExternTrait is a custom derive by a third-party crate, e.g. nanoserde.
    • The standard traits follow order construction, comparison, hashing, debug display. More expressive ones (Copy, Eq) precede their implied counterparts (Clone, PartialEq).

Functions

  1. Getters don't have a get_ prefix.

  2. Use self instead of &self for Copy types, unless they are really big (such as Transform3D).

  3. For Copy types, avoid in-place mutation vector.normalize().
    Instead, use vector = vector.normalized(). The past tense indicates a copy.

  4. Annotate with #[must_use] when ignoring the return value is likely an error.
    Example: builder APIs.

Attributes

Concerns both #[proc_macro_attribute] and the attributes attached to a #[proc_macro_derive].

  1. Attributes always have the same syntax: #[attr(key = "value", key2, key_three = 20)]

    • attr is the outer name grouping different key-value pairs in parentheses.
      A symbol can have multiple attributes, but they cannot share the same name.
    • key = value is a key-value pair. just key is a key-value pair without a value.
      • Keys are always snake_case identifiers.
      • Values are typically strings or numbers, but can be more complex expressions.
      • Multiple key-value pairs are separated by commas. Trailing commas are allowed.
  2. In particular, avoid these forms:

    • #[attr = "value"] (top-level assignment)
    • #[attr("value")] (no key -- note that #[attr(key)] is allowed)
    • #[attr(key(value))]
    • #[attr(key = value, key = value)] (repeated keys)

The reason for this choice is that each attribute maps nicely to a map, where values can have different types. This allows for a recognizable and consistent syntax across all proc-macro APIs. Implementation-wise, this pattern is directly supported by the KvParser type in gdext, which makes it easy to parse and interpret attributes.