2023 in Review:
Establishing Rust as a Godot 4 language
2023 was a crucial year for the godot-rust project. Not only did Rust gain traction as a language in the Godot ecosystem, but we also saw the release of Godot 4.0 and with it an important milestone towards production-readiness.
This article serves as a retrospective on the past year and highlights the achievements of the godot-rust community.
Where it started
2022 was a year of extensive tinkering in a small setting, accompanied by highly appreciated input from cuddlefishie, chitoyuu and Waridley, who were already involved with the gdnative library (the Rust bindings for Godot 3). In November 2022, I open-sourced gdext as the experimental Godot 4 bindings.
While the library was able to run the dodge-the-creeps example, a lot of central features were missing at the time. Arrays and dictionaries were not implemented, and neither were property exports. The project featured the absolute minimum to run the example. There were loads of bugs and safety issues. Nevertheless, 2022 resulted in a working prototype and an open-source foundation for the community to build upon.
January and February 2023 marked the point where several early adopters joined and contributed missing builtins: arrays, packed arrays, dictionaries, vectors, quaternions, matrices and colors. We also cultivated issue #24 as a central point for an up-to-date overview of the implemented features. Furthermore, utility functions, property exports (#45, #177) and a GDScript test runner were implemented. Most of the contributions at the time came from ttencate, mhoff12358 and lilizoey (who has also helped maintain the library since). Thanks a lot for jumping into the cold water!
Godot 4.0 and the road to stability
After years of ambitious work, Godot developers released major version 4.0 on March 1st, effectively turning Godot 4 from an in-dev prototype into a tangible release. According to official plans, it would take 1-2 minor versions to get a lot of the initial work ironed out (due to new user feedback), but the foundation was definitely there.
For binding maintainers, 4.0 was a great relief as well, since it meant that we'd now have official versions to target and that breaking changes would not occur every other week anymore -- as was the case during Godot 4 alpha and beta stages.
However, the GDExtension API was not fully ready at the time; its design didn't allow for long-term binary backward compatibility. This was fixed for Godot 4.1, in large parts due to the great efforts of dsnopek and the GDExtension team, who came up with thorough proposals toward stability and extensibility.
The significance of backward compatibility from 4.1 onwards cannot be overstated: it meant that users developing an extension in 4.1 can still run it with Godot 4.9. What sounds like a "nice-to-have" is absolutely essential for building an ecosystem: if you want to combine a handful GDExtension plugins (e.g. some written in C++, some in Swift, some in Rust), it's very likely that they target different Godot versions and are maintained with varying activity. Not having compatibility would force you to recompile every single plugin for the Godot version you target.
Features upon features
Spring and summer were defined by a continued mapping of GDExtension APIs to Rust.
Some noteworthy highlights:
- Virtual function traits extended virtual methods far beyond the hardcoded
ready()
,process()
andphysics_process()
. A dedicated trait for each class allowed to very specifically select which functionality to override. - Default parameters allowed to optionally provide arguments to engine APIs. Since Rust doesn't natively offer default parameters, we use the builder pattern.
- Example:
object.connect_ex(signal, callable).flags(flags).done()
- Example:
- Improved code generation for engine API.
- Extended builtin support and integration of Godot data structures with Rust idioms.
Callable
GodotString::chars_checked()
to access UTF-32 characters.PackedArray::as_slice()
to access elements via slice.
- Registration of Rust symbols.
#[derive(FromVariant, ToVariant)]
swiftly maps user types to and fromVariant
.- User-defined constants
- User-defined enums
- Notification API: type-safe rather than integer constants.
- Export parameters: GDScript's
@export_range
etc. OnReady<T>
to enable late initialization of properties.
- Hot reloading was one of the most anticipated Godot 4.2 features. Its integration into gdext marked a tremendous improvement for dev cycles, no longer needing to restart the editor.
- Higher-level abstractions: in cases where a "raw" Godot API is prone to memory/type unsafety or other errors, higher-level interfaces can provide a nicer user experience. Examples include:
- Script extensions, allowing the user to implement
GDExtensionScriptInstance
. - The
GFile
struct, which wrapsFileAccess
and integrates it with Rust'sstd::io
traits. load::<T>(file_path)
to load Godot resources from theres://
filesystem.get_node_as::<T>(node_path)
to get a node, given a path and type.InstanceId
as a non-null object ID.godot_print!
that prints to the Godot console and works likeprintln!
.
- Script extensions, allowing the user to implement
- Support for more build configurations.
- Double-precision builds enabled using Godot's
precision=double
build flag. - An early approach to threading opened the door to further discussion on
Sync
types. - The
serde
feature enabled serialization with theserde
crate (later extended in #508).
- Double-precision builds enabled using Godot's
WebAssembly support
Initial support for Web exports has been added in pull request #493. While still experimental, this addition is a proof-of-concept that Rust can be used for Godot web games and marks a huge step forward in our platform support. There is also a tutorial in the book explaining the setup.
Thanks a lot to zecozephyr, Esption and PgBiel for their great collaboration towards this achievement.
Large-scale refactors and FFI hell
Now that the shiny parts are out of the way, let's descend into the mines.
The library underwent a lot of bugfixes and refactorings during 2023, many of which are related to the interaction with the GDExtension C API via FFI. These are typically not witnessed immediately, but contribute to an overall more robust and streamlined experience. FFI with Godot is a topic of significant complexity, especially because there are so many different integration points, all of which work in subtly different ways.1
It would be too much to list all the changes, but here is a sample of larger refactorings:
ToGodot
/FromGodot
traits unified two previously competing marshalling mechanisms into a single API. This reduces the burden for users to make their types interoperate with Godot, while at the same time benefiting from optimizations (FFI ptrcalls) where possible.- Lazy function pointers significantly reduce both runtime overhead to perform FFI calls and the amount of code that needs to be compiled in
godot-core
. An extended discussion of the improvements is available in the blog post FFI Optimizations and Benchmarking. - Pointer call refactoring changed the way how values are passed to and from Godot, fixing several use-after-free bugs.
A more detailed insight can be obtained by looking at merged pull requests with the ub
label (15 at the time of writing). These include fixes related to undefined behavior, and thus improved robustness of the library. Almost all of them are covered by regression tests, ensuring that we won't encounter them again in the future.
Tooling and infrastructure
The godot-rust project involves much more than just code. We have strived to improve the contribution experience for newcomers, providing short and helpful feedback loops. Efforts that have gone into infrastructure span different areas:
- Ever-increasing CI coverage boosts confidence that new contributions are held to a high standard and that existing functionality keeps working. Cargo's toolchain lets us run unit tests, clippy and rustfmt. Additionally, we built a custom integration test suite that runs gdext code against multiple versions of Godot in CI. A minimalist local version of these checks additionally exists as the
check.sh
script. - Address and leak sanitizers are tools from the C++ ecosystem, but equally useful in Rust. We cannot generally use
miri
due to FFI; however, address sanitizers can detect undefined behavior such as use-after-free or out-of-bound access. Leak sanitizers can detect memory leaks. - Prebuilt artifacts shrink the dependency tree to around 30% of its previous size, avoiding heavy crates like
bindgen
andsyn
. By pre-compilingbindgen
artifacts for different platforms, initial builds are sped up considerably and an external LLVM installation is no longer needed. This in turn benefits CI times, further improving integration cycles. - The book provides tutorials to get started with gdext and is also maintained on GitHub.
It was split from the gdnative book for clarity. - The website was fundamentally rewritten with a fresh look. It links to all the relevant places, hosts API docs and this devlog.
- Automated doc generation in PRs runs
cargo doc
for every single pull request opened, publishes the results to a temporary page on our website, and lets a bot comment on the PR with a link to the docs. This allows both contributors and reviewers to see how the resulting API looks. - Custom formatting replaces the annoyingly slow
rustfmt
for generated code, while still preserving human readability. - GitHub merge queues are the successor to the
bors
bot. They enforce that checks are always run against latestmaster
, independent of any other changes that have landed since a branch was opened.
See also list of merged tooling-related PRs (54 at the time of writing). While these only include pull requests on the main repo, they give a good insight into all the smaller tweaks behind the scenes.
The gdnative library continues to be maintained -- it is mostly feature-complete and lays a focus on stability and minimizing breaking changes. There is likely going to be a Godot 3.6 LTS release in the coming year. Thanks to chitoyuu for ongoing work on the gdnative front!
Outlook
In numbers, the GitHub project for gdext today has:
- 1239 commits, by 55 contributors
- 2050 stars and 131 forks
- 139 issues closed as completed, 91 open
- 262 pull requests merged, 2 open
2023 was a year of great progress for godot-rust. What started as a small proof-of-concept, has matured a lot in terms of features and robustness. This would not have been remotely possible without the help of the generous open-source community. Thanks a lot to everyone for the countless hours spent -- whether it's as a contributor navigating through FFI mineshafts in search of light, helping others on Discord, or simply trying out the library and providing feedback -- every effort is highly appreciated!
For the coming year, the focus lies on making the core systems even more stable, as well as promoting an ergonomic, pragmatic gamedev workflow. There will likely still be some breaking changes to unify the overall experience, but we try to work with deprecations to ease migrations.
Some of the larger things planned for 2024 are:
- Re-entrant calls for object references (in progress).
- Publish to crates.io.
- Better threading support.
- Builder API for registration.
- Initial support for Android and iOS -- would however need some volunteers (also due to separate devices/toolchains).
The majority of the scaffolding and infrastructure is now there, so future work can focus more directly on innovation. With that and a growing user base, 2024 looks very exciting!
A Happy New Year to everyone!
Footnotes
GDExtension offers different dimensions to interop with Godot, all of which need to be honored for safe FFI:
- Calls to engine: builtin methods, class methods, utility functions.
- Callbacks from engine: virtual functions, separate callbacks for
to_string
,notification
, etc. - Calling convention: varcall vs. ptrcall.
- Memory management: value semantics, manually managed, ref-counted, copy-on-write.