Skip to content

Commit de0841f

Browse files
authored
Merge pull request #947 from godot-rust/qol/objects-by-ref
Require `AsObjectArg` pass-by-ref, consistent with `AsArg`
2 parents 8beef9d + 1110e37 commit de0841f

15 files changed

+120
-50
lines changed

godot-core/src/meta/traits.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,7 @@ pub trait ArrayElement: GodotType + ToGodot + FromGodot + sealed::Sealed + meta:
175175
builtin_type_string::<Self>()
176176
}
177177

178+
#[doc(hidden)]
178179
fn debug_validate_elements(_array: &builtin::Array<Self>) -> Result<(), ConvertError> {
179180
// No-op for most element types.
180181
Ok(())

godot-core/src/obj/object_arg.rs

Lines changed: 55 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -15,19 +15,37 @@ use std::ptr;
1515

1616
/// Objects that can be passed as arguments to Godot engine functions.
1717
///
18-
/// This trait is implemented for the following types:
19-
/// - [`Gd<T>`] and `&Gd<T>`, to pass objects. Subclasses of `T` are explicitly supported.
20-
/// - [`Option<Gd<T>>`] and `Option<&Gd<T>>`, to pass optional objects. `None` is mapped to a null argument.
18+
/// This trait is implemented for **shared references** in multiple ways:
19+
/// - [`&Gd<T>`][crate::obj::Gd] to pass objects. Subclasses of `T` are explicitly supported.
20+
/// - [`Option<&Gd<T>>`][Option], to pass optional objects. `None` is mapped to a null argument.
2121
/// - [`Gd::null_arg()`], to pass `null` arguments without using `Option`.
2222
///
2323
/// # Nullability
2424
/// <div class="warning">
2525
/// The GDExtension API does not inform about nullability of its function parameters. It is up to you to verify that the arguments you pass
2626
/// are only null when this is allowed. Doing this wrong should be safe, but can lead to the function call failing.
2727
/// </div>
28-
28+
///
29+
/// # Different argument types
30+
/// Currently, the trait requires pass-by-ref, which helps detect accidental cloning when interfacing with Godot APIs. Plus, it is more
31+
/// consistent with the [`AsArg`][crate::meta::AsArg] trait (for strings, but also `AsArg<Gd<T>>` as used in
32+
/// [`Array::push()`][crate::builtin::Array::push] and similar methods).
33+
///
34+
/// The following table lists the possible argument types and how you can pass them. `Gd` is short for `Gd<T>`.
35+
///
36+
/// | Type | Closest accepted type | How to transform |
37+
/// |-------------------|-----------------------|------------------|
38+
/// | `Gd` | `&Gd` | `&arg` |
39+
/// | `&Gd` | `&Gd` | `arg` |
40+
/// | `&mut Gd` | `&Gd` | `&*arg` |
41+
/// | `Option<Gd>` | `Option<&Gd>` | `arg.as_ref()` |
42+
/// | `Option<&Gd>` | `Option<&Gd>` | `arg` |
43+
/// | `Option<&mut Gd>` | `Option<&Gd>` | `arg.as_deref()` |
44+
/// | (null literal) | | `Gd::null_arg()` |
2945
#[diagnostic::on_unimplemented(
30-
message = "The provided argument of type `{Self}` cannot be converted to a `Gd<{T}>` parameter"
46+
message = "Argument of type `{Self}` cannot be passed to an `impl AsObjectArg<{T}>` parameter",
47+
note = "If you pass by value, consider borrowing instead.",
48+
note = "See also `AsObjectArg` docs: https://godot-rust.github.io/docs/gdext/master/godot/meta/trait.AsObjectArg.html"
3149
)]
3250
pub trait AsObjectArg<T>
3351
where
@@ -41,6 +59,12 @@ where
4159
fn consume_arg(self) -> ObjectCow<T>;
4260
}
4361

62+
/*
63+
Currently not implemented for values, to be consistent with AsArg for by-ref builtins. The idea is that this can discover patterns like
64+
api.method(refc.clone()), and encourage better performance with api.method(&refc). However, we need to see if there's a notable ergonomic
65+
impact, and consider that for nodes, Gd<T> copies are relatively cheap (no ref-counting). There is also some value in prematurely ending
66+
the lifetime of a Gd<T> by moving out, so it's not accidentally used later.
67+
4468
impl<T, U> AsObjectArg<T> for Gd<U>
4569
where
4670
T: GodotClass + Bounds<Declarer = bounds::DeclEngine>,
@@ -54,6 +78,7 @@ where
5478
ObjectCow::Owned(self.upcast())
5579
}
5680
}
81+
*/
5782

