Skip to content

Commit 08f24dc

Browse files
authored
Merge pull request #1140 from godot-rust/bugfix/init-unhandled-panic
Propagate panics in object constructors to `Gd::from_init_fn()`, `new_gd()`, `new_alloc()`
2 parents 007b6b8 + e9d517b commit 08f24dc

File tree

9 files changed

+145
-47
lines changed

9 files changed

+145
-47
lines changed

godot-codegen/src/models/json.rs

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

8-
// TODO remove this warning once impl is complete
9-
// Several types have #[allow(dead_code)], can be subsequently removed.
10-
11-
// In #[derive(DeJson)].
8+
// In #[derive(DeJson)]: "this block may be rewritten with the `?` operator"
129
#![allow(clippy::question_mark)]
13-
//
10+
1411
// This file acts as deserialization check of the JSON file. Even if some fields are unused, having them declared makes sure they're
1512
// deserializable and conform to our expectations. It also doesn't add much value to annotate individual fields; it doesn't really
1613
// matter if some are unused because it's external input data.
17-
#![allow(dead_code)]
1814

1915
use nanoserde::DeJson;
2016

@@ -38,7 +34,9 @@ pub struct JsonHeader {
3834
pub version_major: u8,
3935
pub version_minor: u8,
4036
pub version_patch: u8,
37+
#[allow(dead_code)]
4138
pub version_status: String,
39+
#[allow(dead_code)]
4240
pub version_build: String,
4341
pub version_full_name: String,
4442
}
@@ -58,7 +56,9 @@ pub struct JsonBuiltinSizeForConfig {
5856
#[derive(DeJson)]
5957
pub struct JsonBuiltinClass {
6058
pub name: String,
59+
#[allow(dead_code)]
6160
pub indexing_return_type: Option<String>,
61+
#[allow(dead_code)]
6262
pub is_keyed: bool,
6363
// pub members: Option<Vec<Member>>,
6464
// pub constants: Option<Vec<BuiltinConstant>>,
@@ -156,11 +156,14 @@ pub struct JsonBuiltinConstant {
156156
#[derive(DeJson)]
157157
pub struct JsonOperator {
158158
pub name: String,
159+
#[allow(dead_code)]
159160
pub right_type: Option<String>, // null if unary
161+
#[allow(dead_code)]
160162
pub return_type: String,
161163
}
162164

163165
#[derive(DeJson)]
166+
#[allow(dead_code)]
164167
pub struct JsonMember {
165168
pub name: String,
166169
#[nserde(rename = "type")]
@@ -194,7 +197,8 @@ pub struct JsonConstructor {
194197
pub struct JsonUtilityFunction {
195198
pub name: String,
196199
pub return_type: Option<String>,
197-
/// `"general"` or `"math"`
200+
/// Category: `"general"` or `"math"`
201+
#[allow(dead_code)]
198202
pub category: String,
199203
pub is_vararg: bool,
200204
pub hash: i64,

godot-codegen/src/special_cases/special_cases.rs

+1
Original file line numberDiff line numberDiff line change
@@ -595,6 +595,7 @@ pub fn maybe_rename_virtual_method<'m>(class_name: &TyName, rust_method_name: &'
595595

596596
// TODO method-level extra docs, for:
597597
// - Node::rpc_config() -> link to RpcConfig.
598+
// - Node::process/physics_process -> mention `f32`/`f64` duality.
598599

599600
pub fn get_class_extra_docs(class_name: &TyName) -> Option<&'static str> {
600601
match class_name.godot_ty.as_str() {

godot-core/src/meta/error/call_error.rs

+4-1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
use crate::builtin::{Variant, VariantType};
99
use crate::meta::error::{ConvertError, ErasedConvertError};
1010
use crate::meta::{CallContext, ToGodot};
11+
use crate::private::PanicPayload;
1112
use crate::sys;
1213
use godot_ffi::join_debug;
1314
use std::error::Error;
@@ -300,11 +301,13 @@ impl CallError {
300301
}
301302

302303
#[doc(hidden)]
303-
pub fn failed_by_user_panic(call_ctx: &CallContext, reason: String) -> Self {
304+
pub fn failed_by_user_panic(call_ctx: &CallContext, panic_payload: PanicPayload) -> Self {
304305
// This can cause the panic message to be printed twice in some scenarios (e.g. bind_mut() borrow failure).
305306
// But in other cases (e.g. itest `dynamic_call_with_panic`), it is only printed once.
306307
// Would need some work to have a consistent experience.
307308

309+
let reason = panic_payload.into_panic_message();
310+
308311
Self::new(call_ctx, format!("function panicked: {reason}"), None)
309312
}
310313

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

+33-10
Original file line numberDiff line numberDiff line change
@@ -218,7 +218,7 @@ pub fn is_class_runtime(is_tool: bool) -> bool {
218218
}
219219

220220
// ----------------------------------------------------------------------------------------------------------------------------------------------
221-
// Panic handling
221+
// Panic *hook* management
222222

223223
pub fn extract_panic_message(err: &(dyn Send + std::any::Any)) -> String {
224224
if let Some(s) = err.downcast_ref::<&'static str>() {
@@ -370,13 +370,35 @@ pub fn get_gdext_panic_context() -> Option<String> {
370370
None
371371
}
372372

373+
// ----------------------------------------------------------------------------------------------------------------------------------------------
374+
// Panic unwinding and catching
375+
376+
pub struct PanicPayload {
377+
payload: Box<dyn std::any::Any + Send + 'static>,
378+
}
379+
380+
impl PanicPayload {
381+
pub fn new(payload: Box<dyn std::any::Any + Send + 'static>) -> Self {
382+
Self { payload }
383+
}
384+
385+
// While this could be `&self`, it's usually good practice to pass panic payloads around linearly and have only 1 representation at a time.
386+
pub fn into_panic_message(self) -> String {
387+
extract_panic_message(self.payload.as_ref())
388+
}
389+
390+
pub fn repanic(self) -> ! {
391+
std::panic::resume_unwind(self.payload)
392+
}
393+
}
394+
373395
/// Executes `code`. If a panic is thrown, it is caught and an error message is printed to Godot.
374396
///
375397
/// Returns `Err(message)` if a panic occurred, and `Ok(result)` with the result of `code` otherwise.
376398
///
377399
/// In contrast to [`handle_varcall_panic`] and [`handle_ptrcall_panic`], this function is not intended for use in `try_` functions,
378400
/// where the error is propagated as a `CallError` in a global variable.
379-
pub fn handle_panic<E, F, R>(error_context: E, code: F) -> Result<R, String>
401+
pub fn handle_panic<E, F, R>(error_context: E, code: F) -> Result<R, PanicPayload>
380402
where
381403
E: Fn() -> String,
382404
F: FnOnce() -> R + std::panic::UnwindSafe,
@@ -390,8 +412,7 @@ where
390412
cell.borrow_mut().push_function(&error_context)
391413
});
392414

393-
let result =
394-
std::panic::catch_unwind(code).map_err(|payload| extract_panic_message(payload.as_ref()));
415+
let result = std::panic::catch_unwind(code).map_err(PanicPayload::new);
395416

396417
#[cfg(debug_assertions)]
397418
ERROR_CONTEXT_STACK.with(|cell| cell.borrow_mut().pop_function());
@@ -407,8 +428,8 @@ pub fn handle_varcall_panic<F, R>(
407428
) where
408429
F: FnOnce() -> Result<R, CallError> + std::panic::UnwindSafe,
409430
{
410-
let outcome: Result<Result<R, CallError>, String> =
411-
handle_panic(|| format!("{call_ctx}"), code);
431+
let outcome: Result<Result<R, CallError>, PanicPayload> =
432+
handle_panic(|| call_ctx.to_string(), code);
412433

413434
let call_error = match outcome {
414435
// All good.
@@ -437,14 +458,14 @@ pub fn handle_ptrcall_panic<F, R>(call_ctx: &CallContext, code: F)
437458
where
438459
F: FnOnce() -> R + std::panic::UnwindSafe,
439460
{
440-
let outcome: Result<R, String> = handle_panic(|| format!("{call_ctx}"), code);
461+
let outcome: Result<R, PanicPayload> = handle_panic(|| call_ctx.to_string(), code);
441462

442463
let call_error = match outcome {
443464
// All good.
444465
Ok(_result) => return,
445466

446467
// Panic occurred (typically through user): forward message.
447-
Err(panic_msg) => CallError::failed_by_user_panic(call_ctx, panic_msg),
468+
Err(payload) => CallError::failed_by_user_panic(call_ctx, payload),
448469
};
449470

450471
let _id = report_call_error(call_error, false);
@@ -478,13 +499,15 @@ pub fn rebuild_gd(object_ref: &classes::Object) -> Gd<classes::Object> {
478499

479500
#[cfg(test)]
480501
mod tests {
481-
use super::{CallError, CallErrors};
502+
use super::{CallError, CallErrors, PanicPayload};
482503
use crate::meta::CallContext;
483504

484505
fn make(index: usize) -> CallError {
485506
let method_name = format!("method_{index}");
486507
let ctx = CallContext::func("Class", &method_name);
487-
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)
488511
}
489512

490513
#[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

0 commit comments

Comments
 (0)