Skip to content

Commit 995e8d0

Browse files
committed
Test base smuggling and swapping
1 parent b5b4df5 commit 995e8d0

File tree

2 files changed

+94
-2
lines changed

2 files changed

+94
-2
lines changed

godot-core/src/obj/gd.rs

+3-1
Original file line numberDiff line numberDiff line change
@@ -241,9 +241,11 @@ impl<T: GodotClass> Gd<T> {
241241
///
242242
/// This method is safe and never panics.
243243
pub fn instance_id_unchecked(&self) -> InstanceId {
244+
let instance_id = self.raw.instance_id_unchecked();
245+
244246
// SAFETY: a `Gd` can only be created from a non-null `RawGd`, meaning `raw.instance_id_unchecked()` will
245247
// always return `Some`.
246-
unsafe { self.raw.instance_id_unchecked().unwrap_unchecked() }
248+
unsafe { instance_id.unwrap_unchecked() }
247249
}
248250

249251
/// Checks if this smart pointer points to a live object (read description!).

itest/rust/src/object_tests/base_test.rs

+91-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
66
*/
77

8-
use crate::framework::itest;
8+
use crate::framework::{expect_panic, itest};
99
use godot::prelude::*;
1010

1111
#[itest(skip)]
@@ -105,6 +105,86 @@ fn base_gd_self() {
105105
obj.free();
106106
}
107107

108+
// Hardening against https://github.com/godot-rust/gdext/issues/711.
109+
#[itest]
110+
fn base_smuggling() {
111+
let (mut obj, extracted_base) = create_object_with_extracted_base();
112+
113+
// This works because Gd<T> additionally stores the instance ID (through cached_rtti).
114+
assert_eq!(extracted_base.to_gd().instance_id(), obj.instance_id());
115+
116+
// This _also_ works because Gd<T> has the direct object pointer to the Godot object.
117+
obj.set_position(Vector2::new(1.0, 2.0));
118+
assert_eq!(
119+
extracted_base.to_gd().get_position(),
120+
Vector2::new(1.0, 2.0)
121+
);
122+
123+
// Destroy base externally.
124+
extracted_base.to_gd().free();
125+
126+
// Access to object should now fail.
127+
expect_panic("object with dead base: calling base methods", || {
128+
obj.get_position();
129+
});
130+
expect_panic("object with dead base: bind()", || {
131+
obj.bind();
132+
});
133+
expect_panic("object with dead base: instance_id()", || {
134+
obj.instance_id();
135+
});
136+
expect_panic("object with dead base: clone()", || {
137+
let _ = obj.clone();
138+
});
139+
expect_panic("object with dead base: upcast()", || {
140+
obj.upcast::<Object>();
141+
});
142+
143+
// Now vice versa: destroy object, access base.
144+
let (obj, extracted_base) = create_object_with_extracted_base();
145+
obj.free();
146+
147+
expect_panic("accessing extracted base of dead object", || {
148+
extracted_base.to_gd().get_position();
149+
});
150+
}
151+
152+
// While base swapping isn't an encouraged workflow, it can also be regarded as a quicker way to swap all individual properties of two base
153+
// objects -- which is also allowed. It's also similar to slicing in C++. So this is a Ship-of-Theseus problem, and we don't install ergonomic
154+
// obstacles to prevent it. Here, we test that results are expected and safe.
155+
#[itest]
156+
fn base_swapping() {
157+
let (one, mut one_ext_base) = create_object_with_extracted_base();
158+
let one_id = one.instance_id();
159+
160+
let mut two = Based::new_alloc();
161+
let two_id = two.instance_id();
162+
163+
std::mem::swap(&mut one_ext_base, &mut two.bind_mut().base);
164+
165+
// Gd<T> itself isn't affected (it stores the ID separately).
166+
assert_eq!(one_id, one.instance_id());
167+
assert_eq!(two_id, two.instance_id());
168+
169+
// However, the base now has the other object's ID. Gd<T> and T.base having distinct IDs is a bit unintuitive and could lead to follow-up
170+
// logic errors. One option to prevent this would be to add a base integrity check on the entire Gd<T> API (it can't be done from the
171+
// Base<T> side, since that only has direct access to the object pointer, while Gd<T> has access to the object pointer _and_ the base field).
172+
// Not sure if this is worth the effort + complexity though, given that it almost requires malice to get into such a situation.
173+
assert_eq!(one.instance_id(), two.bind().base().instance_id());
174+
assert_eq!(two.instance_id(), one_ext_base.to_gd().instance_id());
175+
176+
one.free();
177+
two.free();
178+
}
179+
180+
fn create_object_with_extracted_base() -> (Gd<Baseless>, Base<Node2D>) {
181+
let mut extracted_base = None;
182+
let obj = Baseless::smuggle_out(&mut extracted_base);
183+
let extracted_base = extracted_base.expect("smuggling didn't work");
184+
185+
(obj, extracted_base)
186+
}
187+
108188
// ----------------------------------------------------------------------------------------------------------------------------------------------
109189

110190
use renamed_bases::Based;
@@ -139,3 +219,13 @@ impl Based {
139219
struct Baseless {
140220
// No need for fields, we just test if we can access this as Gd<Node2D>.
141221
}
222+
223+
impl Baseless {
224+
/// Steals the `Base<T>` from a newly constructed object and stores it in the output parameter.
225+
fn smuggle_out(other_base: &mut Option<Base<Node2D>>) -> Gd<Self> {
226+
Gd::from_init_fn(|base| {
227+
*other_base = Some(base);
228+
Self {}
229+
})
230+
}
231+
}

0 commit comments

Comments
 (0)