|
10 | 10 | //! Please read the [book chapter](https://godot-rust.github.io/book/godot-api/builtins.html) about builtin types.
|
11 | 11 | //!
|
12 | 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 |
| -//! `Vector2i::new(1, 2)`. This is not strong enough of a reason -- if brevity is of concern, a function `vec2i(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 `DynGd::to_gd()` (which only maintains |
66 |
| -//! the class part, not trait) 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)). |
| 13 | +//! API design behind the builtin types (and some wider parts of the library) is elaborated in the |
| 14 | +//! [extended documentation page](../__docs/index.html#builtin-api-design). |
143 | 15 |
|
144 | 16 | // Re-export macros.
|
145 | 17 | pub use crate::{array, dict, real, reals, varray};
|
|
0 commit comments