|
| 1 | ++++ |
| 2 | +# Copyright (c) godot-rust; Bromeon and contributors. |
| 3 | +# This Source Code Form is subject to the terms of the Mozilla Public |
| 4 | +# License, v. 2.0. If a copy of the MPL was not distributed with this |
| 5 | +# file, You can obtain one at https://mozilla.org/MPL/2.0/. |
| 6 | + |
| 7 | +title = "September 2025 dev update" |
| 8 | +authors = ["Yarwin", "Bromeon"] |
| 9 | + |
| 10 | +[extra] |
| 11 | +summary = "v0.4 and recent developments" |
| 12 | +tags = ["dev-update"] |
| 13 | ++++ |
| 14 | + |
| 15 | +We just released godot-rust **v0.4**! |
| 16 | + |
| 17 | +With it, we'd also like to highlight some of the major features and improvements since our last [dev update in May 2025][may-dev-update], |
| 18 | +both during the 0.3 cycle and the 0.4.0 release. |
| 19 | + |
| 20 | + |
| 21 | +## Properties and exports |
| 22 | + |
| 23 | +The `register` module saw various improvements regarding properties and exports: |
| 24 | + |
| 25 | +### Export groups and subgroups |
| 26 | + |
| 27 | +Thanks to Yarwin, properties can now be grouped to organize them neatly in the Inspector dock, [just like in GDScript][gdscript-export-groups] ([#1214], [#1261]). |
| 28 | + |
| 29 | +`#[export_group]` and `#[export_subgroup]` are the Rust equivalents of GDScript's `@export_group` and `@export_subgroup` annotations. |
| 30 | + |
| 31 | +Something unconventional is that they affect multiple following fields, not just one field. We considered alternatives (repetition or struct splitting), but come at the cost of fast gamedev iteration, and many Godot users are already familiar with this pattern. |
| 32 | + |
| 33 | +![car-export-groups.png][car-export-groups-img] |
| 34 | + |
| 35 | +[car-export-groups-img]: car-export-groups.png |
| 36 | + |
| 37 | + |
| 38 | +<details> |
| 39 | +<summary><i>Expand to see code...</i></summary> |
| 40 | + |
| 41 | +```rust |
| 42 | +#[derive(GodotClass)] |
| 43 | +#[class(init, base=Node)] |
| 44 | +struct MyNode { |
| 45 | + #[export_group(name = "Racer Properties")] |
| 46 | + #[export] |
| 47 | + nickname: GString, |
| 48 | + #[export] |
| 49 | + age: i64, |
| 50 | + |
| 51 | + #[export_group(name = "Car Properties")] |
| 52 | + #[export_subgroup(name = "Car prints", prefix = "car_")] |
| 53 | + #[export] |
| 54 | + car_label: GString, |
| 55 | + #[export] |
| 56 | + car_number: i64, |
| 57 | + |
| 58 | + #[export_subgroup(name = "Wheels/Front", prefix = "front_wheel")] |
| 59 | + #[export] |
| 60 | + front_wheel_strength: i64, |
| 61 | + #[export] |
| 62 | + front_wheel_mobility: i64, |
| 63 | + |
| 64 | + #[export_subgroup(name = "Wheels/Rear", prefix = "rear_wheel_")] |
| 65 | + #[export] |
| 66 | + rear_wheel_strength: i64, |
| 67 | + #[export] |
| 68 | + rear_wheel_mobility: i64, |
| 69 | + |
| 70 | + #[export_subgroup(name = "Wheels", prefix = "wheel_")] |
| 71 | + #[export] |
| 72 | + wheel_material: OnEditor<Gd<PhysicsMaterial>>, |
| 73 | + #[export] |
| 74 | + other_car_properties: GString, |
| 75 | + |
| 76 | + // Use empty group name to break out from the group: |
| 77 | + #[export_group(name = "")] |
| 78 | + #[export] |
| 79 | + ungrouped_field: GString, |
| 80 | +} |
| 81 | +``` |
| 82 | +</details> |
| 83 | + |
| 84 | +### Phantom properties |
| 85 | + |
| 86 | +The [`PhantomVar<T>`][api-phantomvar] field type enables ZST (zero-sized type) properties without backing fields, for dynamic properties that are computed on-the-fly or stored elsewhere. Thanks to ttencate for adding this in [#1261]! |
| 87 | + |
| 88 | +```rust |
| 89 | +#[derive(GodotClass)] |
| 90 | +#[class(init, base=Node)] |
| 91 | +struct MyNode { |
| 92 | + #[var(get = get_computed_value)] |
| 93 | + computed_value: PhantomVar<i32>, // zero bytes |
| 94 | +} |
| 95 | + |
| 96 | +#[godot_api] |
| 97 | +impl INode for MyNode { |
| 98 | + #[func] |
| 99 | + fn get_computed_value(&self) -> i32 { ... } |
| 100 | +} |
| 101 | +``` |
| 102 | + |
| 103 | +### Numeric export limits |
| 104 | + |
| 105 | +For integer exports, a reasonable range is automatically inferred. Additionally, `#[export(range)]` literals are validated against field types at compile time ([#1320]). |
| 106 | + |
| 107 | +```rust |
| 108 | +#[export(range = (0.0, 255.0))] // no longer compiles (float) |
| 109 | +int_property: i8, |
| 110 | + |
| 111 | +#[export(range = (0, 128))] // doesn't compile either (out of range) |
| 112 | +int_property: i8, |
| 113 | + |
| 114 | +#[export] // infers from i8 that range = (-128, 127) |
| 115 | +int_property: i32, |
| 116 | +``` |
| 117 | + |
| 118 | +[#1214]: https://github.com/godot-rust/gdext/pull/1214 |
| 119 | +[#1261]: https://github.com/godot-rust/gdext/pull/1261 |
| 120 | +[#1320]: https://github.com/godot-rust/gdext/pull/1320 |
| 121 | + |
| 122 | +[api-phantomvar]: https://godot-rust.github.io/docs/gdext/master/godot/obj/struct.PhantomVar.html |
| 123 | +[gdscript-export-groups]: https://docs.godotengine.org/en/stable/tutorials/scripting/gdscript/gdscript_exports.html#grouping-exports |
| 124 | + |
| 125 | +## Easier callables |
| 126 | + |
| 127 | +Several parts were improved on the `Callable` front: |
| 128 | + |
| 129 | +### Type-safe deferred calls |
| 130 | + |
| 131 | +[`run_deferred()`][api-gd-rundeferred] and [`run_deferred_gd()`][api-gd-rundeferredgd] act as a type-safe `call_deferred()` alternative, allowing deferred method calls based on closures. This eliminates string-based method names and runtime errors. Thanks to goatfryed for the design and implementation ([#1204], [#1327], [#1332])! |
| 132 | + |
| 133 | +```rust |
| 134 | +// Old way (string-based, error-prone): |
| 135 | +node.call_deferred("set_position", &[pos.to_variant()]); |
| 136 | + |
| 137 | +// New way (type-safe): |
| 138 | +node.run_deferred_gd(|obj| obj.set_position(pos)); |
| 139 | +``` |
| 140 | + |
| 141 | +### Type-safe return types |
| 142 | + |
| 143 | +Modern callable constructors like [`from_fn()`][api-callable-fromfn] support any return type implementing `ToGodot`, eliminating manual `Variant` conversion boilerplate ([#1346]). |
| 144 | + |
| 145 | +```rust |
| 146 | +// in 0.3 (and deprecated in 0.4): |
| 147 | +let callable = Callable::from_local_fn("unit", |args| { |
| 148 | + do_sth(args); |
| 149 | + Ok(Variant::nil()) |
| 150 | +}); |
| 151 | + |
| 152 | +// new in 0.4: |
| 153 | +let callable = Callable::from_fn("unit", |args| { |
| 154 | + do_sth(args); |
| 155 | +}); |
| 156 | +``` |
| 157 | + |
| 158 | +[#1204]: https://github.com/godot-rust/gdext/pull/1204 |
| 159 | +[#1223]: https://github.com/godot-rust/gdext/pull/1223 |
| 160 | +[#1327]: https://github.com/godot-rust/gdext/pull/1327 |
| 161 | +[#1332]: https://github.com/godot-rust/gdext/pull/1332 |
| 162 | +[#1344]: https://github.com/godot-rust/gdext/pull/1344 |
| 163 | +[#1346]: https://github.com/godot-rust/gdext/pull/1346 |
| 164 | + |
| 165 | +[api-gd-rundeferred]: https://godot-rust.github.io/docs/gdext/master/godot/obj/struct.Gd.html#method.run_deferred |
| 166 | +[api-gd-rundeferredgd]: https://godot-rust.github.io/docs/gdext/master/godot/obj/struct.Gd.html#method.run_deferred_gd |
| 167 | +[api-callable-fromfn]: https://godot-rust.github.io/docs/gdext/master/godot/builtin/struct.Callable.html#method.from_fn |
| 168 | +[`Gd::linked_callable()`]: https://godot-rust.github.io/docs/gdext/master/godot/obj/struct.Gd.html#method.linked_callable |
| 169 | + |
| 170 | +## Signal enhancements |
| 171 | + |
| 172 | +Since the introduction of signals in v0.3, several convenience APIs have been added. |
| 173 | + |
| 174 | +Signals now offer disconnection ([#1198]): |
| 175 | +```rust |
| 176 | +let handle = self.signals().my_signal().connect(...); |
| 177 | +// Later that day: |
| 178 | +handle.disconnect(); |
| 179 | +``` |
| 180 | + |
| 181 | +Thanks to Yarwin's work on `linked_callable()`, signals connected to a receiver object are automatically disconnected when the receiver is freed ([#1223]): |
| 182 | + |
| 183 | +```rust |
| 184 | +let obj: Gd<MyClass> = ...; |
| 185 | +let handle = self.signals().my_signal().connect_other(&obj, ...); |
| 186 | + |
| 187 | +obj.free(); // Auto-disconnects the signal. |
| 188 | +``` |
| 189 | + |
| 190 | +User ogapo enabled conversion to untyped signals for better Godot interop, with [`TypedSignal::to_untyped()`][api-typedsignal-tountyped] ([#1288]): |
| 191 | + |
| 192 | +```rust |
| 193 | +let typed = self.signals().my_typed_signal(); |
| 194 | +let untyped: Signal = typed.to_untyped(); |
| 195 | +``` |
| 196 | + |
| 197 | +[#1198]: https://github.com/godot-rust/gdext/pull/1198 |
| 198 | +[#1223]: https://github.com/godot-rust/gdext/pull/1223 |
| 199 | +[#1288]: https://github.com/godot-rust/gdext/pull/1288 |
| 200 | + |
| 201 | +[api-typedsignal-tountyped]: https://godot-rust.github.io/docs/gdext/master/godot/register/struct.TypedSignal.html#method.to_untyped |
| 202 | + |
| 203 | +## Ergonomics and developer experience |
| 204 | + |
| 205 | +In good tradition, godot-rust has shipped a truckload of little tools to make everyday development more enjoyable. |
| 206 | + |
| 207 | +### Class dispatching |
| 208 | + |
| 209 | +No more tedious `try_cast()` cascades for explicit dynamic dispatch. The [`match_class!`][api-matchclass] macro allows dynamic class matching, similar to Rust's `match` keyword ([#1225]). |
| 210 | + |
| 211 | +Thanks to sylbeth's work, the macro supports mutable bindings ([#1242]), optional fallback branches ([#1246]), and discard patterns ([#1252]): |
| 212 | + |
| 213 | +```rust |
| 214 | +let simple_dispatch: i32 = match_class!(event, { |
| 215 | + button @ InputEventMouseButton => 1, |
| 216 | + motion @ InputEventMouseMotion => 2, |
| 217 | + action @ InputEventAction => 3, |
| 218 | + _ => 0, // Fallback. |
| 219 | +}); |
| 220 | +``` |
| 221 | + |
| 222 | +### Generic packed arrays |
| 223 | + |
| 224 | +Generic [`PackedArray<T>`][api-packedarray] abstracts over all specific packed array types, enabling code reuse across different array variants ([#1291]): |
| 225 | + |
| 226 | +```rust |
| 227 | +fn format_packed_array<T>(array: &PackedArray<T>) -> String |
| 228 | +where T: PackedArrayElement { |
| 229 | + // ... |
| 230 | +} |
| 231 | +``` |
| 232 | + |
| 233 | +### Variant slices |
| 234 | + |
| 235 | +The [`vslice!`][api-vslice] macro provides a concise way to create `&[Variant]` slices from heterogeneous values ([#1191]): |
| 236 | + |
| 237 | +```rust |
| 238 | +// Old way: |
| 239 | +let args = &[1.to_variant(), "hello".to_variant(), vector.to_variant()]; |
| 240 | + |
| 241 | +// New way: |
| 242 | +let args = vslice![1, "hello", vector]; |
| 243 | +``` |
| 244 | + |
| 245 | +This comes in handy for dynamic/reflection APIs like `Object::call()`. |
| 246 | + |
| 247 | +### Engine API type safety |
| 248 | + |
| 249 | +Arrays and dictionaries now offer runtime type introspection via [`ElementType`][api-elementtype] ([#1304]). |
| 250 | + |
| 251 | +Lots of engine APIs have been made more type-safe; check out [#1315] to get an idea of the scope. In particular, many "intly-typed" method parameters have been replaced with enums or bitfields, no longer leaving you the guesswork of what values are expected. |
| 252 | + |
| 253 | +```rust |
| 254 | +let s: Variant = obj.get_script(); |
| 255 | +// now: |
| 256 | +let s: Option<Gd<Script>> = obj.get_script(); |
| 257 | + |
| 258 | +obj.connect_ex(...).flags(ConnectFlags::DEFERRED as u32).done(); |
| 259 | +// now: |
| 260 | +obj.connect_flags(..., ConnectFlags::DEFERRED); |
| 261 | +``` |
| 262 | + |
| 263 | +### Negative indexing |
| 264 | + |
| 265 | +The [`SignedRange`][api-signedrange] type provides negative indexing for arrays and strings ([#1300]). |
| 266 | + |
| 267 | +### Enum and bitfield introspection |
| 268 | + |
| 269 | +Programmatic access to all enum and bitfield values enables runtime introspection of Godot's type system ([#1232]). |
| 270 | + |
| 271 | +```rust |
| 272 | +// Access all enum constants. |
| 273 | +let constants = MyEnum::all_constants(); |
| 274 | +let values = MyEnum::values(); // Distinct values only. |
| 275 | + |
| 276 | +for (name, value) in constants { |
| 277 | + add_dropdown_option(name, value); |
| 278 | +} |
| 279 | +``` |
| 280 | + |
| 281 | +[#1191]: https://github.com/godot-rust/gdext/pull/1191 |
| 282 | +[#1225]: https://github.com/godot-rust/gdext/pull/1225 |
| 283 | +[#1232]: https://github.com/godot-rust/gdext/pull/1232 |
| 284 | +[#1242]: https://github.com/godot-rust/gdext/pull/1242 |
| 285 | +[#1246]: https://github.com/godot-rust/gdext/pull/1246 |
| 286 | +[#1252]: https://github.com/godot-rust/gdext/pull/1252 |
| 287 | +[#1291]: https://github.com/godot-rust/gdext/pull/1291 |
| 288 | +[#1300]: https://github.com/godot-rust/gdext/pull/1300 |
| 289 | +[#1304]: https://github.com/godot-rust/gdext/pull/1304 |
| 290 | +[#1315]: https://github.com/godot-rust/gdext/pull/1315 |
| 291 | + |
| 292 | +[api-matchclass]: https://godot-rust.github.io/docs/gdext/master/godot/classes/macro.match_class.html |
| 293 | +[api-vslice]: https://godot-rust.github.io/docs/gdext/master/godot/builtin/macro.vslice.html |
| 294 | +[api-packedarray]: https://godot-rust.github.io/docs/gdext/master/godot/builtin/struct.PackedArray.html |
| 295 | +[api-signedrange]: https://godot-rust.github.io/docs/gdext/master/godot/meta/trait.SignedRange.html |
| 296 | +[api-elementtype]: https://godot-rust.github.io/docs/gdext/master/godot/meta/enum.ElementType.html |
| 297 | + |
| 298 | +## Object lifecycle and initialization |
| 299 | + |
| 300 | +Object initialization and lifecycle management was extended to provide better parity with Godot APIs. |
| 301 | + |
| 302 | +### Base pointer access |
| 303 | + |
| 304 | +So far, it has not been possible to do much with the base object during `init()` method. There was a half-broken `to_gd()` method. This |
| 305 | +has been improved with [`Base::to_init_gd()`][api-base-toinitgd]. What seems easy is actually quite a hack due to the way how Godot treats |
| 306 | +ref-counted objects during initialization ([#1273]). |
| 307 | + |
| 308 | +### Virtual methods on `Gd<Self>` |
| 309 | + |
| 310 | +Thanks to the great pull request by Yarwin, `#[func(gd_self)]` can now be used with various lifecycle methods. Using `Gd<T>` instead of `&T`/`&mut T` gives precise control over when the given instance is bound ([#1282]): |
| 311 | + |
| 312 | +```rust |
| 313 | +#[func(gd_self)] |
| 314 | +fn ready(this: Gd<Self>) { |
| 315 | + this.signals().call_me_back_maybe().emit(&this); |
| 316 | +} |
| 317 | +``` |
| 318 | + |
| 319 | +### Post-initialization notification |
| 320 | + |
| 321 | +The [`POSTINITIALIZE`][api-postinitialize] notification is now emitted after `init()` completes, providing a hook for setup that requires a fully initialized object. Thanks to beicause for this addition in [#1211]! |
| 322 | + |
| 323 | +### Generic singleton access |
| 324 | + |
| 325 | +The [`Singleton`][api-singleton] trait enables generic programming with singletons while maintaining backward compatibility with existing `singleton()` methods ([#1325]). |
| 326 | + |
| 327 | +[#1211]: https://github.com/godot-rust/gdext/pull/1211 |
| 328 | +[#1273]: https://github.com/godot-rust/gdext/pull/1273 |
| 329 | +[#1282]: https://github.com/godot-rust/gdext/pull/1282 |
| 330 | +[#1325]: https://github.com/godot-rust/gdext/pull/1325 |
| 331 | + |
| 332 | +[api-base-toinitgd]: https://godot-rust.github.io/docs/gdext/master/godot/obj/struct.Base.html#method.to_init_gd |
| 333 | +[api-singleton]: https://godot-rust.github.io/docs/gdext/master/godot/obj/trait.Singleton.html |
| 334 | +[api-postinitialize]: https://godot-rust.github.io/docs/gdext/master/godot/classes/notify/enum.NodeNotification.html#variant.POSTINITIALIZE |
| 335 | + |
| 336 | +## Advanced argument passing and type conversion |
| 337 | + |
| 338 | +The argument passing system received a comprehensive overhaul with the new `ToGodot::Pass` design, automatic [`AsArg`][api-asarg] implementations, and unified object argument handling. For detailed migration information, see the [v0.4 migration guide][migration-guide]. |
| 339 | + |
| 340 | +The system now uses explicit `ByValue` or `ByRef` passing modes, eliminates the need for manual `AsArg` implementations, and supports optional object parameters through `AsArg<Option<DynGd>>` ([#1285], [#1308], [#1310], [#1314], [#1323]). |
| 341 | + |
| 342 | +[#1285]: https://github.com/godot-rust/gdext/pull/1285 |
| 343 | +[#1308]: https://github.com/godot-rust/gdext/pull/1308 |
| 344 | +[#1310]: https://github.com/godot-rust/gdext/pull/1310 |
| 345 | +[#1314]: https://github.com/godot-rust/gdext/pull/1314 |
| 346 | +[#1323]: https://github.com/godot-rust/gdext/pull/1323 |
| 347 | + |
| 348 | +[api-asarg]: https://godot-rust.github.io/docs/gdext/master/godot/meta/trait.AsArg.html |
| 349 | + |
| 350 | +## Outlook |
| 351 | + |
| 352 | +For upgrading existing code, consult the [v0.4 migration guide][migration-guide]. For a complete list of changes including bugfixes and internal improvements, see the [changelog in the repository][changelog]. |
| 353 | + |
| 354 | +Version 0.4 is yet another milestone in godot-rust's journey, with major improvements to the developer experience. This wouldn't be possible without the many contributions from the community, whether as code, feedback or [building projects with godot-rust][ecosystem]. |
| 355 | + |
| 356 | +The 0.4 cycle will put a focus on more control and performance. An example of that is beicause's work in [#1278] to give the user to trade off performance for safety across different runtime profiles. Other performance PRs are already open, too! |
| 357 | + |
| 358 | +[#1278]: https://github.com/godot-rust/gdext/pull/1278 |
| 359 | +[may-dev-update]: ../may-2025-update |
| 360 | +[migration-guide]: https://godot-rust.github.io/book/migrate/v0.4.html |
| 361 | +[changelog]: https://github.com/godot-rust/gdext/blob/master/Changelog.md |
| 362 | +[ecosystem]: https://godot-rust.github.io/book/ecosystem/ |
0 commit comments