5883
impl<T, U> AsObjectArg<T> for &Gd<U>
5984
where
@@ -71,30 +96,44 @@ where
7196
}
7297
}
7398

74-
impl<T, U> AsObjectArg<T> for &mut Gd<U>
99+
impl<T, U> AsObjectArg<T> for Option<U>
75100
where
76101
T: GodotClass + Bounds<Declarer = bounds::DeclEngine>,
77-
U: Inherits<T>,
102+
U: AsObjectArg<T>,
78103
{
79-
// Delegate to &Gd impl.
80-
81104
fn as_object_arg(&self) -> ObjectArg<T> {
82-
<&Gd<U>>::as_object_arg(&&**self)
105+
self.as_ref()
106+
.map_or_else(ObjectArg::null, AsObjectArg::as_object_arg)
83107
}
84108

85109
fn consume_arg(self) -> ObjectCow<T> {
86-
<&Gd<U>>::consume_arg(&*self)
110+
match self {
111+
Some(obj) => obj.consume_arg(),
112+
None => Gd::null_arg().consume_arg(),
113+
}
87114
}
88115
}
89116

90-
impl<T, U> AsObjectArg<T> for Option<U>
117+
/*
118+
It's relatively common that Godot APIs return `Option<Gd<T>>` or pass this type in virtual functions. To avoid excessive `as_ref()` calls, we
119+
**could** directly support `&Option<Gd>` in addition to `Option<&Gd>`. However, this is currently not done as it hides nullability,
120+
especially in situations where a return type is directly propagated:
121+
api(create_obj().as_ref())
122+
api(&create_obj())
123+
While the first is slightly longer, it looks different from a function create_obj() that returns Gd<T> and thus can never be null.
124+
In some scenarios, it's better to immediately ensure non-null (e.g. through `unwrap()`) instead of propagating nulls to the engine.
125+
It's also quite idiomatic to use as_ref() for inner-option transforms in Rust.
126+
127+
impl<T, U> AsObjectArg<T> for &Option<U>
91128
where
92129
T: GodotClass + Bounds<Declarer = bounds::DeclEngine>,
93-
U: AsObjectArg<T>,
130+
for<'a> &'a U: AsObjectArg<T>,
94131
{
95132
fn as_object_arg(&self) -> ObjectArg<T> {
96-
self.as_ref()
97-
.map_or_else(ObjectArg::null, AsObjectArg::as_object_arg)
133+
match self {
134+
Some(obj) => obj.as_object_arg(),
135+
None => ObjectArg::null(),
136+
}
98137
}
99138
100139
fn consume_arg(self) -> ObjectCow<T> {
@@ -104,6 +143,7 @@ where
104143
}
105144
}
106145
}
146+
*/
107147

108148
impl<T> AsObjectArg<T> for ObjectNullArg<T>
109149
where

