Skip to content

Commit e9d517b

Browse files
committed
Propagate panics in object constructors to Gd::from_init_fn(), new_gd(), new_alloc()
1 parent 63b7da5 commit e9d517b

File tree

6 files changed

+104
-31
lines changed

6 files changed

+104
-31
lines changed

godot-core/src/obj/gd.rs

+12-2
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ use crate::obj::{
2121
bounds, cap, Bounds, DynGd, GdDerefTarget, GdMut, GdRef, GodotClass, Inherits, InstanceId,
2222
OnEditor, RawGd, WithSignals,
2323
};
24-
use crate::private::callbacks;
24+
use crate::private::{callbacks, PanicPayload};
2525
use crate::registry::property::{object_export_element_type_string, Export, Var};
2626
use crate::{classes, out};
2727

@@ -137,11 +137,16 @@ where
137137
/// MyClass { my_base, other_field: 732 }
138138
/// });
139139
/// ```
140+
///
141+
/// # Panics
142+
/// Panics occurring in the `init` function are propagated to the caller.
140143
pub fn from_init_fn<F>(init: F) -> Self
141144
where
142145
F: FnOnce(crate::obj::Base<T::Base>) -> T,
143146
{
144-
let object_ptr = callbacks::create_custom(init);
147+
let object_ptr = callbacks::create_custom(init) // or propagate panic.
148+
.unwrap_or_else(|payload| PanicPayload::repanic(payload));
149+
145150
unsafe { Gd::from_obj_sys(object_ptr) }
146151
}
147152

