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.