From 6d18f49a8d2bf47144cfd4c0fc97c7de1eb84b8f Mon Sep 17 00:00:00 2001 From: Jan Haller Date: Thu, 9 Mar 2023 21:52:46 +0100 Subject: [PATCH 1/3] Rename to `gdext` --- Contributing.md | 10 ++-- ReadMe.md | 44 +++++++++++------- assets/asset-licenses.md | 2 +- ...dextension-ferris.png => gdext-ferris.png} | Bin check.sh | 15 +++--- .../godot/DodgeTheCreeps.gdextension | 2 +- godot-core/src/builtin/vector2.rs | 2 +- godot-core/src/builtin/vector3.rs | 2 +- godot-core/src/builtin/vector4.rs | 2 +- godot-ffi/src/lib.rs | 2 +- godot-macros/src/gdextension.rs | 2 +- 11 files changed, 45 insertions(+), 38 deletions(-) rename assets/{gdextension-ferris.png => gdext-ferris.png} (100%) diff --git a/Contributing.md b/Contributing.md index 5226c05e6..c7b97dc41 100644 --- a/Contributing.md +++ b/Contributing.md @@ -1,8 +1,8 @@ -# Contributing to `gdextension` +# Contributing to `gdext` At this stage, we appreciate if users experiment with the library, use it in small projects and report issues and bugs they encounter. -If you plan to make bigger contributions, make sure to discuss them in a [GitHub issue] first. Since the library is evolving quickly, this avoids that multiple people work on the same thing or implement features in a way that doesn't work with other parts. Also don't hesitate to talk to the developers in the `#dev-gdextension` channel on [Discord]! +If you plan to make bigger contributions, make sure to discuss them in a [GitHub issue] first. Since the library is evolving quickly, this avoids that multiple people work on the same thing or implement features in a way that doesn't work with other parts. Also don't hesitate to talk to the developers in the `#contrib-gdext` channel on [Discord]! ## Check script @@ -22,7 +22,7 @@ $ ln -sf check.sh .git/hooks/pre-commit ## Unit tests -Because most of `gdextension` interacts with the Godot engine, which is not available from the test executable, unit tests (using `cargo test` and the `#[test]` attribute) are pretty limited in scope. +Because most of `gdext` interacts with the Godot engine, which is not available from the test executable, unit tests (using `cargo test` and the `#[test]` attribute) are pretty limited in scope. Because additional flags might be needed, the preferred way to run unit tests is through the `check.sh` script: @@ -32,7 +32,7 @@ $ ./check.sh test ## Integration tests -The `itest/` directory contains a suite of integration tests that actually exercise `gdextension` from within Godot. +The `itest/` directory contains a suite of integration tests that actually exercise `gdext` from within Godot. The `itest/rust` directory is a Rust `cdylib` library project that can be loaded as a GDExtension in Godot, with an entry point for running integration tests. The `itest/godot` directory contains the Godot project that loads this library and invokes the test suite. @@ -71,5 +71,5 @@ To run the testing suite with `double-precision` enabled you may add `--double` $ check.sh --double ``` -[GitHub issue]: https://github.com/godot-rust/gdextension/issues +[GitHub issue]: https://github.com/godot-rust/gdext/issues [Discord]: https://discord.gg/aKUCJ8rJsc diff --git a/ReadMe.md b/ReadMe.md index 8aa0605ff..5c13d7dcb 100644 --- a/ReadMe.md +++ b/ReadMe.md @@ -1,8 +1,10 @@ -![logo.png](assets/gdextension-ferris.png) +![logo.png](assets/gdext-ferris.png) # Rust bindings for GDExtension -This is an early-stage library to bind the **Rust** language to **Godot 4**. +_[Discord] | [Mastodon] | [Twitter]_ + +**gdext** is an early-stage library to bind the **Rust** language to **Godot 4**. [Godot] is an open-source game engine, whose upcoming version 4.0 brings several improvements. Its _GDExtension_ API allows integrating third-party languages and libraries. @@ -17,10 +19,10 @@ Its _GDExtension_ API allows integrating third-party languages and libraries. > * No stability guarantees. APIs will break frequently (for releases, we try to take SemVer seriously though). > Resolving the above two points has currently more weight than a stable API. -We do not recommend building a larger project in GDExtension-Rust yet. +We do not recommend building a larger project in gdext yet. However, the library can serve as a playground for experimenting. -To get an overview of currently supported features, consult [#24](https://github.com/godot-rust/gdextension/issues/24). +To get an overview of currently supported features, consult [#24](https://github.com/godot-rust/gdext/issues/24). At this point, there is **no** support for Android, iOS or WASM. Contributions are very welcome! @@ -40,7 +42,7 @@ In your Cargo.toml, add: ```toml [dependencies] -godot = { git = "https://github.com/godot-rust/gdextension", branch = "master" } +godot = { git = "https://github.com/godot-rust/gdext", branch = "master" } [lib] crate-type = ["cdylib"] @@ -53,21 +55,25 @@ To register the GDExtension library with Godot, you need to create two files rel The `[configuration]` section should be copied as-is. The `[libraries]` section should be updated to match the paths of your dynamic Rust libraries. - ```ini - [configuration] - entry_symbol = "gdextension_rust_init" - - [libraries] - linux.64 = "res://../rust/target/debug/lib{my_ext}.so" - windows.64 = "res://../rust/target/debug/{my_ext}.dll" - macos.64 = "res://../rust/target/debug/{my_ext}.dylib" - ``` + ```ini + [configuration] + entry_symbol = "gdext_rust_init" + + [libraries] + linux.debug.x86_64 = "res://../rust/target/debug/lib{my_ext}.so" + linux.release.x86_64 = "res://../rust/target/release/lib{my_ext}.so" + windows.debug.x86_64 = "res://../rust/target/debug/{my_ext}.dll" + windows.release.x86_64 = "res://../rust/target/release/{my_ext}.dll" + macos.debug = "res://../rust/target/debug/{my_ext}.dylib" + macos.release = "res://../rust/target/release/{my_ext}.dylib" + ``` + (Note that for exporting your project, you'll need to use paths inside `res://`). 2. A second file `res://.godot/extension_list.cfg` should be generated once you open the Godot editor for the first time. If not, you can also manually create it, simply containing the Godot path to your `.gdextension` file: - ``` - res://MyExt.gdextension - ``` + ``` + res://MyExt.gdextension + ``` ### Examples @@ -76,7 +82,7 @@ This integrates a small game with Godot and has all the necessary steps set up. API documentation can be generated locally using `cargo doc -p godot --no-deps --open`. -If you need help, join our [Discord] server and ask in the `#help-gdextension` channel! +If you need help, join our [Discord] server and ask in the `#help-gdext` channel! ## License @@ -96,3 +102,5 @@ Contributions are very welcome! If you want to help out, see [`Contributing.md`] [`gdnative`]: https://github.com/godot-rust/gdnative [mpl]: https://www.mozilla.org/en-US/MPL/ [Discord]: https://discord.gg/aKUCJ8rJsc +[Mastodon]: https://mastodon.gamedev.place/@GodotRust +[Twitter]: https://twitter.com/GodotRust diff --git a/assets/asset-licenses.md b/assets/asset-licenses.md index 344d5182b..d00604227 100644 --- a/assets/asset-licenses.md +++ b/assets/asset-licenses.md @@ -1,6 +1,6 @@ # Asset licenses -The godot-rust logos for both GDNative and GDExtension are derived from the following work, +The godot-rust logos for both _gdnative_ and _gdext_ are derived from the following work, with changes applied from members of the godot-rust community. | Asset | Website | Author | License | diff --git a/assets/gdextension-ferris.png b/assets/gdext-ferris.png similarity index 100% rename from assets/gdextension-ferris.png rename to assets/gdext-ferris.png diff --git a/check.sh b/check.sh index ad41aabae..fb9c859cc 100755 --- a/check.sh +++ b/check.sh @@ -6,8 +6,7 @@ # Small utility to run tests locally # Similar to minimal-ci -# Note: at the moment, there is a lot of useless recompilation. -# This should be better once unit tests and #[cfg] are sorted out. +# Note: at the moment, there is some useless recompilation, which could be improved. # --help menu for arg in $@; do @@ -123,13 +122,13 @@ END='\033[0m' for cmd in "${cmds[@]}"; do echo "> $cmd" $cmd || { - printf "$RED\n==========================" - printf "\ngodot-rust checker FAILED." - printf "\n==========================\n$END" + printf "$RED\n=====================" + printf "\ngdext: checks FAILED." + printf "\n=====================\n$END" exit 1 } done -printf "$GREEN\n==============================" -printf "\ngodot-rust checker SUCCESSFUL." -printf "\n==============================\n$END" +printf "$GREEN\n=========================" +printf "\ngdext: checks SUCCESSFUL." +printf "\n=========================\n$END" diff --git a/examples/dodge-the-creeps/godot/DodgeTheCreeps.gdextension b/examples/dodge-the-creeps/godot/DodgeTheCreeps.gdextension index 172006fb8..6eae34f0f 100644 --- a/examples/dodge-the-creeps/godot/DodgeTheCreeps.gdextension +++ b/examples/dodge-the-creeps/godot/DodgeTheCreeps.gdextension @@ -1,5 +1,5 @@ [configuration] -entry_symbol = "gdextension_rust_init" +entry_symbol = "gdext_rust_init" [libraries] linux.64 = "res://../../../target/debug/libdodge_the_creeps.so" diff --git a/godot-core/src/builtin/vector2.rs b/godot-core/src/builtin/vector2.rs index 8cb3b7583..17af59e37 100644 --- a/godot-core/src/builtin/vector2.rs +++ b/godot-core/src/builtin/vector2.rs @@ -23,7 +23,7 @@ use super::{real, RAffine2, RVec2}; /// /// It uses floating-point coordinates of 32-bit precision, unlike the engine's `float` type which /// is always 64-bit. The engine can be compiled with the option `precision=double` to use 64-bit -/// vectors, but this is not yet supported in the `gdextension` crate. +/// vectors; use the gdext library with the `double-precision` feature in that case. /// /// See [`Vector2i`] for its integer counterpart. #[derive(Debug, Default, Clone, Copy, PartialEq)] diff --git a/godot-core/src/builtin/vector3.rs b/godot-core/src/builtin/vector3.rs index 0c2ea462f..68a72d251 100644 --- a/godot-core/src/builtin/vector3.rs +++ b/godot-core/src/builtin/vector3.rs @@ -24,7 +24,7 @@ use super::{real, RVec3}; /// /// It uses floating-point coordinates of 32-bit precision, unlike the engine's `float` type which /// is always 64-bit. The engine can be compiled with the option `precision=double` to use 64-bit -/// vectors, but this is not yet supported in the `gdextension` crate. +/// vectors; use the gdext library with the `double-precision` feature in that case. /// /// See [`Vector3i`] for its integer counterpart. #[derive(Debug, Default, Clone, Copy, PartialEq)] diff --git a/godot-core/src/builtin/vector4.rs b/godot-core/src/builtin/vector4.rs index 0197b540a..f57392099 100644 --- a/godot-core/src/builtin/vector4.rs +++ b/godot-core/src/builtin/vector4.rs @@ -20,7 +20,7 @@ use super::{real, RVec4}; /// /// It uses floating-point coordinates of 32-bit precision, unlike the engine's `float` type which /// is always 64-bit. The engine can be compiled with the option `precision=double` to use 64-bit -/// vectors, but this is not yet supported in the `gdextension` crate. +/// vectors; use the gdext library with the `double-precision` feature in that case. /// /// See [`Vector4i`] for its integer counterpart. #[derive(Debug, Default, Clone, Copy, PartialEq)] diff --git a/godot-ffi/src/lib.rs b/godot-ffi/src/lib.rs index e71ed2dd5..66675776d 100644 --- a/godot-ffi/src/lib.rs +++ b/godot-ffi/src/lib.rs @@ -62,7 +62,7 @@ pub unsafe fn initialize( ) { let ver = std::ffi::CStr::from_ptr((*interface).version_string); println!( - "Initialize GDExtension interface: {}", + "Initialize GDExtension API for Rust: {}", ver.to_str().unwrap() ); diff --git a/godot-macros/src/gdextension.rs b/godot-macros/src/gdextension.rs index 7ce88ae24..43b993443 100644 --- a/godot-macros/src/gdextension.rs +++ b/godot-macros/src/gdextension.rs @@ -30,7 +30,7 @@ pub fn transform(decl: Declaration) -> ParseResult { let entry_point = parser.handle_ident("entry_point")?; parser.finish()?; - let entry_point = entry_point.unwrap_or_else(|| ident("gdextension_rust_init")); + let entry_point = entry_point.unwrap_or_else(|| ident("gdext_rust_init")); let impl_ty = &impl_decl.self_ty; Ok(quote! { From f72dee7a1c16698250e5c5a8eace47049c3abb05 Mon Sep 17 00:00:00 2001 From: Jan Haller Date: Fri, 10 Mar 2023 21:35:39 +0100 Subject: [PATCH 2/3] String, StringName, NodePath: Debug uses GDScript literals "str", &"name", ^"path" --- godot-core/src/builtin/node_path.rs | 17 +++++++++++++---- godot-core/src/builtin/string.rs | 3 ++- godot-core/src/builtin/string_name.rs | 21 +++++++++------------ 3 files changed, 24 insertions(+), 17 deletions(-) diff --git a/godot-core/src/builtin/node_path.rs b/godot-core/src/builtin/node_path.rs index 7e389c4b6..8bfbbe603 100644 --- a/godot-core/src/builtin/node_path.rs +++ b/godot-core/src/builtin/node_path.rs @@ -4,10 +4,11 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ +use std::fmt; + use crate::builtin::GodotString; use godot_ffi as sys; use godot_ffi::{ffi_methods, GDExtensionTypePtr, GodotFfi}; -use std::fmt::{Display, Formatter, Result as FmtResult}; pub struct NodePath { opaque: sys::types::OpaqueNodePath, @@ -59,10 +60,18 @@ impl From<&str> for NodePath { } } -impl Display for NodePath { - fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { +impl fmt::Display for NodePath { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let string = GodotString::from(self); + ::fmt(&string, f) + } +} + +/// Uses literal syntax from GDScript: `^"node_path"` +impl fmt::Debug for NodePath { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let string = GodotString::from(self); - ::fmt(&string, f) + write!(f, "^\"{string}\"") } } diff --git a/godot-core/src/builtin/string.rs b/godot-core/src/builtin/string.rs index 3611728ff..76f50c728 100644 --- a/godot-core/src/builtin/string.rs +++ b/godot-core/src/builtin/string.rs @@ -110,10 +110,11 @@ impl fmt::Display for GodotString { } } +/// Uses literal syntax from GDScript: `"string"` impl fmt::Debug for GodotString { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let s = String::from(self); - write!(f, "GodotString(\"{s}\")") + write!(f, "\"{s}\"") } } diff --git a/godot-core/src/builtin/string_name.rs b/godot-core/src/builtin/string_name.rs index 394c64e33..8d128b281 100644 --- a/godot-core/src/builtin/string_name.rs +++ b/godot-core/src/builtin/string_name.rs @@ -8,7 +8,7 @@ use crate::builtin::GodotString; use godot_ffi as sys; use sys::{ffi_methods, GodotFfi}; -use std::fmt::{Debug, Display, Formatter, Result as FmtResult}; +use std::fmt; use std::hash::{Hash, Hasher}; #[repr(C)] @@ -68,21 +68,18 @@ impl Default for StringName { } } -impl Display for StringName { - fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { - // TODO consider using GDScript built-in to_string() - +impl fmt::Display for StringName { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let s = GodotString::from(self); - ::fmt(&s, f) + ::fmt(&s, f) } } -impl Debug for StringName { - fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { - // TODO consider using GDScript built-in to_string() - - let s = GodotString::from(self); - ::fmt(&s, f) +/// Uses literal syntax from GDScript: `&"string_name"` +impl fmt::Debug for StringName { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let string = GodotString::from(self); + write!(f, "&\"{string}\"") } } From 207c4e72ac0c24cfb83bab16f856dd09ebc8671c Mon Sep 17 00:00:00 2001 From: Jan Haller Date: Fri, 10 Mar 2023 21:37:30 +0100 Subject: [PATCH 3/3] Provide elaborate error message on failed varcalls --- godot-codegen/src/class_generator.rs | 79 +++++++++++++++++------ godot-core/src/builtin/variant/mod.rs | 5 +- godot-ffi/src/lib.rs | 93 ++++++++++++++++----------- itest/godot/ManualFfiTests.gd | 5 +- itest/rust/src/node_test.rs | 14 +++- itest/rust/src/runner.rs | 2 +- 6 files changed, 133 insertions(+), 65 deletions(-) diff --git a/godot-codegen/src/class_generator.rs b/godot-codegen/src/class_generator.rs index b1ce37f02..5bde66589 100644 --- a/godot-codegen/src/class_generator.rs +++ b/godot-codegen/src/class_generator.rs @@ -7,7 +7,7 @@ //! Generates a file for each Godot engine + builtin class use proc_macro2::{Ident, TokenStream}; -use quote::{format_ident, quote}; +use quote::{format_ident, quote, ToTokens}; use std::path::{Path, PathBuf}; use crate::api_parser::*; @@ -712,12 +712,43 @@ fn make_function_definition( let is_varcall = variant_ffi.is_some(); let fn_name = safe_ident(function_name); - let (params, arg_exprs) = make_params(method_args, is_varcall, ctx); + let [params, variant_types, arg_exprs, arg_names] = make_params(method_args, is_varcall, ctx); + + let (prepare_arg_types, error_fn_context); + if variant_ffi.is_some() { + // varcall (using varargs) + prepare_arg_types = quote! { + let mut __arg_types = Vec::with_capacity(__explicit_args.len() + varargs.len()); + // __arg_types.extend(__explicit_args.iter().map(Variant::get_type)); + __arg_types.extend(varargs.iter().map(Variant::get_type)); + let __vararg_str = varargs.iter().map(|v| format!("{v}")).collect::>().join(", "); + }; + + let joined = arg_names + .iter() + .map(|n| format!("{{{n}:?}}")) + .collect::>() + .join(", "); + + let fmt = format!("{function_name}({joined}; {{__vararg_str}})"); + error_fn_context = quote! { &format!(#fmt) }; + } else { + // ptrcall + prepare_arg_types = quote! { + let __arg_types = [ + #( #variant_types ),* + ]; + }; + error_fn_context = function_name.to_token_stream(); + }; + let (return_decl, call_code) = make_return( return_value, variant_ffi.as_ref(), varcall_invocation, ptrcall_invocation, + prepare_arg_types, + error_fn_context, ctx, ); @@ -733,7 +764,7 @@ fn make_function_definition( #( #arg_exprs ),* ]; - let mut __args = Vec::new(); + let mut __args = Vec::with_capacity(__explicit_args.len() + varargs.len()); __args.extend(__explicit_args.iter().map(Variant::#sys_method)); __args.extend(varargs.iter().map(Variant::#sys_method)); @@ -789,32 +820,32 @@ fn make_params( method_args: &Option>, is_varcall: bool, ctx: &mut Context, -) -> (Vec, Vec) { +) -> [Vec; 4] { let empty = vec![]; let method_args = method_args.as_ref().unwrap_or(&empty); let mut params = vec![]; + let mut variant_types = vec![]; let mut arg_exprs = vec![]; + let mut arg_names = vec![]; for arg in method_args.iter() { let param_name = safe_ident(&arg.name); let param_ty = to_rust_type(&arg.type_, ctx); - params.push(quote! { #param_name: #param_ty }); - if is_varcall { - arg_exprs.push(quote! { - <#param_ty as ToVariant>::to_variant(&#param_name) - }); - } else if let RustTy::EngineClass { tokens: path, .. } = param_ty { - arg_exprs.push(quote! { - <#path as AsArg>::as_arg_ptr(&#param_name) - }); + let arg_expr = if is_varcall { + quote! { <#param_ty as ToVariant>::to_variant(&#param_name) } + } else if let RustTy::EngineClass { tokens: path, .. } = ¶m_ty { + quote! { <#path as AsArg>::as_arg_ptr(&#param_name) } } else { - arg_exprs.push(quote! { - <#param_ty as sys::GodotFfi>::sys_const(&#param_name) - }); - } + quote! { <#param_ty as sys::GodotFfi>::sys_const(&#param_name) } + }; + + params.push(quote! { #param_name: #param_ty }); + variant_types.push(quote! { <#param_ty as VariantMetadata>::variant_type() }); + arg_exprs.push(arg_expr); + arg_names.push(quote! { #param_name }); } - (params, arg_exprs) + [params, variant_types, arg_exprs, arg_names] } fn make_return( @@ -822,6 +853,8 @@ fn make_return( variant_ffi: Option<&VariantFfi>, varcall_invocation: &TokenStream, ptrcall_invocation: &TokenStream, + prepare_arg_types: TokenStream, + error_fn_context: TokenStream, // only for panic message ctx: &mut Context, ) -> (TokenStream, TokenStream) { let return_decl: TokenStream; @@ -851,7 +884,10 @@ fn make_return( let variant = Variant::#from_sys_init_method(|return_ptr| { let mut __err = sys::default_call_error(); #varcall_invocation - sys::panic_on_call_error(&__err); + if __err.error != sys::GDEXTENSION_CALL_OK { + #prepare_arg_types + sys::panic_call_error(&__err, #error_fn_context, &__arg_types); + } }); #return_expr } @@ -863,7 +899,10 @@ fn make_return( let mut __err = sys::default_call_error(); let return_ptr = std::ptr::null_mut(); #varcall_invocation - sys::panic_on_call_error(&__err); + if __err.error != sys::GDEXTENSION_CALL_OK { + #prepare_arg_types + sys::panic_call_error(&__err, #error_fn_context, &__arg_types); + } } } (None, Some(RustTy::EngineClass { tokens, .. })) => { diff --git a/godot-core/src/builtin/variant/mod.rs b/godot-core/src/builtin/variant/mod.rs index e85387a05..9957959d8 100644 --- a/godot-core/src/builtin/variant/mod.rs +++ b/godot-core/src/builtin/variant/mod.rs @@ -122,7 +122,10 @@ impl Variant { ) }; - sys::panic_on_call_error(&error); + if error.error != sys::GDEXTENSION_CALL_OK { + let arg_types: Vec<_> = args.iter().map(Variant::get_type).collect(); + sys::panic_call_error(&error, "call", &arg_types); + } result } diff --git a/godot-ffi/src/lib.rs b/godot-ffi/src/lib.rs index 66675776d..73dc00f07 100644 --- a/godot-ffi/src/lib.rs +++ b/godot-ffi/src/lib.rs @@ -139,42 +139,7 @@ unsafe fn unwrap_ref_unchecked_mut(opt: &mut Option) -> &mut T { } } -#[doc(hidden)] -#[inline] -pub fn default_call_error() -> GDExtensionCallError { - GDExtensionCallError { - error: GDEXTENSION_CALL_OK, - argument: -1, - expected: -1, - } -} - -#[doc(hidden)] -#[inline] -#[track_caller] // panic message points to call site -pub fn panic_on_call_error(err: &GDExtensionCallError) { - let actual = err.error; - - assert_eq!( - actual, - GDEXTENSION_CALL_OK, - "encountered Godot error code {}", - call_error_to_string(actual) - ); -} - -fn call_error_to_string(err: GDExtensionCallErrorType) -> &'static str { - match err { - GDEXTENSION_CALL_OK => "OK", - GDEXTENSION_CALL_ERROR_INVALID_METHOD => "ERROR_INVALID_METHOD", - GDEXTENSION_CALL_ERROR_INVALID_ARGUMENT => "ERROR_INVALID_ARGUMENT", - GDEXTENSION_CALL_ERROR_TOO_MANY_ARGUMENTS => "ERROR_TOO_MANY_ARGUMENTS", - GDEXTENSION_CALL_ERROR_TOO_FEW_ARGUMENTS => "ERROR_TOO_FEW_ARGUMENTS", - GDEXTENSION_CALL_ERROR_INSTANCE_IS_NULL => "ERROR_INSTANCE_IS_NULL", - GDEXTENSION_CALL_ERROR_METHOD_NOT_CONST => "ERROR_METHOD_NOT_CONST", - _ => "(unknown)", - } -} +// ---------------------------------------------------------------------------------------------------------------------------------------------- #[macro_export] #[doc(hidden)] @@ -192,8 +157,6 @@ macro_rules! builtin_call { }; } -// ---------------------------------------------------------------------------------------------------------------------------------------------- - #[macro_export] #[doc(hidden)] macro_rules! interface_fn { @@ -256,3 +219,57 @@ where Some(mapper(ptr)) } } + +// ---------------------------------------------------------------------------------------------------------------------------------------------- + +#[doc(hidden)] +#[inline] +pub fn default_call_error() -> GDExtensionCallError { + GDExtensionCallError { + error: GDEXTENSION_CALL_OK, + argument: -1, + expected: -1, + } +} + +#[doc(hidden)] +#[inline] +#[track_caller] // panic message points to call site +pub fn panic_call_error( + err: &GDExtensionCallError, + function_name: &str, + arg_types: &[VariantType], +) -> ! { + debug_assert_ne!(err.error, GDEXTENSION_CALL_OK); // already checked outside + + let GDExtensionCallError { + error, + argument, + expected, + } = *err; + + let argc = arg_types.len(); + let reason = match error { + GDEXTENSION_CALL_ERROR_INVALID_METHOD => "method not found".to_string(), + GDEXTENSION_CALL_ERROR_INVALID_ARGUMENT => { + let from = arg_types[argument as usize]; + let to = VariantType::from_sys(expected as GDExtensionVariantType); + let i = argument + 1; + + format!("cannot convert argument #{i} from {from:?} to {to:?}") + } + GDEXTENSION_CALL_ERROR_TOO_MANY_ARGUMENTS => { + format!("too many arguments; expected {argument}, but called with {argc}") + } + GDEXTENSION_CALL_ERROR_TOO_FEW_ARGUMENTS => { + format!("too few arguments; expected {argument}, but called with {argc}") + } + GDEXTENSION_CALL_ERROR_INSTANCE_IS_NULL => "instance is null".to_string(), + GDEXTENSION_CALL_ERROR_METHOD_NOT_CONST => "method is not const".to_string(), // not handled in Godot + _ => format!("unknown reason (error code {error})"), + }; + + // Note: Godot also outputs thread ID + // In Godot source: variant.cpp:3043 or core_bind.cpp:2742 + panic!("Function call failed: {function_name} -- {reason}."); +} diff --git a/itest/godot/ManualFfiTests.gd b/itest/godot/ManualFfiTests.gd index ebb86fae4..baaa6b330 100644 --- a/itest/godot/ManualFfiTests.gd +++ b/itest/godot/ManualFfiTests.gd @@ -37,9 +37,8 @@ func test_export(): assert_eq(obj.object_val, node) var texture_val_meta = obj.get_property_list().filter( - func(el)->bool: - return el["name"] == "texture_val" - ).front() + func(el): return el["name"] == "texture_val" + ).front() assert_that(texture_val_meta != null, "'texture_val' is defined") assert_eq(texture_val_meta["hint"], PropertyHint.PROPERTY_HINT_RESOURCE_TYPE) diff --git a/itest/rust/src/node_test.rs b/itest/rust/src/node_test.rs index 439cff36c..440725df5 100644 --- a/itest/rust/src/node_test.rs +++ b/itest/rust/src/node_test.rs @@ -4,8 +4,8 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -use crate::itest; -use godot::builtin::NodePath; +use crate::{itest, TestContext}; +use godot::builtin::{NodePath, Variant}; use godot::engine::{global, node, Node, Node3D, NodeExt, PackedScene, SceneTree}; use godot::obj::Share; @@ -81,3 +81,13 @@ fn node_scene_tree() { parent.free(); child.free(); } + +// FIXME: call_group() crashes +#[itest(skip)] +fn node_call_group(ctx: &TestContext) { + let mut node = ctx.scene_tree.share(); + let mut tree = node.get_tree().unwrap(); + + node.add_to_group("group".into(), true); + tree.call_group("group".into(), "set_name".into(), &[Variant::from("name")]); +} diff --git a/itest/rust/src/runner.rs b/itest/rust/src/runner.rs index bad2b9885..b072d5e98 100644 --- a/itest/rust/src/runner.rs +++ b/itest/rust/src/runner.rs @@ -172,7 +172,7 @@ fn run_rust_test(test: &RustTestCase, ctx: &TestContext) -> TestOutcome { } // Explicit type to prevent tests from returning a value - let err_context = || format!(" !! Test {} failed", test.name); + let err_context = || format!("itest `{}` failed", test.name); let success: Option<()> = godot::private::handle_panic(err_context, || (test.function)(ctx)); TestOutcome::from_bool(success.is_some())