@@ -500,6 +505,11 @@ impl<T: GodotClass> Gd<T> {
500505
/// This is the default for most initializations from FFI. In cases where reference counter
501506
/// should explicitly **not** be updated, [`Self::from_obj_sys_weak`] is available.
502507
pub(crate) unsafe fn from_obj_sys(ptr: sys::GDExtensionObjectPtr) -> Self {
508+
debug_assert!(
509+
!ptr.is_null(),
510+
"Gd::from_obj_sys() called with null pointer"
511+
);
512+
503513
Self::from_obj_sys_or_none(ptr).unwrap()
504514
}
505515

godot-core/src/obj/traits.rs

+6
Original file line numberDiff line numberDiff line change
@@ -511,6 +511,9 @@ pub trait NewGd: GodotClass {
511511
/// Return a new, ref-counted `Gd` containing a default-constructed instance.
512512
///
513513
/// `MyClass::new_gd()` is equivalent to `Gd::<MyClass>::default()`.
514+
///
515+
/// # Panics
516+
/// If `Self` is user-defined and its default constructor `init()` panics, that panic is propagated.
514517
fn new_gd() -> Gd<Self>;
515518
}
516519

@@ -529,6 +532,9 @@ pub trait NewAlloc: GodotClass {
529532
///
530533
/// The result must be manually managed, e.g. by attaching it to the scene tree or calling `free()` after usage.
531534
/// Failure to do so will result in memory leaks.
535+
///
536+
/// # Panics
537+
/// If `Self` is user-defined and its default constructor `init()` panics, that panic is propagated to the caller.
532538
#[must_use]
533539
fn new_alloc() -> Gd<Self>;
534540
}

godot-core/src/private.rs

+8-2
Original file line numberDiff line numberDiff line change
@@ -386,6 +386,10 @@ impl PanicPayload {
386386
pub fn into_panic_message(self) -> String {
387387
extract_panic_message(self.payload.as_ref())
388388
}
389+
390+
pub fn repanic(self) -> ! {
391+
std::panic::resume_unwind(self.payload)
392+
}
389393
}
390394

391395
/// Executes `code`. If a panic is thrown, it is caught and an error message is printed to Godot.
@@ -495,13 +499,15 @@ pub fn rebuild_gd(object_ref: &classes::Object) -> Gd<classes::Object> {
495499

496500
#[cfg(test)]
497501
mod tests {
498-
use super::{CallError, CallErrors};
502+
use super::{CallError, CallErrors, PanicPayload};
499503
use crate::meta::CallContext;
500504

501505
fn make(index: usize) -> CallError {
502506
let method_name = format!("method_{index}");
503507
let ctx = CallContext::func("Class", &method_name);
504-
CallError::failed_by_user_panic(&ctx, "some panic reason".to_string())
508+
let payload = PanicPayload::new(Box::new("some panic reason".to_string()));
509+
510+
CallError::failed_by_user_panic(&ctx, payload)
505511
}
506512

507513
#[test]

godot-core/src/registry/callbacks.rs

+40-26
Original file line numberDiff line numberDiff line change
@@ -15,61 +15,79 @@ use crate::builtin::{StringName, Variant};
1515
use crate::classes::Object;
1616
use crate::meta::PropertyInfo;
1717
use crate::obj::{bounds, cap, AsDyn, Base, Bounds, Gd, GodotClass, Inherits, UserClass};
18+
use crate::private::{handle_panic, PanicPayload};
1819
use crate::registry::plugin::ErasedDynGd;
1920
use crate::storage::{as_storage, InstanceStorage, Storage, StorageRefCounted};
2021
use godot_ffi as sys;
2122
use std::any::Any;
2223
use sys::conv::u32_to_usize;
2324
use sys::interface_fn;
2425

25-
// Creation callback has `p_notify_postinitialize` parameter since 4.4: https://github.com/godotengine/godot/pull/91018.
26+
/// Godot FFI default constructor.
27+
///
28+
/// If the `init()` constructor panics, null is returned.
29+
///
30+
/// Creation callback has `p_notify_postinitialize` parameter since 4.4: <https://github.com/godotengine/godot/pull/91018>.
2631
#[cfg(since_api = "4.4")]
2732
pub unsafe extern "C" fn create<T: cap::GodotDefault>(
2833
_class_userdata: *mut std::ffi::c_void,
2934
_notify_postinitialize: sys::GDExtensionBool,
3035
) -> sys::GDExtensionObjectPtr {
31-
create_custom(T::__godot_user_init)
36+
create_custom(T::__godot_user_init).unwrap_or(std::ptr::null_mut())
3237
}
3338

3439
#[cfg(before_api = "4.4")]
3540
pub unsafe extern "C" fn create<T: cap::GodotDefault>(
3641
_class_userdata: *mut std::ffi::c_void,
3742
) -> sys::GDExtensionObjectPtr {
38-
create_custom(T::__godot_user_init)
43+
create_custom(T::__godot_user_init).unwrap_or(std::ptr::null_mut())
3944
}
4045

46+
/// Godot FFI function for recreating a GDExtension instance, e.g. after a hot reload.
47+
///
48+
/// If the `init()` constructor panics, null is returned.
4149
#[cfg(since_api = "4.2")]
4250
pub unsafe extern "C" fn recreate<T: cap::GodotDefault>(
4351
_class_userdata: *mut std::ffi::c_void,
4452
object: sys::GDExtensionObjectPtr,
4553
) -> sys::GDExtensionClassInstancePtr {
4654
create_rust_part_for_existing_godot_part(T::__godot_user_init, object)
55+
.unwrap_or(std::ptr::null_mut())
4756
}
4857

49-
pub(crate) fn create_custom<T, F>(make_user_instance: F) -> sys::GDExtensionObjectPtr
58+
pub(crate) fn create_custom<T, F>(
59+
make_user_instance: F,
60+
) -> Result<sys::GDExtensionObjectPtr, PanicPayload>
5061
where
5162
T: GodotClass,
5263
F: FnOnce(Base<T::Base>) -> T,
5364
{
5465
let base_class_name = T::Base::class_name();
55-
5666
let base_ptr = unsafe { interface_fn!(classdb_construct_object)(base_class_name.string_sys()) };
5767

58-
create_rust_part_for_existing_godot_part(make_user_instance, base_ptr);
68+
match create_rust_part_for_existing_godot_part(make_user_instance, base_ptr) {
69+
Ok(_extension_ptr) => Ok(base_ptr),
70+
Err(payload) => {
71+
// Creation of extension object failed; we must now also destroy the base object to avoid leak.
72+
// SAFETY: `base_ptr` was just created above.
73+
unsafe { interface_fn!(object_destroy)(base_ptr) };
74+
75+
Err(payload)
76+
}
77+
}
5978

6079
// std::mem::forget(base_class_name);
61-
base_ptr
6280
}
6381

64-
// with GDExt, custom object consists from two parts: Godot object and Rust object, that are
65-
// bound to each other. this method takes the first by pointer, creates the second with
66-
// supplied state and binds them together. that's used for both brand-new objects creation and
67-
// hot reload - during hot-reload, Rust objects are disposed and then created again with an
68-
// updated code, so that's necessary to link them to Godot objects again.
82+
/// Add Rust-side state for a GDExtension base object.
83+
///
84+
/// With godot-rust, custom objects consist of two parts: the Godot object and the Rust object. This method takes the Godot part by pointer,
85+
/// creates the Rust part with the supplied state, and links them together. This is used for both brand-new object creation and hot reload.
86+
/// During hot reload, Rust objects are disposed of and then created again with updated code, so it's necessary to re-link them to Godot objects.
6987
fn create_rust_part_for_existing_godot_part<T, F>(
7088
make_user_instance: F,
7189
base_ptr: sys::GDExtensionObjectPtr,
72-
) -> sys::GDExtensionClassInstancePtr
90+
) -> Result<sys::GDExtensionClassInstancePtr, PanicPayload>
7391
where
7492
T: GodotClass,
7593
F: FnOnce(Base<T::Base>) -> T,
@@ -79,7 +97,13 @@ where
7997
//out!("create callback: {}", class_name.backing);
8098

8199
let base = unsafe { Base::from_sys(base_ptr) };
82-
let user_instance = make_user_instance(unsafe { Base::from_base(&base) });
100+
101+
// User constructor init() can panic, which crashes the engine if unhandled.
102+
let context = || format!("panic during {class_name}::init() constructor");
103+
let code = || make_user_instance(unsafe { Base::from_base(&base) });
104+
let user_instance = handle_panic(context, std::panic::AssertUnwindSafe(code))?;
105+
// Print shouldn't be necessary as panic itself is printed. If this changes, re-enable in error case:
106+
// godot_error!("failed to create instance of {class_name}; Rust init() panicked");
83107

84108
let instance = InstanceStorage::<T>::construct(user_instance, base);
85109
let instance_ptr = instance.into_raw();
@@ -97,7 +121,7 @@ where
97121
}
98122

99123
// std::mem::forget(class_name);
100-
instance_ptr
124+
Ok(instance_ptr)
101125
}
102126

