Skip to content

Commit 207c4e7

Browse files
committed
Provide elaborate error message on failed varcalls
1 parent f72dee7 commit 207c4e7

File tree

6 files changed

+133
-65
lines changed

6 files changed

+133
-65
lines changed

godot-codegen/src/class_generator.rs

Lines changed: 59 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
//! Generates a file for each Godot engine + builtin class
88
99
use proc_macro2::{Ident, TokenStream};
10-
use quote::{format_ident, quote};
10+
use quote::{format_ident, quote, ToTokens};
1111
use std::path::{Path, PathBuf};
1212

1313
use crate::api_parser::*;
@@ -712,12 +712,43 @@ fn make_function_definition(
712712

713713
let is_varcall = variant_ffi.is_some();
714714
let fn_name = safe_ident(function_name);
715-
let (params, arg_exprs) = make_params(method_args, is_varcall, ctx);
715+
let [params, variant_types, arg_exprs, arg_names] = make_params(method_args, is_varcall, ctx);
716+
717+
let (prepare_arg_types, error_fn_context);
718+
if variant_ffi.is_some() {
719+
// varcall (using varargs)
720+
prepare_arg_types = quote! {
721+
let mut __arg_types = Vec::with_capacity(__explicit_args.len() + varargs.len());
722+
// __arg_types.extend(__explicit_args.iter().map(Variant::get_type));
723+
__arg_types.extend(varargs.iter().map(Variant::get_type));
724+
let __vararg_str = varargs.iter().map(|v| format!("{v}")).collect::<Vec<_>>().join(", ");
725+
};
726+
727+
let joined = arg_names
728+
.iter()
729+
.map(|n| format!("{{{n}:?}}"))
730+
.collect::<Vec<_>>()
731+
.join(", ");
732+
733+
let fmt = format!("{function_name}({joined}; {{__vararg_str}})");
734+
error_fn_context = quote! { &format!(#fmt) };
735+
} else {
736+
// ptrcall
737+
prepare_arg_types = quote! {
738+
let __arg_types = [
739+
#( #variant_types ),*
740+
];
741+
};
742+
error_fn_context = function_name.to_token_stream();
743+
};
744+
716745
let (return_decl, call_code) = make_return(
717746
return_value,
718747
variant_ffi.as_ref(),
719748
varcall_invocation,
720749
ptrcall_invocation,
750+
prepare_arg_types,
751+
error_fn_context,
721752
ctx,
722753
);
723754

@@ -733,7 +764,7 @@ fn make_function_definition(
733764
#( #arg_exprs ),*
734765
];
735766

736-
let mut __args = Vec::new();
767+
let mut __args = Vec::with_capacity(__explicit_args.len() + varargs.len());
737768
__args.extend(__explicit_args.iter().map(Variant::#sys_method));
738769
__args.extend(varargs.iter().map(Variant::#sys_method));
739770

@@ -789,39 +820,41 @@ fn make_params(
789820
method_args: &Option<Vec<MethodArg>>,
790821
is_varcall: bool,
791822
ctx: &mut Context,
792-
) -> (Vec<TokenStream>, Vec<TokenStream>) {
823+
) -> [Vec<TokenStream>; 4] {
793824
let empty = vec![];
794825
let method_args = method_args.as_ref().unwrap_or(&empty);
795826

796827
let mut params = vec![];
828+
let mut variant_types = vec![];
797829
let mut arg_exprs = vec![];
830+
let mut arg_names = vec![];
798831
for arg in method_args.iter() {
799832
let param_name = safe_ident(&arg.name);
800833
let param_ty = to_rust_type(&arg.type_, ctx);
801834

802-
params.push(quote! { #param_name: #param_ty });
803-
if is_varcall {
804-
arg_exprs.push(quote! {
805-
<#param_ty as ToVariant>::to_variant(&#param_name)
806-
});
807-
} else if let RustTy::EngineClass { tokens: path, .. } = param_ty {
808-
arg_exprs.push(quote! {
809-
<#path as AsArg>::as_arg_ptr(&#param_name)
810-
});
835+
let arg_expr = if is_varcall {
836+
quote! { <#param_ty as ToVariant>::to_variant(&#param_name) }
837+
} else if let RustTy::EngineClass { tokens: path, .. } = &param_ty {
838+
quote! { <#path as AsArg>::as_arg_ptr(&#param_name) }
811839
} else {
812-
arg_exprs.push(quote! {
813-
<#param_ty as sys::GodotFfi>::sys_const(&#param_name)
814-
});
815-
}
840+
quote! { <#param_ty as sys::GodotFfi>::sys_const(&#param_name) }
841+
};
842+
843+
params.push(quote! { #param_name: #param_ty });
844+
variant_types.push(quote! { <#param_ty as VariantMetadata>::variant_type() });
845+
arg_exprs.push(arg_expr);
846+
arg_names.push(quote! { #param_name });
816847
}
817-
(params, arg_exprs)
848+
[params, variant_types, arg_exprs, arg_names]
818849
}
819850

820851
fn make_return(
821852
return_value: Option<&MethodReturn>,
822853
variant_ffi: Option<&VariantFfi>,
823854
varcall_invocation: &TokenStream,
824855
ptrcall_invocation: &TokenStream,
856+
prepare_arg_types: TokenStream,
857+
error_fn_context: TokenStream, // only for panic message
825858
ctx: &mut Context,
826859
) -> (TokenStream, TokenStream) {
827860
let return_decl: TokenStream;
@@ -851,7 +884,10 @@ fn make_return(
851884
let variant = Variant::#from_sys_init_method(|return_ptr| {
852885
let mut __err = sys::default_call_error();
853886
#varcall_invocation
854-
sys::panic_on_call_error(&__err);
887+
if __err.error != sys::GDEXTENSION_CALL_OK {
888+
#prepare_arg_types
889+
sys::panic_call_error(&__err, #error_fn_context, &__arg_types);
890+
}
855891
});
856892
#return_expr
857893
}
@@ -863,7 +899,10 @@ fn make_return(
863899
let mut __err = sys::default_call_error();
864900
let return_ptr = std::ptr::null_mut();
865901
#varcall_invocation
866-
sys::panic_on_call_error(&__err);
902+
if __err.error != sys::GDEXTENSION_CALL_OK {
903+
#prepare_arg_types
904+
sys::panic_call_error(&__err, #error_fn_context, &__arg_types);
905+
}
867906
}
868907
}
869908
(None, Some(RustTy::EngineClass { tokens, .. })) => {

godot-core/src/builtin/variant/mod.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,10 @@ impl Variant {
122122
)
123123
};
124124

125-
sys::panic_on_call_error(&error);
125+
if error.error != sys::GDEXTENSION_CALL_OK {
126+
let arg_types: Vec<_> = args.iter().map(Variant::get_type).collect();
127+
sys::panic_call_error(&error, "call", &arg_types);
128+
}
126129
result
127130
}
128131

godot-ffi/src/lib.rs

Lines changed: 55 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -139,42 +139,7 @@ unsafe fn unwrap_ref_unchecked_mut<T>(opt: &mut Option<T>) -> &mut T {
139139
}
140140
}
141141

142-
#[doc(hidden)]
143-
#[inline]
144-
pub fn default_call_error() -> GDExtensionCallError {
145-
GDExtensionCallError {
146-
error: GDEXTENSION_CALL_OK,
147-
argument: -1,
148-
expected: -1,
149-
}
150-
}
151-
152-
#[doc(hidden)]
153-
#[inline]
154-
#[track_caller] // panic message points to call site
155-
pub fn panic_on_call_error(err: &GDExtensionCallError) {
156-
let actual = err.error;
157-
158-
assert_eq!(
159-
actual,
160-
GDEXTENSION_CALL_OK,
161-
"encountered Godot error code {}",
162-
call_error_to_string(actual)
163-
);
164-
}
165-
166-
fn call_error_to_string(err: GDExtensionCallErrorType) -> &'static str {
167-
match err {
168-
GDEXTENSION_CALL_OK => "OK",
169-
GDEXTENSION_CALL_ERROR_INVALID_METHOD => "ERROR_INVALID_METHOD",
170-
GDEXTENSION_CALL_ERROR_INVALID_ARGUMENT => "ERROR_INVALID_ARGUMENT",
171-
GDEXTENSION_CALL_ERROR_TOO_MANY_ARGUMENTS => "ERROR_TOO_MANY_ARGUMENTS",
172-
GDEXTENSION_CALL_ERROR_TOO_FEW_ARGUMENTS => "ERROR_TOO_FEW_ARGUMENTS",
173-
GDEXTENSION_CALL_ERROR_INSTANCE_IS_NULL => "ERROR_INSTANCE_IS_NULL",
174-
GDEXTENSION_CALL_ERROR_METHOD_NOT_CONST => "ERROR_METHOD_NOT_CONST",
175-
_ => "(unknown)",
176-
}
177-
}
142+
// ----------------------------------------------------------------------------------------------------------------------------------------------
178143

179144
#[macro_export]
180145
#[doc(hidden)]
@@ -192,8 +157,6 @@ macro_rules! builtin_call {
192157
};
193158
}
194159

195-
// ----------------------------------------------------------------------------------------------------------------------------------------------
196-
197160
#[macro_export]
198161
#[doc(hidden)]
199162
macro_rules! interface_fn {
@@ -256,3 +219,57 @@ where
256219
Some(mapper(ptr))
257220
}
258221
}
222+
223+
// ----------------------------------------------------------------------------------------------------------------------------------------------
224+
225+
#[doc(hidden)]
226+
#[inline]
227+
pub fn default_call_error() -> GDExtensionCallError {
228+
GDExtensionCallError {
229+
error: GDEXTENSION_CALL_OK,
230+
argument: -1,
231+
expected: -1,
232+
}
233+
}
234+
235+
#[doc(hidden)]
236+
#[inline]
237+
#[track_caller] // panic message points to call site
238+
pub fn panic_call_error(
239+
err: &GDExtensionCallError,
240+
function_name: &str,
241+
arg_types: &[VariantType],
242+
) -> ! {
243+
debug_assert_ne!(err.error, GDEXTENSION_CALL_OK); // already checked outside
244+
245+
let GDExtensionCallError {
246+
error,
247+
argument,
248+
expected,
249+
} = *err;
250+
251+
let argc = arg_types.len();
252+
let reason = match error {
253+
GDEXTENSION_CALL_ERROR_INVALID_METHOD => "method not found".to_string(),
254+
GDEXTENSION_CALL_ERROR_INVALID_ARGUMENT => {
255+
let from = arg_types[argument as usize];
256+
let to = VariantType::from_sys(expected as GDExtensionVariantType);
257+
let i = argument + 1;
258+
259+
format!("cannot convert argument #{i} from {from:?} to {to:?}")
260+
}
261+
GDEXTENSION_CALL_ERROR_TOO_MANY_ARGUMENTS => {
262+
format!("too many arguments; expected {argument}, but called with {argc}")
263+
}
264+
GDEXTENSION_CALL_ERROR_TOO_FEW_ARGUMENTS => {
265+
format!("too few arguments; expected {argument}, but called with {argc}")
266+
}
267+
GDEXTENSION_CALL_ERROR_INSTANCE_IS_NULL => "instance is null".to_string(),
268+
GDEXTENSION_CALL_ERROR_METHOD_NOT_CONST => "method is not const".to_string(), // not handled in Godot
269+
_ => format!("unknown reason (error code {error})"),
270+
};
271+
272+
// Note: Godot also outputs thread ID
273+
// In Godot source: variant.cpp:3043 or core_bind.cpp:2742
274+
panic!("Function call failed: {function_name} -- {reason}.");
275+
}

itest/godot/ManualFfiTests.gd

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,8 @@ func test_export():
3737
assert_eq(obj.object_val, node)
3838

3939
var texture_val_meta = obj.get_property_list().filter(
40-
func(el)->bool:
41-
return el["name"] == "texture_val"
42-
).front()
40+
func(el): return el["name"] == "texture_val"
41+
).front()
4342

4443
assert_that(texture_val_meta != null, "'texture_val' is defined")
4544
assert_eq(texture_val_meta["hint"], PropertyHint.PROPERTY_HINT_RESOURCE_TYPE)

itest/rust/src/node_test.rs

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@
44
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
55
*/
66

7-
use crate::itest;
8-
use godot::builtin::NodePath;
7+
use crate::{itest, TestContext};
8+
use godot::builtin::{NodePath, Variant};
99
use godot::engine::{global, node, Node, Node3D, NodeExt, PackedScene, SceneTree};
1010
use godot::obj::Share;
1111

@@ -81,3 +81,13 @@ fn node_scene_tree() {
8181
parent.free();
8282
child.free();
8383
}
84+
85+
// FIXME: call_group() crashes
86+
#[itest(skip)]
87+
fn node_call_group(ctx: &TestContext) {
88+
let mut node = ctx.scene_tree.share();
89+
let mut tree = node.get_tree().unwrap();
90+
91+
node.add_to_group("group".into(), true);
92+
tree.call_group("group".into(), "set_name".into(), &[Variant::from("name")]);
93+
}

itest/rust/src/runner.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,7 @@ fn run_rust_test(test: &RustTestCase, ctx: &TestContext) -> TestOutcome {
172172
}
173173

174174
// Explicit type to prevent tests from returning a value
175-
let err_context = || format!(" !! Test {} failed", test.name);
175+
let err_context = || format!("itest `{}` failed", test.name);
176176
let success: Option<()> = godot::private::handle_panic(err_context, || (test.function)(ctx));
177177

178178
TestOutcome::from_bool(success.is_some())

0 commit comments

Comments
 (0)