|
7 | 7 |
|
8 | 8 | //! Built-in types like `Vector2`, `GString` and `Variant`.
|
9 | 9 | //!
|
10 |
| -//! # Background on the design of vector algebra types |
11 |
| -//! |
12 |
| -//! The basic vector algebra types like `Vector2`, `Matrix4` and `Quaternion` are re-implemented |
13 |
| -//! here, with an API similar to that in the Godot engine itself. There are other approaches, but |
14 |
| -//! they all have their disadvantages: |
15 |
| -//! |
16 |
| -//! - We could invoke API methods from the engine. The implementations could be generated, but it |
17 |
| -//! is slower and prevents inlining. |
18 |
| -//! |
19 |
| -//! - We could re-export types from an existing vector algebra crate, like `glam`. This removes the |
20 |
| -//! duplication, but it would create a strong dependency on a volatile API outside our control. |
21 |
| -//! The `gdnative` crate started out this way, using types from `euclid`, but [found it |
22 |
| -//! impractical](https://github.com/godot-rust/gdnative/issues/594#issue-705061720). Moreover, |
23 |
| -//! the API would not match Godot's own, which would make porting from GDScript (slightly) |
24 |
| -//! harder. |
25 |
| -//! |
26 |
| -//! - We could opaquely wrap types from an existing vector algebra crate. This protects users of |
27 |
| -//! `gdextension` from changes in the wrapped crate. However, direct field access using `.x`, |
28 |
| -//! `.y`, `.z` is no longer possible. Instead of `v.y += a;` you would have to write |
29 |
| -//! `v.set_y(v.get_y() + a);`. (A `union` could be used to add these fields in the public API, |
30 |
| -//! but would make every field access unsafe, which is also not great.) |
31 |
| -//! |
32 |
| -//! - We could re-export types from the [`mint`](https://crates.io/crates/mint) crate, which was |
33 |
| -//! explicitly designed to solve this problem. However, it falls short because [operator |
34 |
| -//! overloading would become impossible](https://github.com/kvark/mint/issues/75). |
| 10 | +//! Please read the [book chapter](https://godot-rust.github.io/book/godot-api/builtins.html) about builtin types. |
| 11 | +//! |
| 12 | +//! # API design |
| 13 | +//! |
| 14 | +//! Our goal is to strive for a middle ground between idiomatic Rust and existing Godot APIs, achieving a decent balance between ergonomics, |
| 15 | +//! correctness and performance. We leverage Rust's type system (such as `Option<T>` or `enum`) where it helps expressivity. |
| 16 | +//! |
| 17 | +//! We have been using a few guiding principles. Those apply to builtins in particular, but some are relevant in other modules, too. |
| 18 | +//! |
| 19 | +//! ## 1. `Copy` for value types |
| 20 | +//! |
| 21 | +//! _Value types_ are types with public fields and no hidden state. This includes all geometric types, colors and RIDs. |
| 22 | +//! |
| 23 | +//! All value types implement the `Copy` trait and thus have no custom `Drop` impl. |
| 24 | +//! |
| 25 | +//! ## 2. By-value (`self`) vs. by-reference (`&self`) receivers |
| 26 | +//! |
| 27 | +//! Most `Copy` builtins use by-value receivers. The exception are matrix-like types (e.g., `Basis`, `Transform2D`, `Transform3D`, `Projection`), |
| 28 | +//! whose methods operate on `&self` instead. This is close to how the underlying `glam` library handles it. |
| 29 | +//! |
| 30 | +//! ## 3. `Default` trait only when the default value is common and useful |
| 31 | +//! |
| 32 | +//! `Default` is deliberately not implemented for every type. Rationale: |
| 33 | +//! - For some types, the default representation (as per Godot) does not constitute a useful value. This goes against Rust's [`Default`] docs, |
| 34 | +//! which explicitly mention "A trait for giving a type a _useful_ default value". For example, `Plane()` in GDScript creates a degenerate |
| 35 | +//! plane which cannot participate in geometric operations. |
| 36 | +//! - Not providing `Default` makes users double-check if the value they want is indeed what they intended. While it seems convenient, not |
| 37 | +//! having implicit default or "null" values is a design choice of Rust, avoiding the Billion Dollar Mistake. In many situations, `Option` or |
| 38 | +//! [`OnReady`][crate::obj::OnReady] is a better alternative. |
| 39 | +//! - For cases where the Godot default is truly desired, we provide an `invalid()` constructor, e.g. `Callable::invalid()` or `Plane::invalid()`. |
| 40 | +//! This makes it explicit that you're constructing a value that first has to be modified before becoming useful. When used in class fields, |
| 41 | +//! `#[init(val = ...)]` can help you initialize such values. |
| 42 | +//! - Outside builtins, we do not implement `Gd::default()` for manually managed types, as this makes it very easy to overlook initialization |
| 43 | +//! (e.g. in `#[derive(Default)]`) and leak memory. A `Gd::new_alloc()` is very explicit. |
| 44 | +//! |
| 45 | +//! ## 4. Prefer explicit conversions over `From` trait |
| 46 | +//! |
| 47 | +//! `From` is quite popular in Rust, but unlike traits such as `Debug`, the convenience of `From` can come at a cost. Like every feature, adding |
| 48 | +//! an `impl From` needs to be justified -- not the other way around: there doesn't need to be a particular reason why it's _not_ added. But |
| 49 | +//! there are in fact some trade-offs to consider: |
| 50 | +//! |
| 51 | +//! 1. `From` next to named conversion methods/constructors adds another way to do things. While it's sometimes good to have choice, multiple |
| 52 | +//! ways to achieve the same has downsides: users wonder if a subtle difference exists, or if all options are in fact identical. |
| 53 | +//! It's unclear which one is the "preferred" option. Recognizing other people's code becomes harder, because there tend to be dialects. |
| 54 | +//! 2. It's often a purely stylistic choice, without functional benefits. Someone may want to write `(1, 2).into()` instead of |
| 55 | +//! `Vector2::new(1, 2)`. This is not strong enough of a reason -- if brevity is of concern, a function `vec2(1, 2)` does the job better. |
| 56 | +//! 3. `From` is less explicit than a named conversion function. If you see `string.to_variant()` or `color.to_hsv()`, you immediately |
| 57 | +//! know the target type. `string.into()` and `color.into()` lose that aspect. Even with `(1, 2).into()`, you'd first have to check whether |
| 58 | +//! `From` is only converting the tuple, or if it _also_ provides an `i32`-to-`f32` cast, thus resulting in `Vector2` instead of `Vector2i`. |
| 59 | +//! This problem doesn't exist with named constructor functions. |
| 60 | +//! 4. The `From` trait doesn't play nicely with type inference. If you write `let v = string.to_variant()`, rustc can infer the type of `v` |
| 61 | +//! based on the right-hand expression alone. With `.into()`, you need follow-up code to determine the type, which may or may not work. |
| 62 | +//! Temporarily commenting out such non-local code breaks the declaration line, too. To make matters worse, turbofish `.into::<Type>()` isn't |
| 63 | +//! possible either. |
| 64 | +//! 5. Rust itself [requires](https://doc.rust-lang.org/std/convert/trait.From.html#when-to-implement-from) that `From` conversions are |
| 65 | +//! infallible, lossless, value-preserving and obvious. This rules out a lot of scenarios such as `Basis::to_quaternion()` (which only maintains |
| 66 | +//! the rotation part, not scale) or `Color::try_to_hsv()` (which is fallible and lossy). |
| 67 | +//! |
| 68 | +//! One main reason to support `From` is to allow generic programming, in particular `impl Into<T>` parameters. This is also the reason |
| 69 | +//! why the string types have historically implemented the trait. But this became less relevant with the advent of |
| 70 | +//! [`AsArg<T>`][crate::meta::AsArg] taking that role, and thus may change in the future. |
| 71 | +//! |
| 72 | +//! ## 5. `Option` for fallible operations |
| 73 | +//! |
| 74 | +//! GDScript often uses degenerate types and custom null states to express that an operation isn't successful. This isn't always consistent: |
| 75 | +//! - [`Rect2::intersection()`] returns an empty rectangle (i.e. you need to check its size). |
| 76 | +//! - [`Plane::intersects_ray()`] returns a `Variant` which is NIL in case of no intersection. While this is a better way to deal with it, |
| 77 | +//! it's not immediately obvious that the result is a point (`Vector2`), and comes with extra marshaling overhead. |
| 78 | +//! |
| 79 | +//! Rust uses `Option` in such cases, making the error state explicit and preventing that the result is accidentally interpreted as valid. |
| 80 | +//! |
| 81 | +//! [`Rect2::intersection()`]: https://docs.godotengine.org/en/stable/classes/class_rect2.html#class-rect2-method-intersection |
| 82 | +//! [`Plane::intersects_ray()`]: https://docs.godotengine.org/en/stable/classes/class_plane.html#class-plane-method-intersects-ray |
| 83 | +//! |
| 84 | +//! ## 6. Public fields and soft invariants |
| 85 | +//! |
| 86 | +//! Some geometric types are subject to "soft invariants". These invariants are not enforced at all times but are essential for certain |
| 87 | +//! operations. For example, bounding boxes must have non-negative volume for operations like intersection or containment checks. Planes |
| 88 | +//! must have a non-zero normal vector. |
| 89 | +//! |
| 90 | +//! We cannot make them hard invariants (no invalid value may ever exist), because that would disallow the convenient public fields, and |
| 91 | +//! it would also mean every value coming over the FFI boundary (e.g. an `#[export]` field set in UI) would constantly need to be validated |
| 92 | +//! and reset to a different "sane" value. |
| 93 | +//! |
| 94 | +//! For **geometric operations**, Godot often doesn't specify the behavior if values are degenerate, which can propagate bugs that then lead |
| 95 | +//! to follow-up problems. godot-rust instead provides best-effort validations _during an operation_, which cause panics if such invalid states |
| 96 | +//! are detected (at least in Debug mode). Consult the docs of a concrete type to see its guarantees. |
| 97 | +//! |
| 98 | +//! ## 7. RIIR for some, but not all builtins |
| 99 | +//! |
| 100 | +//! Builtins use varying degrees of Rust vs. engine code for their implementations. This may change over time and is generally an implementation |
| 101 | +//! detail. |
| 102 | +//! |
| 103 | +//! - 100% Rust, often supported by the `glam` library: |
| 104 | +//! - all vector types (`Vector2`, `Vector2i`, `Vector3`, `Vector3i`, `Vector4`, `Vector4i`) |
| 105 | +//! - all bounding boxes (`Rect2`, `Rect2i`, `Aabb`) |
| 106 | +//! - 2D/3D matrices (`Basis`, `Transform2D`, `Transform3D`) |
| 107 | +//! - `Plane` |
| 108 | +//! - `Rid` (just an integer) |
| 109 | +//! - Partial Rust: `Color`, `Quaternion`, `Projection` |
| 110 | +//! - Only Godot FFI: all others (containers, strings, callables, variant, ...) |
| 111 | +//! |
| 112 | +//! The rationale here is that operations which are absolutely ubiquitous in game development, such as vector/matrix operations, benefit |
| 113 | +//! a lot from being directly implemented in Rust. This avoids FFI calls, which aren't necessarily slow, but remove a lot of optimization |
| 114 | +//! potential for rustc/LLVM. |
| 115 | +//! |
| 116 | +//! Other types, that are used less in bulk and less often in performance-critical paths (e.g. `Projection`), partially fall back to Godot APIs. |
| 117 | +//! Some operations are reasonably complex to implement in Rust, and we're not a math library, nor do we want to depend on one besides `glam`. |
| 118 | +//! An ever-increasing maintenance burden for geometry re-implementations is also detrimental. |
| 119 | +//! |
| 120 | +//! TLDR: it's a trade-off between performance, maintenance effort and correctness -- the current combination of `glam` and Godot seems to be a |
| 121 | +//! relatively well-working sweet spot. |
| 122 | +//! |
| 123 | +//! ## 8. `glam` types are not exposed in public API |
| 124 | +//! |
| 125 | +//! While Godot and `glam` share common operations, there are also lots of differences and Godot specific APIs. |
| 126 | +//! As a result, godot-rust defines its own vector and matrix types, making `glam` an implementation details. |
| 127 | +//! |
| 128 | +//! Alternatives considered: |
| 129 | +//! |
| 130 | +//! 1. Re-export types of an existing vector algebra crate (like `glam`). |
| 131 | +//! The `gdnative` crate started out this way, using types from `euclid`, but [became impractical](https://github.com/godot-rust/gdnative/issues/594#issue-705061720). |
| 132 | +//! Even with extension traits, there would be lots of compromises, where existing and Godot APIs differ slightly. |
| 133 | +//! |
| 134 | +//! Furthermore, it would create a strong dependency on a volatile API outside our control. `glam` had 9 SemVer-breaking versions over the |
| 135 | +//! timespan of two years (2022-2024). While it's often easy to migrate and the changes notably improve the library, this would mean that any |
| 136 | +//! breaking change would also become breaking for godot-rust, requiring a SemVer bump. By abstracting this, we can have our own timeline. |
| 137 | +//! |
| 138 | +//! 2. We could opaquely wrap types, i.e. `Vector2` would contain a private `glam::Vec2`. This would prevent direct field access, which is |
| 139 | +//! _extremely_ inconvenient for vectors. And it would still require us to redefine the front-end of the entire API. |
| 140 | +//! |
| 141 | +//! Eventually, we might add support for [`mint`](https://crates.io/crates/mint) to allow conversions to other linear algebra libraries in the |
| 142 | +//! ecosystem. (Note that `mint` intentionally offers no math operations, see e.g. [mint#75](https://github.com/kvark/mint/issues/75)). |
35 | 143 |
|
36 | 144 | // Re-export macros.
|
37 | 145 | pub use crate::{array, dict, real, reals, varray};
|
|
0 commit comments