103127
pub unsafe extern "C" fn free<T: GodotClass>(
@@ -387,20 +411,10 @@ pub unsafe extern "C" fn validate_property<T: cap::GodotValidateProperty>(
387411

388412
sys::conv::SYS_TRUE
389413
}
414+
390415
// ----------------------------------------------------------------------------------------------------------------------------------------------
391416
// Safe, higher-level methods
392417

393-
/// Abstracts the `GodotDefault` away, for contexts where this trait bound is not statically available
394-
pub fn erased_init<T: cap::GodotDefault>(base: Box<dyn Any>) -> Box<dyn Any> {
395-
let concrete = base
396-
.downcast::<Base<<T as GodotClass>::Base>>()
397-
.expect("erased_init: bad type erasure");
398-
let extracted: Base<_> = sys::unbox(concrete);
399-
400-
let instance = T::__godot_user_init(extracted);
401-
Box::new(instance)
402-
}
403-
404418
pub fn register_class_by_builder<T: cap::GodotRegisterClass>(_class_builder: &mut dyn Any) {
405419
// TODO use actual argument, once class builder carries state
406420
// let class_builder = class_builder

itest/godot/ManualFfiTests.gd

+7
Original file line numberDiff line numberDiff line change
@@ -338,6 +338,13 @@ func test_func_rename():
338338
assert_eq(func_rename.has_method("spell_static"), true)
339339
assert_eq(func_rename.spell_static(), "static")
340340

341+
func test_init_panic():
342+
var obj := InitPanic.new() # panics in Rust
343+
assert_eq(obj, null, "Rust panic in init() returns null in GDScript")
344+
345+
# Alternative behavior (probably not desired):
346+
# assert_eq(obj.get_class(), "RefCounted", "panic in init() returns base instance without GDExtension part")
347+
341348
var gd_self_obj: GdSelfObj
342349
func update_self_reference(value):
343350
gd_self_obj.update_internal(value)

itest/rust/src/register_tests/func_test.rs

+31-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
// Needed for Clippy to accept #[cfg(all())]
99
#![allow(clippy::non_minimal_cfg)]
1010

11-
use crate::framework::itest;
11+
use crate::framework::{expect_panic, itest};
1212
use godot::classes::ClassDb;
1313
use godot::prelude::*;
1414

@@ -263,9 +263,39 @@ impl IRefCounted for GdSelfObj {
263263
}
264264
}
265265

266+
// ----------------------------------------------------------------------------------------------------------------------------------------------
267+
268+
// Also tests lack of #[class].
269+
#[derive(GodotClass)]
270+
struct InitPanic;
271+
272+
#[godot_api]
273+
impl IRefCounted for InitPanic {
274+
// Panicking constructor.
275+
fn init(_base: Base<Self::Base>) -> Self {
276+
panic!("InitPanic::init() exploded");
277+
}
278+
}
279+
266280
// ----------------------------------------------------------------------------------------------------------------------------------------------
267281
// Tests
268282

283+
#[itest]
284+
fn init_panic_is_caught() {
285+
expect_panic("default construction propagates panic", || {
286+
let _obj = InitPanic::new_gd();
287+
});
288+
}
289+
290+
#[itest]
291+
fn init_fn_panic_is_caught() {
292+
expect_panic("Gd::from_init_fn() propagates panic", || {
293+
let _obj = Gd::<InitPanic>::from_init_fn(|_base| panic!("custom init closure exploded"));
294+
});
295+
}
296+
297+
// No test for Gd::from_object(), as that simply moves the existing object without running user code.
298+
269299
#[itest]
270300
fn cfg_doesnt_interfere_with_valid_method_impls() {
271301
// If we re-implement this method but the re-implementation is removed, that should keep the non-removed implementation.

0 commit comments

Comments
 (0)