June 2024 dev update

We'll start our summer with another development update on godot-rust.

Crates.io release

Let's begin with the biggest one: we released on crates.io!

Aptly named godot, you can immediately start using the crate:

[dependencies]
godot = "0.1"

This may come a bit late for some, however there weren't many benefits for crate releases in the early stages, whereas Cargo has integrated well with Git repos. In 2023, the library quickly progressed, and a more relaxed versioning approach enabled fast prototyping. Experimentation still happens, but things are a bit more stable now.

Unlike typical Rust projects, our dependency setup is relatively complex: besides Rust crate version, we also account for an orthogonal Godot version. It is a declared goal of godot-rust to provide compatibility with multiple Godot versions at the same time. For this, we have converged towards a very flexible approach (see next section), however this needed a lot of design work, as well as a great deal of automation to cover 13+ individual Godot stable releases.

Selecting an API level

The API level specifies the minimum version of GDExtension (and the Godot class API) with which gdext is compatible. This is especially important for plugin writers who want to support multiple Godot versions; more details in the book.

The API level previously required a [patch] statement on a top-level Cargo.toml to bend dependencies to comply. Now, this is much simpler, just set the api-* feature when adding the godot-rust dependency. By default, a recent stable minor release is used (currently 4.2).

[dependencies]
godot = { version = "0.1", features = ["api-4.1"] } # Compatible with 4.1+

Streamlined modules

The old module structure grew organically as we mapped more and more Godot APIs, thus a lot of new symbols were just added where it was convenient to develop, rather than where they made sense from a user perspective. A good example was the godot::engine god module which contained everything from Godot classes/enums/utility functions over GFile and ScriptInstance high-level APIs, to translation macros.

The most important modules are now:

  • Godot API: builtin types, classes, global enums/utilities/print
  • Extra functionality on top of Godot: tools
  • Object-related: obj, and obj::script for script instances
  • Registration of Rust symbols: register
  • Entry point and type meta-information: init and meta

The refactoring details are listed in #729, #732, #733, #735, and #738.

Script instance API

Script instances are a relatively unknown but very powerful GDExtension feature that allows custom implementations of scripts, in our case Rust-based ones. This means you can attach a Rust script to scene nodes, like you would do with GDScript or C# scripts.

TitanNano has done groundbreaking work on that front, improving UX by enabling re-entrancy, fixing property/method accessors or properly handling size changes. His improvements have led to ScriptInstance implementations behaving more and more like GodotClass ones, thus feeling very natural within gdext.

Multithreading fixes

Situations where Godot accesses Rust code from different threads (e.g. in callbacks) has sometimes caused panics, because access went through naive dynamic borrow-checks (à la RefCell). Thanks to TitanNano and lilizoey, these obstacles have been removed with proper synchronization.

Collection accessors

Arrays and dictionaries received better accessors, for example an at(index) function. Even untyped Dictionaries are quite ergonomic to use, if you know the element types:

// Create empty dictionary and add key-value pairs.
let dict = dict! {
    "key": "some value",
    "key2": 345,
    "key2": Vector2i::new(1, 2),
};

// Access elements, typed and untyped.
let value: Variant = dict.at("key");
let value: GString = dict.at("key").to(); // Variant::to() extracts GString.
let maybe: Option<Variant> = dict.get("absent_key"); // None.

// Iterate as (Variant, Variant).
for (key, value) in dict.iter_shared() { ... }

// Key type is known -- iterate over (String, Variant).
for (key, value) in dict.iter_shared().typed::<String, Variant>() { ... }

On the packed side, Packed*Array types now have an API which is more consistent with Array. In PR #725, I added the Index/IndexMut traits which allow -- unlike Array/Dictionary -- direct element referencing via array[index] syntax.

Geometric API consistency

The user joriskleiber has recently contributed two important refactorings to the geometric types in godot::builtin. On one hand, more of the vector API was moved to macros, which ensures that function signatures and implementation are identical for the 6 vector types (Vector2, Vector2i, Vector3, Vector3i, Vector4, Vector4i).

On the other, there was quite an inconsistent handling of by-ref and by-value parameters, most notably self vs &self in operations that don't modify the instance. Joris addressed this in #751, so that all geometric primities in one "group" (e.g. vector, matrix, bounding box) now have the same signatures. Furthermore, there is an ongoing PR to add functions such as normalized_or_zero() to vectors.

Error messages

Rust 1.78 added #[diagnostic::on_unimplemented], a feature that allows customizing trait error messages. We immediately implemented this in #692, as it benefits user-facing error messages in many places.

As an example, the following class should have either #[class(init)] or a manual init function, or alternatively #[class(no_init)] to disable the constructor.

#[derive(GodotClass)]
struct MyClass {}

Previously, the error message was:

error[E0277]: the trait bound `MyClass: GodotDefault` is not satisfied
--> path/to/file.rs:123:4
    |
123 | struct MyClass {}
    |        ^^^^^^^ the trait `GodotDefault` is not implemented for `MyClass`
    |

After the change:

error[E0277]: Class `MyClass` requires either an `init` constructor, or
              explicit opt-out
--> path/to/file.rs:123:4
    |
123 | struct MyClass {}
    |        ^^^^^^^ needs `init`
    |
= help: the trait `GodotDefault` is not implemented for `MyClass`
= note: To provide a default constructor, use `#[class(init)]` or implement
        an `init` method
= note: To opt out, use `#[class(no_init)]`
= note: see also: https://godot-rust.github.io/book/register/constructors.html

There are now very concrete notes to guide the user, and even a link for further reading.

More to come

The complete list of merged pull request is available on GitHub.

Now that a lot of the tooling aspects around godot-rust publishing are taken care of, we can focus even more on features. If you are interested in the development, take a look at the issue tracker and voice your opinion on GitHub or Discord!