Skip to content

Commit 47aef2e

Browse files
authored
Merge pull request #1387 from godot-rust/qol/ptrcall-panics
Failed ptrcalls: instead of panic, print error + return default
2 parents 8733da7 + 98477e9 commit 47aef2e

File tree

9 files changed

+100
-80
lines changed

9 files changed

+100
-80
lines changed

godot-core/src/builtin/callable.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -677,7 +677,7 @@ mod custom_callable {
677677
let name = "<optimized out>";
678678
let ctx = meta::CallContext::custom_callable(name);
679679

680-
crate::private::handle_varcall_panic(&ctx, &mut *r_error, move || {
680+
crate::private::handle_fallible_varcall(&ctx, &mut *r_error, move || {
681681
// Get the RustCallable again inside closure so it doesn't have to be UnwindSafe.
682682
let c: &mut C = CallableUserdata::inner_from_raw(callable_userdata);
683683
let result = c.invoke(arg_refs);
@@ -707,7 +707,7 @@ mod custom_callable {
707707
let name = "<optimized out>";
708708
let ctx = meta::CallContext::custom_callable(name);
709709

710-
crate::private::handle_varcall_panic(&ctx, &mut *r_error, move || {
710+
crate::private::handle_fallible_varcall(&ctx, &mut *r_error, move || {
711711
// Get the FnWrapper again inside closure so the FnMut doesn't have to be UnwindSafe.
712712
let w: &mut FnWrapper<F> = CallableUserdata::inner_from_raw(callable_userdata);
713713

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ use crate::meta::{CallContext, ToGodot};
1616
use crate::private::PanicPayload;
1717
use crate::sys;
1818

19+
/// Result type for function calls that can fail.
20+
pub(crate) type CallResult<R> = Result<R, CallError>;
21+
1922
/// Error capable of representing failed function calls.
2023
///
2124
/// This type is returned from _varcall_ functions in the Godot API that begin with `try_` prefixes,

godot-core/src/meta/param_tuple.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,9 @@
77

88
use godot_ffi as sys;
99

10-
use super::{CallContext, CallResult, PropertyInfo};
1110
use crate::builtin::Variant;
11+
use crate::meta::error::CallResult;
12+
use crate::meta::{CallContext, PropertyInfo};
1213

1314
mod impls;
1415

@@ -64,7 +65,7 @@ pub trait InParamTuple: ParamTuple {
6465
args_ptr: *const sys::GDExtensionConstTypePtr,
6566
call_type: sys::PtrcallType,
6667
call_ctx: &CallContext,
67-
) -> Self;
68+
) -> CallResult<Self>;
6869

6970
/// Converts `array` to `Self` by calling [`from_variant`](crate::meta::FromGodot::from_variant) on each argument.
7071
fn from_variant_array(array: &[&Variant]) -> Self;

godot-core/src/meta/param_tuple/impls.rs

Lines changed: 13 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,10 @@ use godot_ffi as sys;
1414
use sys::GodotFfi;
1515

1616
use crate::builtin::Variant;
17-
use crate::meta::error::{CallError, ConvertError};
17+
use crate::meta::error::{CallError, CallResult};
1818
use crate::meta::{
19-
signature, ArgPassing, CallContext, FromGodot, GodotConvert, GodotType, InParamTuple,
20-
OutParamTuple, ParamTuple, ToGodot,
19+
ArgPassing, CallContext, FromGodot, GodotConvert, GodotType, InParamTuple, OutParamTuple,
20+
ParamTuple, ToGodot,
2121
};
2222

2323
macro_rules! count_idents {
@@ -57,7 +57,7 @@ macro_rules! unsafe_impl_param_tuple {
5757
unsafe fn from_varcall_args(
5858
args_ptr: *const sys::GDExtensionConstVariantPtr,
5959
call_ctx: &crate::meta::CallContext,
60-
) -> signature::CallResult<Self> {
60+
) -> CallResult<Self> {
6161
let args = (
6262
$(
6363
// SAFETY: `args_ptr` is an array with length `Self::LEN` and each element is a valid pointer, since they
@@ -80,14 +80,16 @@ macro_rules! unsafe_impl_param_tuple {
8080
args_ptr: *const sys::GDExtensionConstTypePtr,
8181
call_type: sys::PtrcallType,
8282
call_ctx: &crate::meta::CallContext,
83-
) -> Self {
84-
(
83+
) -> CallResult<Self> {
84+
let tuple = (
8585
$(
8686
// SAFETY: `args_ptr` has length `Self::LEN` and `$n` is less than `Self::LEN`, and `args_ptr` must be an array whose
8787
// `$n`-th element is of type `$P`.
88-
unsafe { ptrcall_arg::<$P, $n>(args_ptr, call_ctx, call_type) },
88+
unsafe { ptrcall_arg::<$P, $n>(args_ptr, call_ctx, call_type)? },
8989
)*
90-
)
90+
);
91+
92+
Ok(tuple) // If none of the `?` above were hit.
9193
}
9294

9395
fn from_variant_array(array: &[&Variant]) -> Self {
@@ -196,7 +198,7 @@ pub(super) unsafe fn ptrcall_arg<P: FromGodot, const N: isize>(
196198
args_ptr: *const sys::GDExtensionConstTypePtr,
197199
call_ctx: &CallContext,
198200
call_type: sys::PtrcallType,
199-
) -> P {
201+
) -> CallResult<P> {
200202
// SAFETY: It is safe to dereference `args_ptr` at `N`.
201203
let offset_ptr = unsafe { *args_ptr.offset(N) };
202204

@@ -207,7 +209,7 @@ pub(super) unsafe fn ptrcall_arg<P: FromGodot, const N: isize>(
207209

208210
<P::Via as GodotType>::try_from_ffi(ffi)
209211
.and_then(P::try_from_godot)
210-
.unwrap_or_else(|err| param_error::<P>(call_ctx, N as i32, err))
212+
.map_err(|err| CallError::failed_param_conversion::<P>(call_ctx, N, err))
211213
}
212214

213215
/// Converts `arg` into a value of type `P`.
@@ -219,7 +221,7 @@ pub(super) unsafe fn varcall_arg<P: FromGodot>(
219221
arg: sys::GDExtensionConstVariantPtr,
220222
call_ctx: &CallContext,
221223
param_index: isize,
222-
) -> Result<P, CallError> {
224+
) -> CallResult<P> {
223225
// SAFETY: It is safe to dereference `args_ptr` at `N` as a `Variant`.
224226
let variant_ref = unsafe { Variant::borrow_var_sys(arg) };
225227

@@ -228,11 +230,6 @@ pub(super) unsafe fn varcall_arg<P: FromGodot>(
228230
.map_err(|err| CallError::failed_param_conversion::<P>(call_ctx, param_index, err))
229231
}
230232

231-
fn param_error<P>(call_ctx: &CallContext, index: i32, err: ConvertError) -> ! {
232-
let param_ty = std::any::type_name::<P>();
233-
panic!("in function `{call_ctx}` at parameter [{index}] of type {param_ty}: {err}");
234-
}
235-
236233
fn assert_array_length<P: ParamTuple>(array: &[&Variant]) {
237234
assert_eq!(
238235
array.len(),

godot-core/src/meta/signature.rs

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,18 +9,17 @@ use std::borrow::Cow;
99
use std::fmt;
1010
use std::marker::PhantomData;
1111

12-
use godot_ffi::{self as sys, GodotFfi};
12+
use godot_ffi as sys;
13+
use sys::GodotFfi;
1314

1415
use crate::builtin::Variant;
15-
use crate::meta::error::{CallError, ConvertError};
16+
use crate::meta::error::{CallError, CallResult, ConvertError};
1617
use crate::meta::{
1718
FromGodot, GodotConvert, GodotType, InParamTuple, MethodParamOrReturnInfo, OutParamTuple,
1819
ParamTuple, ToGodot,
1920
};
2021
use crate::obj::{GodotClass, ValidatedObject};
2122

22-
pub(super) type CallResult<R> = Result<R, CallError>;
23-
2423
/// A full signature for a function.
2524
///
2625
/// For in-calls (that is, calls from the Godot engine to Rust code) `Params` will implement [`InParamTuple`] and `Ret`
@@ -101,19 +100,21 @@ where
101100
ret: sys::GDExtensionTypePtr,
102101
func: fn(sys::GDExtensionClassInstancePtr, Params) -> Ret,
103102
call_type: sys::PtrcallType,
104-
) {
103+
) -> CallResult<()> {
105104
// $crate::out!("in_ptrcall: {call_ctx}");
106105

107106
#[cfg(feature = "trace")]
108107
trace::push(true, true, call_ctx);
109108

110109
// SAFETY: TODO.
111-
let args = unsafe { Params::from_ptrcall_args(args_ptr, call_type, call_ctx) };
110+
let args = unsafe { Params::from_ptrcall_args(args_ptr, call_type, call_ctx)? };
112111

113112
// SAFETY:
114113
// `ret` is always a pointer to an initialized value of type $R
115114
// TODO: double-check the above
116-
unsafe { ptrcall_return::<Ret>(func(instance_ptr, args), ret, call_ctx, call_type) }
115+
unsafe { ptrcall_return::<Ret>(func(instance_ptr, args), ret, call_ctx, call_type) };
116+
117+
Ok(())
117118
}
118119
}
119120

godot-core/src/private.rs

Lines changed: 39 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ use std::sync::atomic;
1313
use sys::Global;
1414

1515
use crate::global::godot_error;
16-
use crate::meta::error::CallError;
16+
use crate::meta::error::{CallError, CallResult};
1717
use crate::meta::CallContext;
1818
use crate::obj::Gd;
1919
use crate::{classes, sys};
@@ -417,7 +417,7 @@ impl PanicPayload {
417417
///
418418
/// Returns `Err(message)` if a panic occurred, and `Ok(result)` with the result of `code` otherwise.
419419
///
420-
/// In contrast to [`handle_varcall_panic`] and [`handle_ptrcall_panic`], this function is not intended for use in `try_` functions,
420+
/// In contrast to [`handle_fallible_varcall`] and [`handle_fallible_ptrcall`], this function is not intended for use in `try_` functions,
421421
/// where the error is propagated as a `CallError` in a global variable.
422422
pub fn handle_panic<E, F, R>(error_context: E, code: F) -> Result<R, PanicPayload>
423423
where
@@ -440,59 +440,57 @@ where
440440
result
441441
}
442442

443-
// TODO(bromeon): make call_ctx lazy-evaluated (like error_ctx) everywhere;
444-
// or make it eager everywhere and ensure it's cheaply constructed in the call sites.
445-
pub fn handle_varcall_panic<F, R>(
443+
/// Invokes a function with the _varcall_ calling convention, handling both expected errors and user panics.
444+
pub fn handle_fallible_varcall<F, R>(
446445
call_ctx: &CallContext,
447446
out_err: &mut sys::GDExtensionCallError,
448447
code: F,
449448
) where
450-
F: FnOnce() -> Result<R, CallError> + std::panic::UnwindSafe,
449+
F: FnOnce() -> CallResult<R> + std::panic::UnwindSafe,
451450
{
452-
let outcome: Result<Result<R, CallError>, PanicPayload> =
453-
handle_panic(|| call_ctx.to_string(), code);
454-
455-
let call_error = match outcome {
456-
// All good.
457-
Ok(Ok(_result)) => return,
458-
459-
// Call error signalled by Godot's or gdext's validation.
460-
Ok(Err(err)) => err,
461-
462-
// Panic occurred (typically through user): forward message.
463-
Err(panic_msg) => CallError::failed_by_user_panic(call_ctx, panic_msg),
464-
};
465-
466-
let error_id = report_call_error(call_error, true);
467-
468-
// Abuse 'argument' field to store our ID.
469-
*out_err = sys::GDExtensionCallError {
470-
error: sys::GODOT_RUST_CUSTOM_CALL_ERROR,
471-
argument: error_id,
472-
expected: 0,
451+
if let Some(error_id) = handle_fallible_call(call_ctx, code, true) {
452+
// Abuse 'argument' field to store our ID.
453+
*out_err = sys::GDExtensionCallError {
454+
error: sys::GODOT_RUST_CUSTOM_CALL_ERROR,
455+
argument: error_id,
456+
expected: 0,
457+
};
473458
};
474459

475460
//sys::interface_fn!(variant_new_nil)(sys::AsUninit::as_uninit(ret));
476461
}
477462

478-
pub fn handle_ptrcall_panic<F, R>(call_ctx: &CallContext, code: F)
463+
/// Invokes a function with the _ptrcall_ calling convention, handling both expected errors and user panics.
464+
pub fn handle_fallible_ptrcall<F>(call_ctx: &CallContext, code: F)
479465
where
480-
F: FnOnce() -> R + std::panic::UnwindSafe,
466+
F: FnOnce() -> CallResult<()> + std::panic::UnwindSafe,
481467
{
482-
let outcome: Result<R, PanicPayload> = handle_panic(|| call_ctx.to_string(), code);
468+
handle_fallible_call(call_ctx, code, false);
469+
}
470+
471+
/// Common error handling for fallible calls, handling detectable errors and user panics.
472+
///
473+
/// Returns `None` if the call succeeded, or `Some(error_id)` if it failed.
474+
///
475+
/// `track_globally` indicates whether the error should be stored as an index in the global error database (for varcall calls), to convey
476+
/// out-of-band, godot-rust specific error information to the caller.
477+
fn handle_fallible_call<F, R>(call_ctx: &CallContext, code: F, track_globally: bool) -> Option<i32>
478+
where
479+
F: FnOnce() -> CallResult<R> + std::panic::UnwindSafe,
480+
{
481+
let outcome: Result<CallResult<R>, PanicPayload> = handle_panic(|| call_ctx.to_string(), code);
483482

484483
let call_error = match outcome {
485484
// All good.
486-
Ok(_result) => return,
485+
Ok(Ok(_result)) => return None,
487486

488-
// Panic occurred (typically through user): forward message.
489-
Err(payload) => CallError::failed_by_user_panic(call_ctx, payload),
490-
};
487+
// Error from Godot or godot-rust validation (e.g. parameter conversion).
488+
Ok(Err(err)) => err,
491489

492-
let _id = report_call_error(call_error, false);
493-
}
490+
// User panic occurred: forward message.
491+
Err(panic_msg) => CallError::failed_by_user_panic(call_ctx, panic_msg),
492+
};
494493

495-
fn report_call_error(call_error: CallError, track_globally: bool) -> i32 {
496494
// Print failed calls to Godot's console.
497495
// TODO Level 1 is not yet set, so this will always print if level != 0. Needs better logic to recognize try_* calls and avoid printing.
498496
// But a bit tricky with multiple threads and re-entrancy; maybe pass in info in error struct.
@@ -501,11 +499,13 @@ fn report_call_error(call_error: CallError, track_globally: bool) -> i32 {
501499
}
502500

503501
// Once there is a way to auto-remove added errors, this could be always true.
504-
if track_globally {
502+
let error_id = if track_globally {
505503
call_error_insert(call_error)
506504
} else {
507505
0
508-
}
506+
};
507+
508+
Some(error_id)
509509
}
510510

511511
// Currently unused; implemented due to temporary need and may come in handy.

godot-macros/src/class/data_models/func.rs

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ pub fn make_virtual_callback(
8686
ret: sys::GDExtensionTypePtr,
8787
) {
8888
let call_ctx = #call_ctx;
89-
let _success = ::godot::private::handle_ptrcall_panic(
89+
::godot::private::handle_fallible_ptrcall(
9090
&call_ctx,
9191
|| #invocation
9292
);
@@ -566,7 +566,7 @@ fn make_varcall_fn(call_ctx: &TokenStream, wrapped_method: &TokenStream) -> Toke
566566
err: *mut sys::GDExtensionCallError,
567567
) {
568568
let call_ctx = #call_ctx;
569-
::godot::private::handle_varcall_panic(
569+
::godot::private::handle_fallible_varcall(
570570
&call_ctx,
571571
&mut *err,
572572
|| #invocation
@@ -587,14 +587,10 @@ fn make_ptrcall_fn(call_ctx: &TokenStream, wrapped_method: &TokenStream) -> Toke
587587
ret: sys::GDExtensionTypePtr,
588588
) {
589589
let call_ctx = #call_ctx;
590-
let _success = ::godot::private::handle_panic(
591-
|| format!("{call_ctx}"),
590+
::godot::private::handle_fallible_ptrcall(
591+
&call_ctx,
592592
|| #invocation
593593
);
594-
595-
// if success.is_err() {
596-
// // TODO set return value to T::default()?
597-
// }
598594
}
599595
}
600596
}

itest/godot/input/GenFfiTests.template.gd

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,21 @@ func test_ptrcall_IDENT():
6767
mark_test_succeeded()
6868
#)
6969

70+
# Functions that are invoked via ptrcall do not have an API to propagate the error back to the caller, but Godot pre-initializes their
71+
# return value to the default value of that type. This test verifies that in case of panic, the default value (per Godot) is returned.
72+
#(
73+
func test_ptrcall_panic_IDENT():
74+
mark_test_pending()
75+
var ffi := GenFfi.new()
76+
77+
var from_rust: TYPE = ffi.panic_IDENT()
78+
_check_callconv("panic_IDENT", "ptrcall")
79+
80+
var expected_default: TYPE # initialize to default (null for objects)
81+
assert_eq(from_rust, expected_default, "return value from panicked ptrcall fn == default value")
82+
mark_test_succeeded()
83+
#)
84+
7085
#(
7186
func test_ptrcall_static_IDENT():
7287
mark_test_pending()

itest/rust/build.rs

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -320,12 +320,14 @@ fn generate_rust_methods(inputs: &[Input]) -> Vec<TokenStream> {
320320
..
321321
} = input;
322322

323-
let return_method = format_ident!("return_{}", ident);
324-
let accept_method = format_ident!("accept_{}", ident);
325-
let mirror_method = format_ident!("mirror_{}", ident);
326-
let return_static_method = format_ident!("return_static_{}", ident);
327-
let accept_static_method = format_ident!("accept_static_{}", ident);
328-
let mirror_static_method = format_ident!("mirror_static_{}", ident);
323+
let return_method = format_ident!("return_{ident}");
324+
let accept_method = format_ident!("accept_{ident}");
325+
let mirror_method = format_ident!("mirror_{ident}");
326+
let panic_method = format_ident!("panic_{ident}");
327+
328+
let return_static_method = format_ident!("return_static_{ident}");
329+
let accept_static_method = format_ident!("accept_static_{ident}");
330+
let mirror_static_method = format_ident!("mirror_static_{ident}");
329331

330332
quote! {
331333
#[func]
@@ -343,6 +345,11 @@ fn generate_rust_methods(inputs: &[Input]) -> Vec<TokenStream> {
343345
i
344346
}
345347

348+
#[func]
349+
fn #panic_method(&self) -> #rust_ty {
350+
panic!("intentional panic in `{}`", stringify!(#panic_method));
351+
}
352+
346353
#[func]
347354
fn #return_static_method() -> #rust_ty {
348355
#rust_val

0 commit comments

Comments
 (0)