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:
-
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.
-
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.
-
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 theExtensionLibrary
trait. -
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.
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:
- The official API documentation.
- A small example game Dodge the Creeps.
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
asgodot4
, - 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
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
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 api-custom
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
- Create a Godot project
- Create a Rust crate
- Wire up Godot with Rust
- Your first Rust extension
- Creating a Rust class
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.
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.
- Consult GDExtension docs for more possible values.
- 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 yourHelloWorld.gdextension
file is. You can learn more about Godot's resource paths here. - If you remember the file structure, the
godot
andrust
directories are siblings, so we need to go up one level to reachrust
.
- The
- 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.
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!
When exporting your project, you need to use paths inside res://
.
Outside paths like ..
are not supported.
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
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:
- Place the
prelude
module from thegodot
crate into scope. This module contains the most common symbols in the gdext API. - Define a struct called
MyExtension
. This is just a type tag without data or methods, you can name it however you like. - 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.
Troubleshooting
It's common that there are some issues with first-time setup. Particularly, errors related to the library not being found or the gdext_rust_init
entry point symbol being missing or impossible to resolve come up, usually due to an incorrect initial setup. Here are a few troubleshooting steps
that should solve the most common problems.
- Have you run
cargo build
? - In
Cargo.toml
, have you setcrate-type = ["cdylib"]
? - In
my-extension.gdextension
, have you setentry_symbol = "gdext_rust_init"
? No other symbol can work. - Are the paths set in
my-extension.gdextension
correct?- Are you sure? Double check
/rust/target/debug/
to see if the name of the.so
/.dll
/.dylib
is spelled the way you expect. - The paths must also be relative to the directory that
project.godot
is in. Typically it'll beres://../rust/...
.
- Are you sure? Double check
- Have you written the Rust code necessary to generate the entry point symbol?
- See above for how.
- Is your directory structure like this below? It's much easier when you ask for help if it is.
my-cool-project
├── godot
│ ├── project.godot
│ └── my-extension.gdextension
└── rust
├── Cargo.toml
├── src
└── target
└── debug
└── (lib)?my_extension.(so|dll|dylib)
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::classes::Sprite2D; #[derive(GodotClass)] #[class(base=Sprite2D)] struct Player { speed: f64, angular_speed: f64, base: Base<Sprite2D> } }
Let's break this down.
-
The gdext prelude contains the most common symbols. Less frequent classes are located in the
engine
module. -
The
#[derive]
attribute registersPlayer
as a class in the Godot engine. See API docs for details about#[derive(GodotClass)]
. -
The optional
#[class]
attribute configures how the class is registered. In this case, we specify thatPlayer
inherits Godot'sSprite2D
class. If you don't specify thebase
key, the base class will implicitly beRefCounted
, just as if you omitted theextends
keyword in GDScript. -
We define two fields
speed
andangular_speed
for the logic. These are regular Rust fields, no magic involved. More about their use later. -
The
Base<T>
type is used for thebase
field, which allowsself
to access the base instance (via composition, as Rust does not have native inheritance). This enables two methods that can be accessed asself.base()
andself.base_mut()
on your type (through an extension trait).T
must match the declared base class. For example,#[class(base=Sprite2D)]
impliesBase<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 inheritingRefCounted
.
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::classes::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.
-
#[godot_api]
- this lets gdext know that the followingimpl
block is part of the Rust API to expose to Godot. This attribute is required here; accidentally forgetting it will cause a compile error. -
impl ISprite2D
- each of the engine classes has aI{ClassName}
trait, which comes with virtual functions for that specific class, as well as general-purpose functionality such asinit
(the constructor) orto_string
(String conversion). The trait has no required methods. -
The
init
constructor is an associated function ("static method" in other languages) that takes the base instance as argument and returns a constructed instance ofSelf
. While the base is usually just forwarded, the constructor is the place to initialize all your other fields. In this example, we assign initial values400.0
andPI
.
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::classes::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.
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.
Launching the Godot application
Unfortunately there is a GDExtension limitation that prevents recompilation while the editor is open before Godot 4.2. Since Godot 4.2, it is possible to hot-reload extensions. This means you can recompile your Rust code and Godot will pick up changes, without needing to restart the editor.
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 for more information.
We now add a translation component to the sprite, following the upstream tutorial.
#![allow(unused)] fn main() { use godot::classes::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.
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 theemit_signal
method (which every Godot class provides, since it is inherited fromObject
).
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
,Vector4
1,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. They are accessible through godot::builtin
, and most symbols are also part of the prelude.
Most builtins have a 1:1 equivalent (e.g. Vector2f
, Color
etc.). The following list highlights some noteworthy mappings:
GDScript type | Rust type | Rust example expression |
---|---|---|
int | i64 2 | -12345 |
float | f64 2 | 3.14159 |
real | real (either f32 or f64 ) | real!(3.14159) |
String | GString | "Some string".into() |
StringName | StringName | "MyClass".into() |
NodePath | NodePath | Nodes/MyNode".into() |
Array[T] | Array<T> | array![1, 2, 3] |
Array | VariantArray or Array<Variant> | varray![1, "two", true] |
Dictionary | Dictionary | dict!{"key": "value"} |
AABB | Aabb | Aabb::new(pos, size) |
Object | Gd<Object> | Object::new_alloc() |
SomeClass | Gd<SomeClass> | Resource::new_gd() |
SomeClass (nullable) | Option<Gd<SomeClass>> | None |
Variant (also implicit) | Variant | Variant::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.3
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 currently does 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. This is why the []
operator (Index/IndexMut
traits)
is absent, and at()
is provided instead, returning by value.
#![allow(unused)] fn main() { let a = array![0, 11, 22]; assert_eq!(a.len(), 3); assert_eq!(a.at(1), 11); // Panics on out-of-bounds. assert_eq!(a.get(1), Some(11)); // Also by value, not Some(&11). 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!(b.is_empty()); 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.at("str"), "hello".to_variant()); // Panics on missing key. assert_eq!(c.get("int"), Some(42.to_variant())); // Option<Variant>, again by value. 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 to the collection. This also means it's your
responsibility to ensure that the array/dictionary 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() { // elem has type GString. println!("Element: {elem}"); } for (key, value) in d.iter_shared() { // key and value both have type Variant. println!("Key: {key}, value: {value}"); } }
Packed arrays
Packed*Array
types are used for storing elements space-efficiently ("packed") in contiguous memory.
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]); // Get/set individual elements using Index and IndexMut operators. ints[1] = 5; assert_eq!(ints[1], 5); // Access as Rust shared/mutable slices. let bytes_slice: &[u8] = b.as_slice(); let ints_slice: &mut [i32] = i.as_mut_slice(); // Access sub-ranges of the array using the same type. let part: PackedByteArray = bytes.subarray(1, 3); // 1..3, or 1..=2 assert_eq!(part.as_slice(), &[0x0B, 0x0C]); }
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
PackedVector4Array
is only available since Godot version 4.3; added in PR #85474.
Godot's int
and float
types are canonically mapped to i64
and f64
in Rust. However, some Godot APIs specify the domain of
these types more specifically, so it's possible to encounter i8
, u64
, f32
etc.
When constructing StringName
from &str
or String
, the conversion is rather expensive, since UTF-8 is re-encoded as
UTF-32. As Rust recently introduced C-string literals (c"hello"
), we can now directly construct from them in case of ASCII. 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 useRefCounted
. Object
is the root of the inheritance tree. It is rarely used directly, but it is the base class ofNode
andRefCounted
. Use it only when you really need it; it requires manual memory management and is harder to handle.
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):
-
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"); }
-
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>(); }
-
A signal handler for the
body_entered
signal of aNode3D
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.
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::classes
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".into(), 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".into(), 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 struct
s 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:
-
Exporting a property. This does not register the property with Godot, but renders it visible in the editor.
- GDScript uses the
@export
annotation for this, we use#[export]
. - See also GDScript exported properties.
- GDScript uses the
-
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.
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.
#[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
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.
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.
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
- Exporting variables
- Enums
- Advanced usage
- Custom types with
#[var]
and#[export]
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.
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:
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 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.
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)]
.
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.
Script-virtual functions
The GDExtension API allows you to define virtual functions in Rust, which can be overridden in scripts attached to your objects.
Note that these are conceptually different from virtual functions like ready()
, which are defined by Godot and overridden by you (in Rust).
Hence the emphasis on "script-virtual".
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.
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 be10
(the default implementation in Rust). - If the
Monster
node has theOrc.gd
script attached,damage_points
will be20
. - If the
Monster
node has theGoblin.gd
script attached,damage_points
will be a random number between5
and15
.
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
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.
-
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. Callingmonster._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 invokedefault_damage()
inside the Rustdamage()
method. -
No access to
super
methods.In OOP languages, you can call the base method from the overriding method, typically using
super
orbase
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.
-
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 implicitGd::bind()
orGd::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)]
. Thegd_self
requires the first parameter to be of typeGd<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 Godot4.1.1
or4.2
. - You cannot run a
4.2
extension in Godot4.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:
- 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.
- Non-stable releases (alpha, beta, RC).
- 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.
- Godot with non-standard build flags (e.g. disabled modules).
- 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
Supporting multiple Godot versions is a key feature of gdext. Especially if you plan to share your extension with others (as a library or an editor plugin), this page elaborates your choices and their trade-offs in detail. The previous chapter about compatibility is expected as a prerequisite.
Table of contents
Motivation
To refresh, you have two Godot versions to consider:
-
API version, against which gdext compiles.
- Affects Rust symbols (classes, methods, etc.) you have available at compile time.
- This sets a lower bound on the Godot binary you can run your extension in.
-
Runtime version, the Godot engine version, in which you run the Rust extension.
- Affects the runtime behavior, e.g. newer versions may fix some bugs.
- It is advised to stay up-to-date with Godot releases, to benefit from new improvements.
GDExtension is designed to be backward-compatible, so an extension built with a certain API version can be run in all Godot binaries greater than that version.1 Therefore, the lower your API version, the more Godot versions you support.
Why support multiple versions?
The choice you have in the context of gdext is the API version. If you just make a game on your own, the defaults are typically good enough.
Explicitly selecting an API version can be helpful in the following scenarios:
- You run/test your application on different Godot minor versions.
- You are collaborating in a team, or you want to give your Godot project to friends to experiment with.
- You work on a library or plugin to share with the community, either open-source (distributed as code) or closed-source (distributed as compiled dynamic library).
Especially in the last case, you may want your extension to be compatible with as many Godot versions as possible, to reach a broader audience.
At first glance, it may not seem obvious why a plugin would support anything but the latest Godot version. After all, users can just update, right?
However, sometimes users cannot update their Godot version due to regressions, incompatibilities or project/company constraints.
Furthermore, imagine you want to use two GDExtension plugins: X (API level 4.3) and Y (4.2). Unfortunately, Y contains a bug that causes some issues with Godot 4.3. This means you cannot use both together, and you are left with some suboptimal choices:
- Only use X on 4.3.
- Only use Y on 4.2.
- Help the author of Y to patch the bug. But they may just sail the Caribbean and not respond on their repo. Or worse, Y might even be a closed-source plugin that you paid for.
Not only are you now left with a less-than-ideal situation, but you cannot build your own tool Z which uses both X and Y, either. Had X declared API 4.2, people could stick to that version until Y is fixed, and you too could release Z with API 4.2.
A longer compatibility range gives users more flexibility regarding when they update what. It accounts for the fact that developers iterate at varying pace, and enables projects to depend on each other. At scale, this enables a vibrant ecosystem of extensions around Godot.
Cutting edge vs. compatibility
Lower API versions allow supporting a wider range of Godot versions. For example, if you set the API version to 4.2, you can run it in Godot 4.2, 4.2.2 or 4.3, but not Godot 4.1.
On the flip side, lower API versions reduce the API surface that you can statically2 use in your Rust extension. If you select 4.2, you will not see classes and functions introduced in 4.3.
This is the core trade-off, and you need to decide based on your use case. If you are unsure, you can always start with a conservatively low API version, and bump it when you find yourself needing more recent features.
Selecting the API version in gdext
Now that the why part is clarified, let's get into how you can choose the API version in gdext.
Default version
By default, gdext uses the current minor release of Godot 4, with patch 0. This ensures that it can be run with all Godot patch versions for that minor release.
Example: if the current release is Godot 4.3.5, then gdext will use API version 4.3.0.
Lower minor version
To change the API level to a lower version, simply turn on the Cargo feature api-4-x
, where x
is the minor version you want to target.
Example in Cargo.toml:
[dependencies]
# API level 4.2
godot = { ..., features = ["api-4-2"] }
You can also explicitly set the current minor version (the same as the default). This has the advantage that you keep that compatibility, even once gdext starts targeting a newer version by default.
Lower or higher patch version
gdext supports API version granularity on a patch level, if absolutely needed. This is rarely necessary and can cause confusion to users, so only select a patch-level API if you have a very good reasons. Also note that GDExtension itself is only updated in minor releases.
Reasons to want this might be:
- Godot ships a bugfix in a patch version that is vital for your extension to function properly.
- A new API is introduced in a patch version, and you would like its class/function definitions. This happens quite rarely.
To require a minimum patch level, use a api-4-x-y
feature:
[dependencies]
# API level 4.2.1
godot = { ..., features = ["api-4-2-1"] }
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
api-custom
. If enabled, this will look for a Godot binary in two locations, in this order:
- The environment variable
GODOT4_BIN
. - The binary
godot4
in yourPATH
.
Generated code inside the godot::builtin
, godot::classes
and godot::global
modules may now look different from stable releases.
Note that we do not give any support or compatibility guarantees for custom-built GDExtension APIs.
Working with the api-custom
feature requires the bindgen
crate, 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
.
Footnotes
Godot 4.0 has been released before the GDExtension API committed to stability, so no single 4.0.x release is compatible with any other release (not even patch versions among each other). We provide 4.0 API levels, but due to their limited utility, we will phase out support very soon.
Even if your API level is 4.2, it is possible to access 4.3 features, but you need to do so dynamically. This can be
achieved using reflection APIs like Object::call()
, but you lose the type safety and convenience of the statically generated API.
To obtain version information, check out the GdextBuild
API.
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 Android
Exporting with gdext for Godot requires some of the same pieces that are required for building Godot from source. Specifically, the Android SDK Command Line Tools and JDK 17 as mentioned in Godot's documentation here.
Once you have those installed, you then need to follow Godot's instructions for setting up the build system here.
To find the jdk and nkd versions that are needed, reference the Godot configuration that your version of Godot is using. For example:
Compiling
The environment variable CLANG_PATH
is used by bindgen's clang-sys dependency. See also
clang-sys documentation
Set the environment variable CLANG_PATH
to point to Android's build of clang. Example:
export CLANG_PATH=\
"{androidCliDirectory}/{androidCliVersion}/ndk/{ndkVersion}/toolchains/llvm/prebuilt/{hostMachineOs}/bin/clang"
Then set the CARGO_TARGET_{shoutTargetTriple}_LINKER
to point to the Android linker for the Android triple you are targeting.
The {shoutTargetTriple}
should be in SHOUT_CASE
so that a triple such as aarch64-linux-android
becomes AARCH64_LINUX_ANDROID
.
You need to compile your gdext library for each Android triple individually. Possible targets can be found by running:
rustup target list
You can find the linkers in the Android CLI directory at:
{androidCliDirectory}/{androidCliVersion}/ndk/{ndkVersion}/toolchains/llvm/prebuilt/
{hostMachineOs}/bin/{targetTriple}{androidVersion}
As of writing this, the tested triples are:
Triple | Environment Variable | Godot Arch | GDExtension Config |
---|---|---|---|
aarch64-linux-android | CARGO_TARGET_AARCH64_LINUX_ANDROID_LINKER | arm64 | android.debug.arm64 |
x86_64-linux-android | CARGO_TARGET_X86_64_LINUX_ANDROID_LINKER | x86_64 | android.debug.x86_64 |
armv7-linux-androideabi | CARGO_TARGET_ARMV7_LINUX_ANDROID_LINKER | arm32 | android.debug.armeabi-v7a |
i686-linux-android | CARGO_TARGET_I686_LINUX_ANDROID_LINKER | x86_32 | android.debug.x86 |
Notice how the environment variables are in all-caps and the triple's "-" is replaced with "_".
Make sure to add all of the triples you want to support to rustup
via:
rustup target add {targetTriple}
Example:
rustup target add aarch64-linux-android
A complete example
Putting it all together, here is an example compiling for aarch64-linux-android
. This is also probably the most common
Android target, as of the writing of this.
Assuming the following things:
- Android CLI is installed in the
$HOME
folder. - Godot is still relying on Android NDK version 23.2.8568313. Check here.
- The downloaded Android CLI version is: 11076708_latest (update this to be the version you downloaded).
- This is being run on Linux. Change the
linux-x86_64
folder inCLANG_PATH
andCARGO_TARGET_AARCH64_LINUX_ANDROID_LINKER
to be your host machine's operating system. - You are targeting Android version 34.
And here is what the commands look like running from a bash shell:
rustup target add aarch64-linux-android
export CLANG_PATH="$HOME/android-cli/11076708_latest/ndk/23.2.8568313/toolchains/llvm/prebuilt/linux-x86_64/bin/clang"
export CARGO_TARGET_AARCH64_LINUX_ANDROID_LINKER=\
"$HOME/android-cli/11076708_latest/ndk/23.2.8568313/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android34-clang"
cargo build --target=aarch64-linux-android
And then you should find a built version of your GDExtension library in:
target/aarch64-linux-android/debug/{YourCrate}.so
Make sure to update your .gdextension
file to point to the compiled lib. Example:
android.debug.arm64="res://path/to/rust/lib/target/aarch64-linux-android/debug/{YourCrate}.so
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.
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 emcc
2 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 yourCargo.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.
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.
- 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.
Note: Due to a bug with emscripten
, the maximum version of emcc
2 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
.
emcc
is the name of Emscripten's compiler.
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.
Export to macOS and iOS
Mac libraries that are intended to be shared with other people require Code Signing and Notarization. For this you will need:
- A Mac Computer
- An Apple ID enrolled in Apple Developer Program (99 USD per year)
Without Code Signing and Notarization, the other person can still use the built library, but either needs to:
- rebuild the whole thing locally
- re-sign it
- accept that it may contain malicious code.
Prerequisites:
- Download and install Xcode on your Mac computer.
Building macOS universal lib
Add both x64 and arm64 targets. This is needed in order to create a universal build.
rustup target add x86_64-apple-darwin
rustup target add aarch64-apple-darwin
Build the library for both target architectures:
cargo build --target=x86_64-apple-darwin --release
cargo build --target=aarch64-apple-darwin --release
Run the lipo tool to merge the two in one universal library.
lipo -create -output target/release/lib{YourCrate}.macos.dylib \
target/aarch64-apple-darwin/release/lib{YourCrate}.dylib \
target/x86_64-apple-darwin/release/lib{YourCrate}.dylib
The result of this will be the file target/release/lib{YourCrate}.macos.dylib
that will now have support for both x64 and arm64 platforms.
The user would need to replace {YourCrate}
with the crate name.
The name of your library will be the one you provided in Cargo.toml
file, prefixed with lib
and followed by .dylib
:
[package]
name = "{YourCrate}"
Next, you will need to create the .framework
folder.
mkdir target/release/lib{YourCrate}.macos.framework
cp target/release/lib{YourCrate}.macos.dylib \
target/release/lib{YourCrate}.macos.framework/lib{YourCrate}.macos.dylib
Next, create the Info.plist
file inside the Resources
folder:
mkdir target/release/lib{YourCrate}.macos.framework/Resources
File contents:
<?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>CFBundleExecutable</key>
<string>lib{YourCrate}.macos.dylib</string>
<key>CFBundleIdentifier</key>
<string>org.mywebsite.myapp</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>My App Name</string>
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>1.0.0</string>
<key>CFBundleSupportedPlatforms</key>
<array>
<string>MacOSX</string>
</array>
<key>NSHumanReadableCopyright</key>
<string>Copyright (c)...</string>
<key>CFBundleVersion</key>
<string>1.0.0</string>
<key>LSMinimumSystemVersion</key>
<string>10.12</string>
</dict>
</plist>
The CFBundleExecutable
name must match the dylib file name. Some of the contents in the XML file must not contain some characters.
Generally avoid using anything other than letters and numbers.
Related StackOverflow issue.
Edit the project's .gdextension
file to include support for macOS.
This file will probably be at godot/{YourCrate}.gdextension
.
The format will be similar to the following:
[libraries]
...
macos.release = "res://../rust/target/release/lib{YourCrate}.macos.framework"
Building an iOS library
Add as target arm64 iOS.
rustup target add aarch64-apple-ios
Build the library:
cargo build --target=aarch64-apple-ios --release
The result of this will be the file target/aarch64-apple-ios/release/lib{YourCrate}.dylib
.
Next, you will need to create the .framework
folder.
mkdir target/release/lib{YourCrate}.ios.framework
cp target/release/lib{YourCrate}.ios.dylib \
target/release/lib{YourCrate}.ios.framework/lib{YourCrate}.ios.dylib
Next, create the Info.plist
file inside the .framework
folder, with the following contents:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>lib{YourCrate}.ios.dylib</string>
<key>CFBundleName</key>
<string>My App Name</string>
<key>CFBundleDisplayName</key>
<string>My App Name</string>
<key>CFBundleIdentifier</key>
<string>org.my-website.my-app</string>
<key>NSHumanReadableCopyright</key>
<string>Copyright (c) ...</string>
<key>CFBundleVersion</key>
<string>0.12.0</string>
<key>CFBundleShortVersionString</key>
<string>0.12.0</string>
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CSResourcesFileMapped</key>
<true/>
<key>DTPlatformName</key>
<string>iphoneos</string>
<key>MinimumOSVersion</key>
<string>12.0</string>
</dict>
</plist>
See XML format requirements above.
Edit the project's .gdextension
file to include support for iOS.
This file will probably be at godot/{YourCrate}.gdextension
.
The format will be similar to the following:
[libraries]
...
ios.release = "res://../rust/target/release/lib{YourCrate}.ios.framework"
Code Signing and Notarizing (macOS only)
This step is only needed if you want to share the library. If you are building the whole game, you will sign everything and don't need to sign the library. You can skip to Godot Build step.
In order to code-sign and notarize your app, you will first need to gather some information from your enrolled Apple Developer account. We will create corresponding environment variables and use a script to sign, so it's easier to run. Here are the environment variables needed:
APPLE_CERT_BASE64
APPLE_CERT_PASSWORD
APPLE_DEV_ID
APPLE_DEV_TEAM_ID
APPLE_DEV_PASSWORD
APPLE_DEV_APP_ID
Firstly, make sure to enroll your Apple ID to the Developer Program:
- Create an Apple ID if you don't have one already.
- Use your Apple ID to register in the Apple Developer Program by going to developer.apple.com.
- Accept all agreements from the Apple Developer Page.
APPLE_DEV_ID
- Apple ID
Your email used for your Apple ID.
APPLE_DEV_ID = email@provider.com
APPLE_DEV_TEAM_ID
- Apple Team ID
Go to developer.apple.com. Go to account.
Go to membership details. Copy Team ID.
APPLE_DEV_TEAM_ID = 1ABCD23EFG
APPLE_DEV_PASSWORD
- Apple App-Specific Password
Create Apple App-Specific Password. Copy the password.
APPLE_DEV_PASSWORD = abcd-abcd-abcd-abcd
APPLE_CERT_BASE64
, APPLE_CERT_PASSWORD
and APPLE_DEV_APP_ID
Go to developer.apple.com. Go to account.
Go to certificates.
Click on + at Certificates tab. Create Developer ID Application. Click Continue.
Leave profile type as is. Create a certificate signing request from a Mac. You can use your own name and email address. Save the file to disk.
You will get a file called CertificateSigningRequest.certSigningRequest
. Upload it to the Developer ID Application request. Click Continue.
Download the certificate. You will get a file developerID_application.cer
.
On a Mac, right click and select open. Add it to the login keychain. In the Keychain Access app that opened, log into Keychain tab, go to Keys, sort by date modified, and expand your key (the key should have the name you entered at Common Name). Right click the expanded certificate, get info, and copy the text at Details -> Subject Name -> Common Name. For example:
APPLE_DEV_APP_ID = Developer ID Application: Common Name (1ABCD23EFG)
Then, select the certificate, right click and click export. At file format select p12
. When exporting, set a password for the certificate.
This will be the value of APPLE_CERT_PASSWORD
. You will get a Certificates.p12
file.
For example:
APPLE_CERT_PASSWORD = <password_set_when_exporting_p12>
Then you need to make a base64 file out of it, by running:
base64 -i Certificates.p12 -o Certificates.base64
Copy the contents of the generated file, e.g.:
APPLE_CERT_BASE64 = ...(A long text file)
After these secrets are obtained, all that remains is to set them as environment variables. Afterwards you can use the following script for signing ci-sign-macos.ps1. In order to run this script you will need to install powershell on your Mac.
ci-sign-macos.ps1 target/release/{YourCrate}.framework
The user is responsible for the security and up-to-dateness of the script.
Godot Build
After building the libraries, you can now distribute them as they are, or build the whole game using Godot. For that, follow Godot's How to export guide:
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 Resource
s are exposed to the end user to use within their development. Resource
s 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.
Inspector plugins
The inspector dock allows you to create custom widgets to edit properties through plugins. This can be beneficial when working with custom datatypes and resources, although you can use the feature to change the inspector widgets for built-in types. You can design custom controls for specific properties, entire objects, and even separate controls associated with particular datatypes. For more info, see docs.godotengine.org.
The example in the Godot docs in Rust. It will replace integer input with a button that creates a random value.
Before (int input):
After (button):
Add this dependency to Rust with the shell in the same directory as Cargo.toml
.
cargo add rand
Add file addon.rs
and import it in lib.rs
:
#![allow(unused)] fn main() { // file: lib.rs mod addon; }
Add the following imports at the beginning of the file:
#![allow(unused)] fn main() { use godot::classes::{ Button, EditorInspectorPlugin, EditorPlugin, EditorProperty, IEditorInspectorPlugin, IEditorPlugin, IEditorProperty, }; use godot::global; use godot::prelude::*; use rand::Rng; }
Since Rust is a statically typed language, we will proceed in reverse order unlike in Godot documentation, to avoid encountering errors unnecessarily.
Add Property Editor
To begin with, let's define the editor for properties:
#![allow(unused)] fn main() { #[derive(GodotClass)] #[class(tool, init, base=EditorProperty)] struct RandomIntEditor { base: Base<EditorProperty>, button: Option<Gd<Button>>, } }
After that, we need to add an implementation for the trait IEditorProperty
:
#![allow(unused)] fn main() { #[godot_api] impl IEditorProperty for RandomIntEditor { fn enter_tree(&mut self) { // Create button element. let mut button = Button::new_alloc(); // Add handler for this button, handle_press will be define in another impl. button.connect("pressed".into(), self.base().callable("handle_press")); button.set_text("Randomize".into()); // Save pointer to the button into struct. self.button = Some(button.clone()); self.base_mut().add_child(button.upcast()); } fn exit_tree(&mut self) { // Remove element from inspector when this plugin unmount: if let Some(button) = self.button.take() { self.base_mut().remove_child(button.upcast()); } else { // Log error if button disappeared before godot_error!("Button wasn't found in exit_tree"); } } } }
Let's add a handler for the button:
#![allow(unused)] fn main() { #[godot_api] impl RandomIntEditor { #[func] fn handle_press(&mut self) { // Update value by button click: // - Take property name, randomize number. // - Send property name and random number to Godot engine to update value. // - Update button text. let property_name = self.base().get_edited_property(); let num = rand::thread_rng().gen_range(0..100); godot_print!("Randomize! {num} for {property_name}"); self.base_mut() .emit_changed(property_name, num.to_variant()); if let Some(mut button) = self.button.clone() { let text = format!("Randomize: {num}"); button.set_text(text.into()); } else { // Print error of something went wrong godot_error!("Button wasn't found in handle_press"); } } } }
Add Inspector plugin
Now we need to connect this editor to fields with an integer type.
To do this, we need to create an EditorInspectorPlugin
.
#![allow(unused)] fn main() { #[derive(GodotClass)] #[class(tool, init, base=EditorInspectorPlugin)] struct RandomInspectorPlugin { base: Base<EditorInspectorPlugin>, } }
To add a property editor (which we implemented earlier), you need to implement the IEditorInspectorPlugin
trait:
#![allow(unused)] fn main() { #[godot_api] impl IEditorInspectorPlugin for RandomInspectorPlugin { fn parse_property( &mut self, _object: Gd<Object>, // object that is being inspected value_type: VariantType, name: GString, _hint_type: global::PropertyHint, _hit_string: GString, _flags: global::PropertyUsageFlags, _wide: bool, ) -> bool { if value_type == VariantType::INT { self.base_mut() .add_property_editor(name, RandomIntEditor::new_alloc().upcast()); return true; } false } // This method says Godot that this plugin handle the object if it returns true fn can_handle(&self, object: Gd<Object>) -> bool { // This plugin handle only Node2D and object that extends it object.is_class("Node2D".into()) } } }
If parse_property
returns true
, the editor plugin will be created and replace the current
representation; if not, it's necessary to return false
.
This allows you to control where and how processing is done by this plugin.
Adding an editor plugin
Only one thing left to do: define the editor plugin that will kick off all this magic!
This can be a generic EditorPlugin
or a more specific InspectorEditorPlugin
, depending
on what you want to achieve.
#![allow(unused)] fn main() { #[derive(GodotClass)] #[class(tool, init, editor_plugin, base=EditorPlugin)] struct RustEditorPlugin { base: Base<EditorPlugin>, random_inspector: Gd<RandomInspectorPlugin>, } }
#![allow(unused)] fn main() { #[godot_api] impl IEditorPlugin for RustEditorPlugin { fn enter_tree(&mut self) { // Create our inspector plugin and save it. let plugin = RandomInspectorPlugin::new_gd(); self.random_inspector = plugin.clone(); self.base_mut().add_inspector_plugin(plugin.upcast()); } fn exit_tree(&mut self) { // Remove inspector plugin when editor plugin leaves scene tree. let plugin = self.random_inspector.clone(); self.base_mut().remove_inspector_plugin(plugin.upcast()); } } }
Sometimes after compilation, you may encounter errors or panic. Most likely, all you need to do is simply restart the Godot Editor.
Example error:
Initialize godot-rust (API v4.2.stable.official, runtime v4.2.2.stable.official)
ERROR: Cannot get class 'RandomInspectorPlugin'.
at: (core/object/class_db.cpp:392)
ERROR: Cannot get class 'RandomInspectorPlugin'.
at: (core/object/class_db.cpp:392)
Engine singletons
It is important for you to understand the Singleton pattern to properly utilize this system.
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::classes::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
- Registering a singleton
- Calling from GDScript
- Calling from Rust
- Singletons and the
SceneTree
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(); } } } }
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::classes::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"
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.
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:
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:
-
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.
-
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.
-
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.
-
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 :)
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:
- rustfmt for code formatting (config options).
- clippy for lints and style warnings (list of lints).
- Clang's AddressSanitizer and LeakSanitizer for memory safety.
- Various specialized tools:
- skywalking-eyes to enforce license headers.
- cargo-deny and cargo-machete for dependency verification.
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
-
Anything that is not intended to be accessible by the user, but must be
pub
for technical reasons, should be marked as#[doc(hidden)]
.- This does not constitute part of the public API.
-
We do not use the
prelude
inside the project, except in examples and doctests. -
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)]
)
- Type aliases in traits (
-
Inside files, there is no strict order yet, except
use
andmod
at the top. Prefer to declare public-facing symbols before private ones. -
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
-
Avoid tuple-enums
enum E { Var(u32, u32) }
and tuple-structsstruct S(u32, u32)
with more than 1 field. Use named fields instead. -
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
-
Getters don't have a
get_
prefix. -
Use
self
instead of&self
forCopy
types, unless they are really big (such asTransform3D
). -
For
Copy
types, avoid in-place mutationvector.normalize()
.
Instead, usevector = vector.normalized()
. The past tense indicates a copy. -
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]
.
-
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. justkey
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.
- Keys are always
-
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.