Skip to content

Commit 71cf7e5

Browse files
lilizoeyBromeon
authored andcommitted
Fix UB in virtual method calls that take objects
Fix incorrect incrementing of refcount when calling in to godot Fix refcount not being incremented when we receive a refcounted object in virtual methods
1 parent 1c19ad0 commit 71cf7e5

File tree

12 files changed

+282
-35
lines changed

12 files changed

+282
-35
lines changed

godot-core/src/builtin/quaternion.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,7 @@ impl Quaternion {
181181
}
182182

183183
// pub fn spherical_cubic_interpolate(self, b: Self, pre_a: Self, post_b: Self, weight: real) -> Self {}
184-
// TODO: Implement godot's function in rust
184+
// TODO: Implement godot's function in Rust
185185
/*
186186
pub fn spherical_cubic_interpolate_in_time(
187187
self,

godot-core/src/builtin/string/godot_string.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,7 @@ impl fmt::Debug for GodotString {
157157
}
158158

159159
// ----------------------------------------------------------------------------------------------------------------------------------------------
160-
// Conversion from/into rust string-types
160+
// Conversion from/into Rust string-types
161161

162162
impl<S> From<S> for GodotString
163163
where

godot-core/src/obj/as_arg.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,11 @@ pub trait AsArg: Sealed {
2222
impl<T: GodotClass> Sealed for Gd<T> {}
2323
impl<T: GodotClass> AsArg for Gd<T> {
2424
fn as_arg_ptr(&self) -> sys::GDExtensionConstTypePtr {
25-
// Pass argument to engine: increment refcount
26-
<T::Mem as crate::obj::mem::Memory>::maybe_inc_ref(self);
25+
// We're passing a reference to the object to the callee. If the reference count needs to be
26+
// incremented then the callee will do so. We do not need to prematurely do so.
27+
//
28+
// In Rust terms, if `T` is refcounted then we are effectively passing a `&Arc<T>`, and the callee
29+
// would need to call `.clone()` if desired.
2730
self.sys_const()
2831
}
2932
}

godot-core/src/obj/gd.rs

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -552,13 +552,20 @@ where
552552
// https://github.com/godotengine/godot-cpp/issues/954
553553

554554
unsafe fn from_arg_ptr(ptr: sys::GDExtensionTypePtr, call_type: PtrcallType) -> Self {
555-
if T::Mem::pass_as_ref(call_type) {
556-
let obj_ptr = interface_fn!(ref_get_object)(ptr as sys::GDExtensionRefPtr);
557-
// ref_get_object increments the ref_count for us
558-
Self::from_obj_sys_weak(obj_ptr)
555+
let obj_ptr = if T::Mem::pass_as_ref(call_type) {
556+
// ptr is `Ref<T>*`
557+
// See the docs for `PtrcallType::Virtual` for more info on `Ref<T>`.
558+
interface_fn!(ref_get_object)(ptr as sys::GDExtensionRefPtr)
559+
} else if matches!(call_type, PtrcallType::Virtual) {
560+
// ptr is `T**`
561+
*(ptr as *mut sys::GDExtensionObjectPtr)
559562
} else {
560-
Self::from_obj_sys(ptr as sys::GDExtensionObjectPtr)
561-
}
563+
// ptr is `T*`
564+
ptr as sys::GDExtensionObjectPtr
565+
};
566+
567+
// obj_ptr is `T*`
568+
Self::from_obj_sys(obj_ptr)
562569
}
563570

564571
unsafe fn move_return_ptr(self, ptr: sys::GDExtensionTypePtr, call_type: PtrcallType) {

godot-ffi/src/godot_ffi.rs

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ pub unsafe trait GodotFfi {
5757
/// Return Godot opaque pointer, for an immutable operation.
5858
///
5959
/// Note that this is a `*mut` pointer despite taking `&self` by shared-ref.
60-
/// This is because most of Godot's rust API is not const-correct. This can still
60+
/// This is because most of Godot's Rust API is not const-correct. This can still
6161
/// enhance user code (calling `sys_mut` ensures no aliasing at the time of the call).
6262
fn sys(&self) -> sys::GDExtensionTypePtr;
6363

@@ -155,11 +155,15 @@ pub enum PtrcallType {
155155

156156
/// Virtual pointer call.
157157
///
158-
/// A virtual call behaves like [`PtrcallType::Standard`], except for `RefCounted` objects.
159-
/// `RefCounted` objects are instead passed in and returned as `Ref<T>` objects in Godot.
158+
/// A virtual call behaves like [`PtrcallType::Standard`], except for Objects.
160159
///
161-
/// To properly get a value from an argument in a pointer call, you must use `ref_get_object`. And to
162-
/// return a value you must use `ref_set_object`.
160+
/// Objects that do not inherit from `RefCounted` are passed in as `Object**`
161+
/// (`*mut GDExtensionObjectPtr` in GDExtension terms), and objects that inherit from
162+
/// `RefCounted` are passed in as `Ref<T>*` (`GDExtensionRefPtr` in GDExtension
163+
/// terms) and returned as `Ref<T>` objects in Godot.
164+
///
165+
/// To get a `GDExtensionObjectPtr` from a `GDExtensionRefPtr`, you must use `ref_get_object`, and to
166+
/// set a `GDExtensionRefPtr` to some object, you must use `ref_set_object`.
163167
///
164168
/// See also https://github.com/godotengine/godot-cpp/issues/954.
165169
Virtual,

itest/godot/.godot/global_script_class_cache.cfg

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
11
list=Array[Dictionary]([{
2+
"base": &"Node",
3+
"class": &"GDScriptTestRunner",
4+
"icon": "",
5+
"language": &"GDScript",
6+
"path": "res://TestRunner.gd"
7+
}, {
28
"base": &"RefCounted",
39
"class": &"TestStats",
410
"icon": "",
@@ -10,4 +16,10 @@ list=Array[Dictionary]([{
1016
"icon": "",
1117
"language": &"GDScript",
1218
"path": "res://TestSuite.gd"
19+
}, {
20+
"base": &"TestSuite",
21+
"class": &"TestSuiteSpecial",
22+
"icon": "",
23+
"language": &"GDScript",
24+
"path": "res://TestSuiteSpecial.gd"
1325
}])

itest/godot/SpecialTests.gd

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
# This Source Code Form is subject to the terms of the Mozilla Public
2+
# License, v. 2.0. If a copy of the MPL was not distributed with this
3+
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
4+
5+
extends TestSuiteSpecial
6+
7+
# Tests in here require specific setup/configuration that is not easily achievable through the standard
8+
# integration testing API.
9+
#
10+
# Using the standard API if possible is highly preferred.
11+
12+
# Test that we can call `_input_event` on a class defined in rust, as a virtual method.
13+
#
14+
# This tests #267, which was caused by us incorrectly handing Objects when passed as arguments to virtual
15+
# methods. `_input_event` is the easiest such method to test. However it can only be triggered by letting a
16+
# full physics frame pass after calling `push_unhandled_input`. Thus we cannot use the standard API for
17+
# testing this at the moment, since we dont have any way to let frames pass in between the start and end of
18+
# an integration test.
19+
func test_collision_object_2d_input_event():
20+
var root: Node = Engine.get_main_loop().root
21+
22+
var window := Window.new()
23+
window.physics_object_picking = true
24+
root.add_child(window)
25+
26+
var collision_object := CollisionObject2DTest.new()
27+
collision_object.input_pickable = true
28+
29+
var collision_shape := CollisionShape2D.new()
30+
collision_shape.shape = RectangleShape2D.new()
31+
collision_object.add_child(collision_shape)
32+
33+
window.add_child(collision_object)
34+
35+
assert_that(not collision_object.input_event_called())
36+
assert_eq(collision_object.get_viewport(), null)
37+
38+
var event := InputEventMouseMotion.new()
39+
event.global_position = Vector2.ZERO
40+
window.push_unhandled_input(event)
41+
42+
# Ensure we run a full physics frame
43+
await root.get_tree().physics_frame
44+
45+
assert_that(collision_object.input_event_called())
46+
assert_eq(collision_object.get_viewport(), window)
47+
48+
window.queue_free()
49+

itest/godot/TestRunner.gd

Lines changed: 38 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,12 @@
33
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
44

55
extends Node
6+
class_name GDScriptTestRunner
67

78
func _ready():
9+
# Ensure physics is initialized, for tests that require it.
10+
await get_tree().physics_frame
11+
812
var allow_focus := true
913
var unrecognized_args: Array = []
1014
for arg in OS.get_cmdline_user_args():
@@ -32,7 +36,17 @@ func _ready():
3236
for method in suite.get_method_list():
3337
var method_name: String = method.name
3438
if method_name.begins_with("test_"):
35-
gdscript_tests.push_back(GDScriptTestCase.new(suite, method_name))
39+
gdscript_tests.push_back(GDScriptExecutableTestCase.new(suite, method_name))
40+
41+
var special_case_test_suites: Array = [
42+
preload("res://SpecialTests.gd").new(),
43+
]
44+
45+
for suite in special_case_test_suites:
46+
for method in suite.get_method_list():
47+
var method_name: String = method.name
48+
if method_name.begins_with("test_"):
49+
gdscript_tests.push_back(await suite.run_test(suite, method_name))
3650

3751
var success: bool = rust_runner.run_all_tests(
3852
gdscript_tests,
@@ -55,13 +69,32 @@ class GDScriptTestCase:
5569
self.method_name = method_name
5670
self.suite_name = _suite_name(suite)
5771

72+
func run():
73+
push_error("run unimplemented")
74+
return false
75+
76+
static func _suite_name(suite: Object) -> String:
77+
var script: GDScript = suite.get_script()
78+
return str(script.resource_path.get_file().get_basename(), ".gd")
79+
80+
# Standard test case used for whenever something can be tested by just running a gdscript function.
81+
class GDScriptExecutableTestCase extends GDScriptTestCase:
5882
func run():
5983
# This is a no-op if the suite doesn't have this property.
6084
suite.set("_assertion_failed", false)
6185
var result = suite.call(method_name)
6286
var ok: bool = (result == true || result == null) && !suite.get("_assertion_failed")
6387
return ok
64-
65-
static func _suite_name(suite: Object) -> String:
66-
var script: GDScript = suite.get_script()
67-
return str(script.resource_path.get_file().get_basename(), ".gd")
88+
89+
# Hardcoded test case used for special cases where the standard testing API is not sufficient.
90+
#
91+
# Stores the errors generated during the execution, so they can be printed when it is appropriate to do so.
92+
# As we may not run this test case at the time we say we do in the terminal.
93+
class GDScriptHardcodedTestCase extends GDScriptTestCase:
94+
# Errors generated during execution of the test.
95+
var errors: Array[String] = []
96+
var execution_time_seconds: float = 0
97+
var result: bool = false
98+
99+
func run():
100+
return result

itest/godot/TestSuite.gd

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,12 @@ extends RefCounted
77

88
var _assertion_failed: bool = false
99

10+
func print_newline():
11+
printerr()
12+
13+
func print_error(s: String):
14+
push_error(s)
15+
1016
## Asserts that `what` is `true`, but does not abort the test. Returns `what` so you can return
1117
## early from the test function if the assertion failed.
1218
func assert_that(what: bool, message: String = "") -> bool:
@@ -15,11 +21,11 @@ func assert_that(what: bool, message: String = "") -> bool:
1521

1622
_assertion_failed = true
1723

18-
printerr() # previous line not yet broken
24+
print_newline() # previous line not yet broken
1925
if message:
20-
push_error("GDScript assertion failed: %s" % message)
26+
print_error("GDScript assertion failed: %s" % message)
2127
else:
22-
push_error("GDScript assertion failed.")
28+
print_error("GDScript assertion failed.")
2329
return false
2430

2531
func assert_eq(left, right, message: String = "") -> bool:
@@ -28,9 +34,9 @@ func assert_eq(left, right, message: String = "") -> bool:
2834

2935
_assertion_failed = true
3036

31-
printerr() # previous line not yet broken
37+
print_newline() # previous line not yet broken
3238
if message:
33-
push_error("GDScript assertion failed: %s\n left: %s\n right: %s" % [message, left, right])
39+
print_error("GDScript assertion failed: %s\n left: %s\n right: %s" % [message, left, right])
3440
else:
35-
push_error("GDScript assertion failed: `(left == right)`\n left: %s\n right: %s" % [left, right])
41+
print_error("GDScript assertion failed: `(left == right)`\n left: %s\n right: %s" % [left, right])
3642
return false

itest/godot/TestSuiteSpecial.gd

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# This Source Code Form is subject to the terms of the Mozilla Public
2+
# License, v. 2.0. If a copy of the MPL was not distributed with this
3+
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
4+
5+
class_name TestSuiteSpecial
6+
extends TestSuite
7+
8+
var errors: Array[String] = []
9+
10+
func print_newline():
11+
errors.push_back("")
12+
13+
func print_error(s: String):
14+
errors.push_back(s)
15+
16+
# Run a special test case, generating a hardcoded test-case based on the outcome of the test.
17+
func run_test(suite: Object, method_name: String) -> GDScriptTestRunner.GDScriptHardcodedTestCase:
18+
var callable: Callable = Callable(suite, method_name)
19+
20+
_assertion_failed = false
21+
var start_time = Time.get_ticks_usec()
22+
var result = await callable.call()
23+
var end_time = Time.get_ticks_usec()
24+
25+
var test_case := GDScriptTestRunner.GDScriptHardcodedTestCase.new(suite, method_name)
26+
test_case.execution_time_seconds = float(end_time - start_time) / 1000000.0
27+
test_case.result = (result or result == null) and not _assertion_failed
28+
test_case.errors = clear_errors()
29+
return test_case
30+
31+
func clear_errors() -> Array[String]:
32+
var old_errors := errors
33+
errors = []
34+
return old_errors

itest/rust/src/runner.rs

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,9 @@
77
use std::time::{Duration, Instant};
88

99
use godot::bind::{godot_api, GodotClass};
10-
use godot::builtin::{ToVariant, Variant, VariantArray};
10+
use godot::builtin::{Array, GodotString, ToVariant, Variant, VariantArray};
1111
use godot::engine::Node;
12+
use godot::log::godot_error;
1213
use godot::obj::Gd;
1314

1415
use crate::{RustTestCase, TestContext};
@@ -57,8 +58,8 @@ impl IntegrationTests {
5758
self.run_rust_tests(rust_tests, scene_tree);
5859
let rust_time = clock.elapsed();
5960
let gdscript_time = if !focus_run {
60-
self.run_gdscript_tests(gdscript_tests);
61-
Some(clock.elapsed() - rust_time)
61+
let extra_duration = self.run_gdscript_tests(gdscript_tests);
62+
Some((clock.elapsed() - rust_time) + extra_duration)
6263
} else {
6364
None
6465
};
@@ -79,22 +80,31 @@ impl IntegrationTests {
7980
}
8081
}
8182

82-
fn run_gdscript_tests(&mut self, tests: VariantArray) {
83+
fn run_gdscript_tests(&mut self, tests: VariantArray) -> Duration {
8384
let mut last_file = None;
85+
let mut extra_duration = Duration::new(0, 0);
86+
8487
for test in tests.iter_shared() {
8588
let test_file = get_property(&test, "suite_name");
8689
let test_case = get_property(&test, "method_name");
8790

8891
print_test_pre(&test_case, test_file, &mut last_file, true);
8992
let result = test.call("run", &[]);
93+
if let Some(duration) = get_execution_time(&test) {
94+
extra_duration += duration;
95+
}
9096
let success = result.try_to::<bool>().unwrap_or_else(|_| {
9197
panic!("GDScript test case {test} returned non-bool: {result}")
9298
});
99+
for error in get_errors(&test).iter_shared() {
100+
godot_error!("{error}");
101+
}
93102
let outcome = TestOutcome::from_bool(success);
94103

95104
self.update_stats(&outcome);
96105
print_test_post(&test_case, outcome);
97106
}
107+
extra_duration
98108
}
99109

100110
fn conclude(
@@ -224,6 +234,20 @@ fn get_property(test: &Variant, property: &str) -> String {
224234
test.call("get", &[property.to_variant()]).to::<String>()
225235
}
226236

237+
fn get_execution_time(test: &Variant) -> Option<Duration> {
238+
let seconds = test
239+
.call("get", &["execution_time_seconds".to_variant()])
240+
.try_to::<f64>()
241+
.ok()?;
242+
Some(Duration::from_secs_f64(seconds))
243+
}
244+
245+
fn get_errors(test: &Variant) -> Array<GodotString> {
246+
test.call("get", &["errors".to_variant()])
247+
.try_to::<Array<GodotString>>()
248+
.unwrap_or(Array::new())
249+
}
250+
227251
#[must_use]
228252
enum TestOutcome {
229253
Passed,

0 commit comments

Comments
 (0)