godot-core/src/obj/script.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -386,7 +386,7 @@ impl<'a, T: ScriptInstance> SiMut<'a, T> {
386386
/// let name = this.base().get_name();
387387
/// godot_print!("name is {name}");
388388
/// // However, we cannot call methods that require `&mut Base`, such as:
389-
/// // this.base().add_child(node);
389+
/// // this.base().add_child(&node);
390390
/// Ok(Variant::nil())
391391
/// }
392392
/// # fn class_name(&self) -> GString { todo!() }

godot-core/src/obj/traits.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -304,7 +304,7 @@ pub trait WithBaseField: GodotClass + Bounds<Declarer = bounds::DeclUser> {
304304
/// fn process(&mut self, _delta: f64) {
305305
/// let node = Node::new_alloc();
306306
/// // fails because `add_child` requires a mutable reference.
307-
/// self.base().add_child(node);
307+
/// self.base().add_child(&node);
308308
/// }
309309
/// }
310310
///
@@ -344,7 +344,7 @@ pub trait WithBaseField: GodotClass + Bounds<Declarer = bounds::DeclUser> {
344344
/// impl INode for MyClass {
345345
/// fn process(&mut self, _delta: f64) {
346346
/// let node = Node::new_alloc();
347-
/// self.base_mut().add_child(node);
347+
/// self.base_mut().add_child(&node);
348348
/// }
349349
/// }
350350
///

godot-core/src/registry/property.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ pub trait Export: Var {
6060
///
6161
/// Only overridden for `Gd<T>`, to detect erroneous exports of `Node` inside a `Resource` class.
6262
#[allow(clippy::wrong_self_convention)]
63+
#[doc(hidden)]
6364
fn as_node_class() -> Option<ClassName> {
6465
None
6566
}

godot-core/src/tools/save_load.rs

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -78,17 +78,18 @@ where
7878
/// See [`try_save`] for more information.
7979
///
8080
/// # Panics
81-
/// If the resouce cannot be saved.
81+
/// If the resource cannot be saved.
8282
///
8383
/// # Example
8484
/// ```no_run
8585
/// use godot::prelude::*;
8686
///
87-
/// save(Resource::new_gd(), "res://BaseResource.tres")
87+
/// let obj = Resource::new_gd();
88+
/// save(&obj, "res://BaseResource.tres")
8889
/// ```
8990
/// use godot::
9091
#[inline]
91-
pub fn save<T>(obj: Gd<T>, path: impl AsArg<GString>)
92+
pub fn save<T>(obj: &Gd<T>, path: impl AsArg<GString>)
9293
where
9394
T: Inherits<Resource>,
9495
{
@@ -120,12 +121,12 @@ where
120121
/// };
121122
///
122123
/// let save_state = SavedGame::new_gd();
123-
/// let res = try_save(save_state, "user://save.tres");
124+
/// let res = try_save(&save_state, "user://save.tres");
124125
///
125126
/// assert!(res.is_ok());
126127
/// ```
127128
#[inline]
128-
pub fn try_save<T>(obj: Gd<T>, path: impl AsArg<GString>) -> Result<(), IoError>
129+
pub fn try_save<T>(obj: &Gd<T>, path: impl AsArg<GString>) -> Result<(), IoError>
129130
where
130131
T: Inherits<Resource>,
131132
{
@@ -163,7 +164,7 @@ where
163164
}
164165
}
165166

166-
fn save_impl<T>(obj: Gd<T>, path: &GString) -> Result<(), IoError>
167+
fn save_impl<T>(obj: &Gd<T>, path: &GString) -> Result<(), IoError>
167168
where
168169
T: Inherits<Resource>,
169170
{

itest/rust/src/engine_tests/native_audio_structures_test.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ fn native_audio_structure_out_parameter() {
113113
.cast::<SceneTree>();
114114

115115
tree.get_root().unwrap().add_child(&player);
116-
player.set_stream(generator);
116+
player.set_stream(&generator);
117117

118118
// Start playback so we can push audio frames through the audio pipeline.
119119
player.play();

itest/rust/src/engine_tests/node_test.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,11 @@ fn node_get_node() {
2222

2323
let mut parent = Node3D::new_alloc();
2424
parent.set_name("parent");
25-
parent.add_child(child);
25+
parent.add_child(&child);
2626

2727
let mut grandparent = Node::new_alloc();
2828
grandparent.set_name("grandparent");
29-
grandparent.add_child(parent);
29+
grandparent.add_child(&parent);
3030

3131
// Directly on Gd<T>
3232
let found = grandparent.get_node_as::<Node3D>("parent/child");
@@ -74,7 +74,7 @@ fn node_scene_tree() {
7474
assert_eq!(err, global::Error::OK);
7575

7676
let mut tree = SceneTree::new_alloc();
77-
let err = tree.change_scene_to_packed(scene);
77+
let err = tree.change_scene_to_packed(&scene);
7878
assert_eq!(err, global::Error::OK);
7979

8080
// Note: parent + child are not owned by PackedScene, thus need to be freed

itest/rust/src/engine_tests/save_load_test.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,13 +34,13 @@ fn save_test() {
3434

3535
let resource = SavedGame::new_gd();
3636

37-
let res = try_save(resource.clone(), FAULTY_PATH);
37+
let res = try_save(&resource, FAULTY_PATH);
3838
assert!(res.is_err());
3939

40-
let res = try_save(resource.clone(), &res_path);
40+
let res = try_save(&resource, &res_path);
4141
assert!(res.is_ok());
4242

43-
save(resource.clone(), &res_path);
43+
save(&resource, &res_path);
4444

4545
remove_test_file(RESOURCE_NAME);
4646
}
@@ -53,7 +53,7 @@ fn load_test() {
5353
let mut resource = SavedGame::new_gd();
5454
resource.bind_mut().set_level(level);
5555

56-
save(resource.clone(), &res_path);
56+
save(&resource, &res_path);
5757

5858
let res = try_load::<SavedGame>(FAULTY_PATH);
5959
assert!(res.is_err());

itest/rust/src/object_tests/object_arg_test.rs

Lines changed: 36 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ use godot::obj::{Gd, NewAlloc, NewGd};
1313
use crate::framework::itest;
1414
use crate::object_tests::object_test::{user_refc_instance, RefcPayload};
1515

16+
/*
1617
#[itest]
1718
fn object_arg_owned() {
1819
with_objects(|manual, refc| {
@@ -22,6 +23,7 @@ fn object_arg_owned() {
2223
(a, b)
2324
});
2425
}
26+
*/
2527

2628
#[itest]
2729
fn object_arg_borrowed() {
@@ -37,12 +39,17 @@ fn object_arg_borrowed() {
3739
fn object_arg_borrowed_mut() {
3840
with_objects(|mut manual, mut refc| {
3941
let db = ClassDb::singleton();
40-
let a = db.class_set_property(&mut manual, "name", &Variant::from("hello"));
41-
let b = db.class_set_property(&mut refc, "value", &Variant::from(-123));
42+
43+
let manual_ref = &mut manual;
44+
let refc_ref = &mut refc;
45+
46+
let a = db.class_set_property(&*manual_ref, "name", &Variant::from("hello"));
47+
let b = db.class_set_property(&*refc_ref, "value", &Variant::from(-123));
4248
(a, b)
4349
});
4450
}
4551

52+
/*
4653
#[itest]
4754
fn object_arg_option_owned() {
4855
with_objects(|manual, refc| {
@@ -52,6 +59,7 @@ fn object_arg_option_owned() {
5259
(a, b)
5360
});
5461
}
62+
*/
5563

5664
#[itest]
5765
fn object_arg_option_borrowed() {
@@ -63,12 +71,30 @@ fn object_arg_option_borrowed() {
6371
});
6472
}
6573

74+
/*
75+
#[itest]
76+
fn object_arg_option_borrowed_outer() {
77+
with_objects(|manual, refc| {
78+
let db = ClassDb::singleton();
79+
let a = db.class_set_property(&Some(manual), "name", &Variant::from("hello"));
80+
let b = db.class_set_property(&Some(refc), "value", &Variant::from(-123));
81+
(a, b)
82+
});
83+
}
84+
*/
85+
6686
#[itest]
6787
fn object_arg_option_borrowed_mut() {
88+
// If you have an Option<&mut Gd<T>>, you can use as_deref() to get Option<&Gd<T>>.
89+
6890
with_objects(|mut manual, mut refc| {
6991
let db = ClassDb::singleton();
70-
let a = db.class_set_property(Some(&mut manual), "name", &Variant::from("hello"));
71-
let b = db.class_set_property(Some(&mut refc), "value", &Variant::from(-123));
92+
93+
let manual_opt: Option<&mut Gd<Node>> = Some(&mut manual);
94+
let refc_opt: Option<&mut Gd<RefcPayload>> = Some(&mut refc);
95+
96+
let a = db.class_set_property(manual_opt.as_deref(), "name", &Variant::from("hello"));
97+
let b = db.class_set_property(refc_opt.as_deref(), "value", &Variant::from(-123));
7298
(a, b)
7399
});
74100
}
@@ -80,10 +106,10 @@ fn object_arg_option_none() {
80106

81107
// Will emit errors but should not crash.
82108
let db = ClassDb::singleton();
83-
let error = db.class_set_property(manual, "name", &Variant::from("hello"));
109+
let error = db.class_set_property(manual.as_ref(), "name", &Variant::from("hello"));
84110
assert_eq!(error, global::Error::ERR_UNAVAILABLE);
85111

86-
let error = db.class_set_property(refc, "value", &Variant::from(-123));
112+
let error = db.class_set_property(refc.as_ref(), "value", &Variant::from(-123));
87113
assert_eq!(error, global::Error::ERR_UNAVAILABLE);
88114
}
89115

@@ -106,14 +132,14 @@ fn object_arg_owned_default_params() {
106132
let b = ResourceFormatLoader::new_gd();
107133

108134
// Use direct and explicit _ex() call syntax.
109-
ResourceLoader::singleton().add_resource_format_loader(a.clone()); // by value
135+
ResourceLoader::singleton().add_resource_format_loader(&a);
110136
ResourceLoader::singleton()
111-
.add_resource_format_loader_ex(b.clone()) // by value
137+
.add_resource_format_loader_ex(&b)
112138
.done();
113139

114140
// Clean up (no leaks).
115-
ResourceLoader::singleton().remove_resource_format_loader(a);
116-
ResourceLoader::singleton().remove_resource_format_loader(b);
141+
ResourceLoader::singleton().remove_resource_format_loader(&a);
142+
ResourceLoader::singleton().remove_resource_format_loader(&b);
117143
}
118144

119145
// ----------------------------------------------------------------------------------------------------------------------------------------------

itest/rust/src/object_tests/object_swap_test.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ fn object_subtype_swap_argument_passing(ctx: &TestContext) {
119119

120120
let mut tree = ctx.scene_tree.clone();
121121
expect_panic("pass badly typed Gd<T> to Godot engine API", || {
122-
tree.add_child(node);
122+
tree.add_child(&node);
123123
});
124124

125125
swapped_free!(obj, node2);

0 commit comments

Comments
 (0)