Skip to content

Commit 529654c

Browse files
authored
Merge pull request #744 from godot-rust/bugfix/base-access
Test `Base<T>` destruction and swapping
2 parents dcfd5df + 995e8d0 commit 529654c

File tree

3 files changed

+120
-16
lines changed

3 files changed

+120
-16
lines changed

godot-core/src/obj/base.rs

+26-14
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ use std::mem::ManuallyDrop;
1414
///
1515
/// Behaves similarly to [`Gd`][crate::obj::Gd], but is more constrained. Cannot be constructed by the user.
1616
pub struct Base<T: GodotClass> {
17+
// Like `Gd`, it's theoretically possible that Base is destroyed while there are still other Gd pointers to the underlying object. This is
18+
// safe, may however lead to unintended behavior. The base_test.rs file checks some of these scenarios.
19+
1720
// Internal smart pointer is never dropped. It thus acts like a weak pointer and is needed to break reference cycles between Gd<T>
1821
// and the user instance owned by InstanceStorage.
1922
//
@@ -34,19 +37,37 @@ pub struct Base<T: GodotClass> {
3437
}
3538

3639
impl<T: GodotClass> Base<T> {
40+
/// "Copy constructor": allows to share a `Base<T>` weak pointer.
41+
///
42+
/// The return value is a weak pointer, so it will not keep the instance alive.
43+
///
3744
/// # Safety
38-
/// The returned Base is a weak pointer, so holding it will not keep the object alive. It must not be accessed after the object is destroyed.
45+
/// `base` must be alive at the time of invocation, i.e. user `init()` (which could technically destroy it) must not have run yet.
46+
/// If `base` is destroyed while the returned `Base<T>` is in use, that constitutes a logic error, not a safety issue.
3947
pub(crate) unsafe fn from_base(base: &Base<T>) -> Base<T> {
40-
Base::from_obj(Gd::from_obj_sys_weak(base.as_gd().obj_sys()))
48+
debug_assert!(base.obj.is_instance_valid());
49+
Base::from_obj(Gd::from_obj_sys_weak(base.obj.obj_sys()))
4150
}
4251

52+
/// Create base from existing object (used in script instances).
53+
///
54+
/// The return value is a weak pointer, so it will not keep the instance alive.
55+
///
4356
/// # Safety
44-
/// The returned Base is a weak pointer, so holding it will not keep the object alive. It must not be accessed after the object is destroyed.
57+
/// `gd` must be alive at the time of invocation. If it is destroyed while the returned `Base<T>` is in use, that constitutes a logic
58+
/// error, not a safety issue.
4559
pub(crate) unsafe fn from_gd(gd: &Gd<T>) -> Self {
60+
debug_assert!(gd.is_instance_valid());
4661
Base::from_obj(Gd::from_obj_sys_weak(gd.obj_sys()))
4762
}
4863

49-
// Note: not &mut self, to only borrow one field and not the entire struct
64+
/// Create new base from raw Godot object.
65+
///
66+
/// The return value is a weak pointer, so it will not keep the instance alive.
67+
///
68+
/// # Safety
69+
/// `base_ptr` must point to a valid, live object at the time of invocation. If it is destroyed while the returned `Base<T>` is in use,
70+
/// that constitutes a logic error, not a safety issue.
5071
pub(crate) unsafe fn from_sys(base_ptr: sys::GDExtensionObjectPtr) -> Self {
5172
assert!(!base_ptr.is_null(), "instance base is null pointer");
5273

@@ -76,16 +97,7 @@ impl<T: GodotClass> Base<T> {
7697
(*self.obj).clone()
7798
}
7899

79-
/// Returns a [`Gd`] referencing the same object as this reference.
80-
///
81-
/// Using this method to call methods on the base field of a Rust object is discouraged, instead use the
82-
/// methods from [`WithBaseField`](super::WithBaseField) when possible.
83-
#[doc(hidden)]
84-
pub fn as_gd(&self) -> &Gd<T> {
85-
&self.obj
86-
}
87-
88-
// Currently only used in outbound virtual calls (for scripts).
100+
// Currently only used in outbound virtual calls (for scripts); search for: base_field(self).obj_sys().
89101
#[doc(hidden)]
90102
pub fn obj_sys(&self) -> sys::GDExtensionObjectPtr {
91103
self.obj.obj_sys()

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)