From 9fa4ea592b5d30d9160b1daf3b31519a62bd6319 Mon Sep 17 00:00:00 2001 From: Pedro Ciambra Date: Fri, 6 Jan 2023 22:03:55 -0300 Subject: [PATCH 01/54] Call rustfmt directly, as rustup may not be installed. --- godot-codegen/src/lib.rs | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/godot-codegen/src/lib.rs b/godot-codegen/src/lib.rs index fa8b46cfd..a98afa8b9 100644 --- a/godot-codegen/src/lib.rs +++ b/godot-codegen/src/lib.rs @@ -96,12 +96,8 @@ fn rustfmt_if_needed(out_files: Vec) { println!("Format {} generated files...", out_files.len()); for files in out_files.chunks(20) { - let mut process = std::process::Command::new("rustup"); - process - .arg("run") - .arg("stable") - .arg("rustfmt") - .arg("--edition=2021"); + let mut process = std::process::Command::new("rustfmt"); + process.arg("--edition=2021"); println!(" Format {} files...", files.len()); for file in files { From 0f4701e7e2d738fc2026a9d657b6c2d0d5b97d0c Mon Sep 17 00:00:00 2001 From: Khelthal <80304879+Khelthal@users.noreply.github.com> Date: Wed, 11 Jan 2023 11:27:40 -0600 Subject: [PATCH 02/54] Update Cargo.toml Removed the "scalar-math" feature from glam. --- godot-core/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/godot-core/Cargo.toml b/godot-core/Cargo.toml index 1367d252d..20a1027a2 100644 --- a/godot-core/Cargo.toml +++ b/godot-core/Cargo.toml @@ -20,7 +20,7 @@ godot-ffi = { path = "../godot-ffi" } once_cell = "1.8" # See https://docs.rs/glam/latest/glam/index.html#feature-gates -glam = { version = "0.22", features = ["debug-glam-assert", "scalar-math"] } +glam = { version = "0.22", features = ["debug-glam-assert"] } # Reverse dev dependencies so doctests can use `godot::` prefix [dev-dependencies] From 67aacf85af6f85f5ba84ccf9a2b9afa52fa2bb41 Mon Sep 17 00:00:00 2001 From: Jan Haller Date: Wed, 18 Jan 2023 15:43:28 +0100 Subject: [PATCH 03/54] Update to latest Godot version Changes: * Header license text * os::Month -> time::Month * OS methods --- godot-codegen/input/gdextension_interface.h | 58 ++++++++++----------- godot-codegen/src/lib.rs | 5 +- itest/rust/src/enum_test.rs | 26 ++++----- itest/rust/src/singleton_test.rs | 3 +- 4 files changed, 46 insertions(+), 46 deletions(-) diff --git a/godot-codegen/input/gdextension_interface.h b/godot-codegen/input/gdextension_interface.h index 6049eb799..bf6f419aa 100644 --- a/godot-codegen/input/gdextension_interface.h +++ b/godot-codegen/input/gdextension_interface.h @@ -1,32 +1,32 @@ -/*************************************************************************/ -/* gdextension_interface.h */ -/*************************************************************************/ -/* This file is part of: */ -/* GODOT ENGINE */ -/* https://godotengine.org */ -/*************************************************************************/ -/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */ -/* */ -/* Permission is hereby granted, free of charge, to any person obtaining */ -/* a copy of this software and associated documentation files (the */ -/* "Software"), to deal in the Software without restriction, including */ -/* without limitation the rights to use, copy, modify, merge, publish, */ -/* distribute, sublicense, and/or sell copies of the Software, and to */ -/* permit persons to whom the Software is furnished to do so, subject to */ -/* the following conditions: */ -/* */ -/* The above copyright notice and this permission notice shall be */ -/* included in all copies or substantial portions of the Software. */ -/* */ -/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ -/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ -/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ -/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ -/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ -/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ -/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -/*************************************************************************/ +/**************************************************************************/ +/* gdextension_interface.h */ +/**************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/**************************************************************************/ #ifndef GDEXTENSION_INTERFACE_H #define GDEXTENSION_INTERFACE_H diff --git a/godot-codegen/src/lib.rs b/godot-codegen/src/lib.rs index fa8b46cfd..2bec26315 100644 --- a/godot-codegen/src/lib.rs +++ b/godot-codegen/src/lib.rs @@ -202,9 +202,8 @@ const SELECTED_CLASSES: &[&str] = &[ "CollisionObject2D", "CollisionShape2D", "Control", - "Input", - "OS", "FileAccess", + "Input", "Label", "MainLoop", "Marker2D", @@ -213,6 +212,7 @@ const SELECTED_CLASSES: &[&str] = &[ "Node3D", "Node3DGizmo", "Object", + "OS", "PackedScene", "PathFollow2D", "PhysicsBody2D", @@ -223,5 +223,6 @@ const SELECTED_CLASSES: &[&str] = &[ "SceneTree", "Sprite2D", "SpriteFrames", + "Time", "Timer", ]; diff --git a/itest/rust/src/enum_test.rs b/itest/rust/src/enum_test.rs index 8df9b9124..4a9c98f56 100644 --- a/itest/rust/src/enum_test.rs +++ b/itest/rust/src/enum_test.rs @@ -6,7 +6,7 @@ use crate::itest; use godot::engine::input::CursorShape; -use godot::engine::{file_access, os}; +use godot::engine::{file_access, time}; use std::collections::HashSet; pub fn run() -> bool { @@ -52,18 +52,18 @@ fn enum_equality() { #[itest] fn enum_hash() { let mut months = HashSet::new(); - months.insert(os::Month::MONTH_JANUARY); - months.insert(os::Month::MONTH_FEBRUARY); - months.insert(os::Month::MONTH_MARCH); - months.insert(os::Month::MONTH_APRIL); - months.insert(os::Month::MONTH_MAY); - months.insert(os::Month::MONTH_JUNE); - months.insert(os::Month::MONTH_JULY); - months.insert(os::Month::MONTH_AUGUST); - months.insert(os::Month::MONTH_SEPTEMBER); - months.insert(os::Month::MONTH_OCTOBER); - months.insert(os::Month::MONTH_NOVEMBER); - months.insert(os::Month::MONTH_DECEMBER); + months.insert(time::Month::MONTH_JANUARY); + months.insert(time::Month::MONTH_FEBRUARY); + months.insert(time::Month::MONTH_MARCH); + months.insert(time::Month::MONTH_APRIL); + months.insert(time::Month::MONTH_MAY); + months.insert(time::Month::MONTH_JUNE); + months.insert(time::Month::MONTH_JULY); + months.insert(time::Month::MONTH_AUGUST); + months.insert(time::Month::MONTH_SEPTEMBER); + months.insert(time::Month::MONTH_OCTOBER); + months.insert(time::Month::MONTH_NOVEMBER); + months.insert(time::Month::MONTH_DECEMBER); assert_eq!(months.len(), 12); } diff --git a/itest/rust/src/singleton_test.rs b/itest/rust/src/singleton_test.rs index ae71e140e..90ef8c4f1 100644 --- a/itest/rust/src/singleton_test.rs +++ b/itest/rust/src/singleton_test.rs @@ -45,8 +45,7 @@ fn singleton_is_operational() { let value = GodotString::from("SOME_VALUE"); // set_environment is const, for some reason - let is_ok = os.set_environment(key.clone(), value.clone()); - assert!(is_ok); + os.set_environment(key.clone(), value.clone()); let read_value = os.get_environment(key); assert_eq!(read_value, value); From e96bf9529d0f694abb1bc2dda6adb3d4b20416b4 Mon Sep 17 00:00:00 2001 From: Jan Haller Date: Sun, 18 Dec 2022 12:17:41 +0100 Subject: [PATCH 04/54] Fail integration tests if Godot reports leaks on exit --- .github/composite/godot/action.yml | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/.github/composite/godot/action.yml b/.github/composite/godot/action.yml index 8d03daf7d..b36498a43 100644 --- a/.github/composite/godot/action.yml +++ b/.github/composite/godot/action.yml @@ -109,7 +109,7 @@ runs: run: | cd itest/godot echo "OUTCOME=itest" >> $GITHUB_ENV - $GODOT4_BIN --headless 2>&1 | tee >(grep "SCRIPT ERROR:" -q && { + $GODOT4_BIN --headless 2>&1 | tee "${{ runner.temp }}/log.txt" | tee >(grep "SCRIPT ERROR:" -q && { printf "\n -- Godot engine encountered error, abort...\n"; pkill godot echo "OUTCOME=godot-runtime" >> $GITHUB_ENV @@ -119,6 +119,14 @@ runs: echo "OUTCOME=success" >> $GITHUB_ENV shell: bash + - name: "Check for memory leaks" + run: | + if grep -q "ObjectDB instances leaked at exit" "${{ runner.temp }}/log.txt"; then + echo "OUTCOME=godot-leak" >> $GITHUB_ENV + exit 2 + fi + shell: bash + - name: "Conclusion" if: always() run: | @@ -137,10 +145,17 @@ runs: exit 2 ;; + "godot-leak") + echo "### :x: Memory leak" > $GITHUB_STEP_SUMMARY + echo "$GODOT_BUILT_FROM" >> $GITHUB_STEP_SUMMARY + echo "Integration tests cause memory leaks." >> $GITHUB_STEP_SUMMARY + exit 3 + ;; + "itest") echo "### :x: Godot integration tests failed" > $GITHUB_STEP_SUMMARY echo "$GODOT_BUILT_FROM" >> $GITHUB_STEP_SUMMARY - exit 3 + exit 4 ;; "header-diff") @@ -150,7 +165,7 @@ runs: *) echo "### :x: Unknown error occurred" > $GITHUB_STEP_SUMMARY echo "$GODOT_BUILT_FROM" >> $GITHUB_STEP_SUMMARY - exit 4 + exit 5 ;; esac shell: bash From 48b7c6e84a63c380927d48eb8244e2331b243e1c Mon Sep 17 00:00:00 2001 From: Jan Haller Date: Sun, 18 Dec 2022 15:04:34 +0100 Subject: [PATCH 05/54] Fix #[cfg] statements related to clippy and test --- godot-core/src/init/mod.rs | 4 ++-- godot-core/src/lib.rs | 11 ++++++++--- godot-ffi/src/plugins.rs | 1 + itest/rust/src/lib.rs | 2 +- 4 files changed, 12 insertions(+), 6 deletions(-) diff --git a/godot-core/src/init/mod.rs b/godot-core/src/init/mod.rs index 30950cfcf..4a8b47588 100644 --- a/godot-core/src/init/mod.rs +++ b/godot-core/src/init/mod.rs @@ -7,7 +7,7 @@ use godot_ffi as sys; use std::collections::btree_map::BTreeMap; -#[cfg(any(test, feature = "unit-test"))] +#[cfg(feature = "unit-test")] pub fn __gdext_load_library( interface: *const sys::GDExtensionInterface, library: sys::GDExtensionClassLibraryPtr, @@ -16,7 +16,7 @@ pub fn __gdext_load_library( sys::panic_no_godot!(__gdext_load_library) } -#[cfg(not(any(test, feature = "unit-test")))] +#[cfg(not(feature = "unit-test"))] #[doc(hidden)] pub fn __gdext_load_library( interface: *const sys::GDExtensionInterface, diff --git a/godot-core/src/lib.rs b/godot-core/src/lib.rs index 6610f0357..2168ef344 100644 --- a/godot-core/src/lib.rs +++ b/godot-core/src/lib.rs @@ -15,8 +15,12 @@ // workaround https://github.com/rust-lang/rust/issues/59168#issuecomment-962214945. However, this *also* does not work, // as #[cfg(doctest)] is currently near-useless for conditional compilation: https://github.com/rust-lang/rust/issues/67295. // Yet even then, our compile error here is only one of many, as the compiler tries to build doctest without hitting this. -#[cfg(all(test, not(feature = "unit-test")))] -compile_error!("Running `cargo test` requires `--features unit-test`."); +#[cfg(all( + test, // `cargo test` + not(feature = "unit-test"), // but forgot `--features unit-test` + not(gdext_clippy) // and not `cargo clippy --cfg gdext_clippy` (this implicitly enables `test`) +))] +compile_error!("Running `cargo test` requires `--features unit-test`; `cargo clippy` requires `--cfg gdext_clippy`"); // ---------------------------------------------------------------------------------------------------------------------------------------------- @@ -34,12 +38,13 @@ pub mod obj; pub use godot_ffi as sys; pub use registry::*; -#[cfg(not(any(test, feature = "unit-test")))] +#[cfg(not(feature = "unit-test"))] pub mod engine; // Output of generated code. Mimics the file structure, symbols are re-exported. #[rustfmt::skip] #[allow(unused_imports, dead_code, non_upper_case_globals, non_snake_case)] +#[allow(clippy::too_many_arguments)] mod gen; #[cfg(feature = "unit-test")] diff --git a/godot-ffi/src/plugins.rs b/godot-ffi/src/plugins.rs index 662764e0a..f54f8448e 100644 --- a/godot-ffi/src/plugins.rs +++ b/godot-ffi/src/plugins.rs @@ -28,6 +28,7 @@ macro_rules! plugin_registry { #[doc(hidden)] #[macro_export] +#[allow(clippy::all)] #[cfg_attr(rustfmt, rustfmt::skip)] // ^ skip: paste's [< >] syntax chokes fmt // cfg_attr: workaround for https://github.com/rust-lang/rust/pull/52234#issuecomment-976702997 diff --git a/itest/rust/src/lib.rs b/itest/rust/src/lib.rs index 743cf717d..ca79593f7 100644 --- a/itest/rust/src/lib.rs +++ b/itest/rust/src/lib.rs @@ -4,7 +4,7 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -#[cfg(test)] +#[all(cfg(test), not(cfg(gdext_clippy)))] compile_error!("`cargo test` not supported for integration test -- use `cargo run`."); use godot::bind::{godot_api, GodotClass}; From 1642f631dde0f572b5bc2f3a710d911dc4aa85ec Mon Sep 17 00:00:00 2001 From: Jan Haller Date: Sun, 18 Dec 2022 15:05:16 +0100 Subject: [PATCH 06/54] Fix actual memory leak --- itest/godot/ManualFfiTests.gd | 1 + 1 file changed, 1 insertion(+) diff --git a/itest/godot/ManualFfiTests.gd b/itest/godot/ManualFfiTests.gd index e9d87e40b..0121c3417 100644 --- a/itest/godot/ManualFfiTests.gd +++ b/itest/godot/ManualFfiTests.gd @@ -44,5 +44,6 @@ func test_export() -> bool: var object_val_correct = obj.object_val == node obj.free() + node.free() return int_val_correct && string_val_correct && object_val_correct From 0d64c2329dbc5f3bdacd9b4d21425b1969a53b20 Mon Sep 17 00:00:00 2001 From: Jan Haller Date: Sun, 18 Dec 2022 15:17:44 +0100 Subject: [PATCH 07/54] Apply Clippy fixes --- godot-codegen/src/api_parser.rs | 1 + godot-codegen/src/central_generator.rs | 16 +++----- godot-codegen/src/class_generator.rs | 26 ++++++------ godot-codegen/src/context.rs | 15 +++---- godot-codegen/src/godot_exe.rs | 42 +++++++++---------- godot-codegen/src/godot_version.rs | 2 +- godot-codegen/src/special_cases.rs | 2 +- godot-codegen/src/tests.rs | 2 +- godot-codegen/src/util.rs | 4 +- godot-codegen/src/watch.rs | 2 +- godot-core/Cargo.toml | 2 +- godot-core/src/bind.rs | 1 + godot-core/src/builtin/meta/signature.rs | 9 +++-- godot-core/src/builtin/variant/impls.rs | 1 + godot-core/src/engine.rs | 2 +- godot-core/src/init/mod.rs | 42 ++++++++++++++++--- godot-core/src/lib.rs | 5 ++- godot-core/src/obj/gd.rs | 17 +++++--- godot-core/src/registry.rs | 13 +++++- godot-core/src/storage.rs | 3 ++ godot-ffi/src/global_registry.rs | 2 +- godot-ffi/src/godot_ffi.rs | 6 +++ godot-ffi/src/lib.rs | 1 + godot-ffi/src/plugins.rs | 4 +- godot-macros/src/derive_godot_class.rs | 24 +++++------ godot-macros/src/gdextension.rs | 4 +- godot-macros/src/godot_api.rs | 12 +++--- godot-macros/src/itest.rs | 2 +- godot-macros/src/lib.rs | 18 ++------- godot-macros/src/util.rs | 17 ++------ itest/rust/build.rs | 51 +++++++++++------------- itest/rust/src/export_test.rs | 11 +++-- itest/rust/src/gdscript_ffi_test.rs | 3 +- itest/rust/src/lib.rs | 2 +- itest/rust/src/virtual_methods_test.rs | 1 - 35 files changed, 194 insertions(+), 171 deletions(-) diff --git a/godot-codegen/src/api_parser.rs b/godot-codegen/src/api_parser.rs index 0218c0ca5..a22d8756c 100644 --- a/godot-codegen/src/api_parser.rs +++ b/godot-codegen/src/api_parser.rs @@ -6,6 +6,7 @@ // TODO remove this warning once impl is complete #![allow(dead_code)] +#![allow(clippy::question_mark)] // in #[derive(DeJson)] use crate::{godot_exe, StopWatch}; diff --git a/godot-codegen/src/central_generator.rs b/godot-codegen/src/central_generator.rs index 263a41b9f..63658516e 100644 --- a/godot-codegen/src/central_generator.rs +++ b/godot-codegen/src/central_generator.rs @@ -295,7 +295,7 @@ fn make_core_code(central_items: &CentralItems) -> String { fn make_central_items(api: &ExtensionApi, build_config: &str, ctx: &mut Context) -> CentralItems { let mut opaque_types = vec![]; for class in &api.builtin_class_sizes { - if &class.build_configuration == build_config { + if class.build_configuration == build_config { for ClassSize { name, size } in &class.sizes { opaque_types.push(make_opaque_type(name, *size)); } @@ -412,7 +412,7 @@ fn collect_builtin_types<'a>( } // Lowercase without underscore, to map SHOUTY_CASE to shoutycase - let normalized = shout_case.to_ascii_lowercase().replace("_", ""); + let normalized = shout_case.to_ascii_lowercase().replace('_', ""); // TODO cut down on the number of cached functions generated // e.g. there's no point in providing operator< for int @@ -500,7 +500,7 @@ fn make_variant_fns( builtin_types: &HashMap, ) -> (TokenStream, TokenStream) { let (construct_decls, construct_inits) = - make_construct_fns(&type_names, constructors, builtin_types); + make_construct_fns(type_names, constructors, builtin_types); let (destroy_decls, destroy_inits) = make_destroy_fns(type_names, has_destructor); let (op_eq_decls, op_eq_inits) = make_operator_fns(type_names, operators, "==", "EQUAL"); let (op_lt_decls, op_lt_inits) = make_operator_fns(type_names, operators, "<", "LESS"); @@ -622,8 +622,7 @@ fn make_extra_constructors( let mut extra_inits = Vec::with_capacity(constructors.len() - 2); let variant_type = &type_names.sys_variant_type; - for i in 2..constructors.len() { - let ctor = &constructors[i]; + for (i, ctor) in constructors.iter().enumerate().skip(2) { if let Some(args) = &ctor.arguments { let type_name = &type_names.snake_case; let ident = if args.len() == 1 && args[0].name == "from" { @@ -688,7 +687,7 @@ fn make_operator_fns( sys_name: &str, ) -> (TokenStream, TokenStream) { if operators.is_none() - || !operators.unwrap().iter().any(|op| &op.name == json_name) + || !operators.unwrap().iter().any(|op| op.name == json_name) || is_trivial(type_names) { return (TokenStream::new(), TokenStream::new()); @@ -726,10 +725,7 @@ fn make_operator_fns( } fn format_load_error(ident: &impl std::fmt::Display) -> String { - format!( - "failed to load GDExtension function `{}`", - ident.to_string() - ) + format!("failed to load GDExtension function `{ident}`") } /// Returns true if the type is so trivial that most of its operations are directly provided by Rust, and there is no need diff --git a/godot-codegen/src/class_generator.rs b/godot-codegen/src/class_generator.rs index 19d78cdb5..078bbc7d2 100644 --- a/godot-codegen/src/class_generator.rs +++ b/godot-codegen/src/class_generator.rs @@ -31,7 +31,7 @@ pub(crate) fn generate_class_files( continue; } - if special_cases::is_class_deleted(&class.name.as_str()) { + if special_cases::is_class_deleted(class.name.as_str()) { continue; } @@ -39,7 +39,7 @@ pub(crate) fn generate_class_files( let file_contents = generated_class.tokens.to_string(); let module_name = to_module_name(&class.name); - let out_path = gen_path.join(format!("{}.rs", module_name)); + let out_path = gen_path.join(format!("{module_name}.rs")); std::fs::write(&out_path, file_contents).expect("failed to write class file"); out_files.push(out_path); @@ -312,25 +312,25 @@ fn is_method_excluded(method: &Method, #[allow(unused_variables)] ctx: &mut Cont if method .return_value .as_ref() - .map_or(false, |ret| is_type_excluded(&ret.type_.as_str(), ctx)) + .map_or(false, |ret| is_type_excluded(ret.type_.as_str(), ctx)) || method.arguments.as_ref().map_or(false, |args| { args.iter() - .any(|arg| is_type_excluded(&arg.type_.as_str(), ctx)) + .any(|arg| is_type_excluded(arg.type_.as_str(), ctx)) }) { return true; } // -- end. - method.name.starts_with("_") + method.name.starts_with('_') || method .return_value .as_ref() - .map_or(false, |ret| ret.type_.contains("*")) + .map_or(false, |ret| ret.type_.contains('*')) || method .arguments .as_ref() - .map_or(false, |args| args.iter().any(|arg| arg.type_.contains("*"))) + .map_or(false, |args| args.iter().any(|arg| arg.type_.contains('*'))) } #[cfg(feature = "codegen-full")] @@ -343,10 +343,10 @@ fn is_function_excluded(function: &UtilityFunction, ctx: &mut Context) -> bool { function .return_type .as_ref() - .map_or(false, |ret| is_type_excluded(&ret.as_str(), ctx)) + .map_or(false, |ret| is_type_excluded(ret.as_str(), ctx)) || function.arguments.as_ref().map_or(false, |args| { args.iter() - .any(|arg| is_type_excluded(&arg.type_.as_str(), ctx)) + .any(|arg| is_type_excluded(arg.type_.as_str(), ctx)) }) } @@ -460,7 +460,7 @@ pub(crate) fn make_function_definition( quote! { pub fn #function_name( #( #params ),* ) #return_decl { - let result = unsafe { + unsafe { let __function_name = StringName::from(#function_name_str); let __call_fn = sys::interface_fn!(variant_get_ptr_utility_function)(__function_name.string_sys(), #hash); let __call_fn = __call_fn.unwrap_unchecked(); @@ -471,9 +471,7 @@ pub(crate) fn make_function_definition( let __args_ptr = __args.as_ptr(); #call - }; - - result + } } } } @@ -587,7 +585,7 @@ fn make_utility_return( let return_ty; if let Some(ret) = return_value { - let ty = to_rust_type(&ret, ctx); + let ty = to_rust_type(ret, ctx); return_decl = ty.return_decl(); return_ty = Some(ty); } else { diff --git a/godot-codegen/src/context.rs b/godot-codegen/src/context.rs index 9802b2b22..395629b61 100644 --- a/godot-codegen/src/context.rs +++ b/godot-codegen/src/context.rs @@ -38,11 +38,11 @@ impl<'a> Context<'a> { continue; } - println!("-- add engine class {}", class_name); + println!("-- add engine class {class_name}"); ctx.engine_classes.insert(class_name); if let Some(base) = class.inherits.as_ref() { - println!(" -- inherits {}", base); + println!(" -- inherits {base}"); ctx.inheritance_tree .insert(class_name.to_string(), base.clone()); } @@ -90,13 +90,10 @@ impl InheritanceTree { pub fn map_all_bases(&self, derived: &str, apply: impl Fn(&str) -> T) -> Vec { let mut maybe_base = derived; let mut result = vec![]; - loop { - if let Some(base) = self.derived_to_base.get(maybe_base).map(String::as_str) { - result.push(apply(base)); - maybe_base = base; - } else { - break; - } + + while let Some(base) = self.derived_to_base.get(maybe_base).map(String::as_str) { + result.push(apply(base)); + maybe_base = base; } result } diff --git a/godot-codegen/src/godot_exe.rs b/godot-codegen/src/godot_exe.rs index 1241883e5..460c8b347 100644 --- a/godot-codegen/src/godot_exe.rs +++ b/godot-codegen/src/godot_exe.rs @@ -11,10 +11,10 @@ use std::process::Command; /// Commands related to Godot executable -const GODOT_VERSION_PATH: &'static str = +const GODOT_VERSION_PATH: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/input/gen/godot_version.txt"); -const EXTENSION_API_PATH: &'static str = +const EXTENSION_API_PATH: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/input/gen/extension_api.json"); pub fn load_extension_api_json(watch: &mut StopWatch) -> String { @@ -35,7 +35,7 @@ pub fn load_extension_api_json(watch: &mut StopWatch) -> String { } let result = std::fs::read_to_string(json_path) - .expect(&format!("failed to open file {}", json_path.display())); + .unwrap_or_else(|_| panic!("failed to open file {}", json_path.display())); watch.record("read_json_file"); result } @@ -53,23 +53,23 @@ fn update_version_file(version: &str) { let version_path = Path::new(GODOT_VERSION_PATH); rerun_on_changed(version_path); - std::fs::write(version_path, version).expect(&format!( - "write Godot version to file {}", - version_path.display() - )); + std::fs::write(version_path, version) + .unwrap_or_else(|_| panic!("write Godot version to file {}", version_path.display())); } fn read_godot_version(godot_bin: &Path) -> String { - let output = Command::new(&godot_bin) + let output = Command::new(godot_bin) .arg("--version") .output() - .expect(&format!( - "failed to invoke Godot executable '{}'", - godot_bin.display() - )); + .unwrap_or_else(|_| { + panic!( + "failed to invoke Godot executable '{}'", + godot_bin.display() + ) + }); let output = String::from_utf8(output.stdout).expect("convert Godot version to UTF-8"); - println!("Godot version: {}", output); + println!("Godot version: {output}"); match parse_godot_version(&output) { Ok(parsed) => { @@ -84,14 +84,14 @@ fn read_godot_version(godot_bin: &Path) -> String { } Err(e) => { // Don't treat this as fatal error - panic!("failed to parse Godot version '{}': {}", output, e) + panic!("failed to parse Godot version '{output}': {e}") } } } fn dump_extension_api(godot_bin: &Path, out_file: &Path) { let cwd = out_file.parent().unwrap(); - std::fs::create_dir_all(cwd).expect(&format!("create directory '{}'", cwd.display())); + std::fs::create_dir_all(cwd).unwrap_or_else(|_| panic!("create directory '{}'", cwd.display())); println!("Dump extension API to dir '{}'...", cwd.display()); Command::new(godot_bin) @@ -100,17 +100,19 @@ fn dump_extension_api(godot_bin: &Path, out_file: &Path) { .arg("--dump-extension-api") .arg(cwd) .output() - .expect(&format!( - "failed to invoke Godot executable '{}'", - godot_bin.display() - )); + .unwrap_or_else(|_| { + panic!( + "failed to invoke Godot executable '{}'", + godot_bin.display() + ) + }); println!("Generated {}/extension_api.json.", cwd.display()); } fn locate_godot_binary() -> PathBuf { if let Ok(string) = std::env::var("GODOT4_BIN") { - println!("Found GODOT4_BIN with path to executable: '{}'", string); + println!("Found GODOT4_BIN with path to executable: '{string}'"); PathBuf::from(string) } else if let Ok(path) = which::which("godot4") { println!("Found 'godot4' executable in PATH: {}", path.display()); diff --git a/godot-codegen/src/godot_version.rs b/godot-codegen/src/godot_version.rs index 2df3f452f..84be68ddb 100644 --- a/godot-codegen/src/godot_version.rs +++ b/godot-codegen/src/godot_version.rs @@ -33,7 +33,7 @@ pub fn parse_godot_version(version_str: &str) -> Result bool { } } -pub fn maybe_renamed<'c, 'm>(class_name: &'c str, method_name: &'m str) -> &'m str { +pub fn maybe_renamed<'m>(class_name: &str, method_name: &'m str) -> &'m str { match (class_name, method_name) { ("GDScript", "new") => "instantiate", _ => method_name, diff --git a/godot-codegen/src/tests.rs b/godot-codegen/src/tests.rs index 4359b93c7..d17fc85ac 100644 --- a/godot-codegen/src/tests.rs +++ b/godot-codegen/src/tests.rs @@ -50,7 +50,7 @@ fn module_name_generator() { ]; tests.iter().for_each(|(class_name, expected)| { let actual = to_module_name(class_name); - assert_eq!(*expected, actual, "Input: {}", class_name); + assert_eq!(*expected, actual, "Input: {class_name}"); }); } diff --git a/godot-codegen/src/util.rs b/godot-codegen/src/util.rs index ccd92e532..3639ce5b7 100644 --- a/godot-codegen/src/util.rs +++ b/godot-codegen/src/util.rs @@ -14,7 +14,7 @@ pub fn make_enum_definition(enum_: &dyn Enum) -> TokenStream { // This would allow exhaustive matches (or at least auto-completed matches + #[non_exhaustive]). But even without #[non_exhaustive], // this might be a forward compatibility hazard, if Godot deprecates enumerators and adds new ones with existing ords. - let enum_name = ident(&enum_.name()); + let enum_name = ident(enum_.name()); let values = enum_.values(); let mut enumerators = Vec::with_capacity(values.len()); @@ -22,7 +22,7 @@ pub fn make_enum_definition(enum_: &dyn Enum) -> TokenStream { let mut unique_ords = Vec::with_capacity(values.len()); for enumerator in values { - let name = make_enumerator_name(&enumerator.name, &enum_.name()); + let name = make_enumerator_name(&enumerator.name, enum_.name()); let ordinal = Literal::i32_unsuffixed(enumerator.value); enumerators.push(quote! { diff --git a/godot-codegen/src/watch.rs b/godot-codegen/src/watch.rs index 434ab8c17..30de8fed0 100644 --- a/godot-codegen/src/watch.rs +++ b/godot-codegen/src/watch.rs @@ -75,7 +75,7 @@ impl StopWatch { } fn log10(n: u128) -> usize { - std::iter::successors(Some(n), |&n| (n >= 10).then(|| n / 10)).count() + std::iter::successors(Some(n), |&n| (n >= 10).then_some(n / 10)).count() } struct Metric { diff --git a/godot-core/Cargo.toml b/godot-core/Cargo.toml index 1367d252d..d41ef31af 100644 --- a/godot-core/Cargo.toml +++ b/godot-core/Cargo.toml @@ -24,7 +24,7 @@ glam = { version = "0.22", features = ["debug-glam-assert", "scalar-math"] } # Reverse dev dependencies so doctests can use `godot::` prefix [dev-dependencies] -#godot = { path = "../godot" } # when re-enabling, add unit-test feature to `godot`, or it will mess things up +godot = { path = "../godot" } # when re-enabling, add unit-test feature to `godot`, or it will mess things up #godot-ffi = { path = "../godot-ffi", features = ["unit-test"] } # unit-test [build-dependencies] diff --git a/godot-core/src/bind.rs b/godot-core/src/bind.rs index 4c7db0e05..be74d9f01 100644 --- a/godot-core/src/bind.rs +++ b/godot-core/src/bind.rs @@ -25,6 +25,7 @@ use crate::obj::GodotClass; /// Do not call any of these methods directly -- they are an interface to Godot. Functionality /// described here is available through other means (e.g. `init` via `Gd::new_default`). #[allow(unused_variables)] +#[allow(clippy::unimplemented)] // TODO consider using panic! with specific message, possibly generated code pub trait GodotExt: crate::private::You_forgot_the_attribute__godot_api where Self: GodotClass, diff --git a/godot-core/src/builtin/meta/signature.rs b/godot-core/src/builtin/meta/signature.rs index 83c0bced2..8ba1b5092 100644 --- a/godot-core/src/builtin/meta/signature.rs +++ b/godot-core/src/builtin/meta/signature.rs @@ -8,6 +8,7 @@ use godot_ffi as sys; use godot_ffi::VariantType; use std::fmt::Debug; +#[doc(hidden)] pub trait SignatureTuple { type Params; type Ret; @@ -16,7 +17,7 @@ pub trait SignatureTuple { fn property_info(index: i32, param_name: &str) -> PropertyInfo; fn param_metadata(index: i32) -> sys::GDExtensionClassMethodArgumentMetadata; - fn varcall( + unsafe fn varcall( instance_ptr: sys::GDExtensionClassInstancePtr, args_ptr: *const sys::GDExtensionConstVariantPtr, ret: sys::GDExtensionVariantPtr, @@ -27,7 +28,7 @@ pub trait SignatureTuple { // Note: this method imposes extra bounds on GodotFfi, which may not be implemented for user types. // We could fall back to varcalls in such cases, and not require GodotFfi categorically. - fn ptrcall( + unsafe fn ptrcall( instance_ptr: sys::GDExtensionClassInstancePtr, args_ptr: *const sys::GDExtensionConstTypePtr, ret: sys::GDExtensionTypePtr, @@ -104,7 +105,7 @@ macro_rules! impl_signature_for_tuple { } #[inline] - fn varcall( + unsafe fn varcall( instance_ptr: sys::GDExtensionClassInstancePtr, args_ptr: *const sys::GDExtensionConstVariantPtr, ret: sys::GDExtensionVariantPtr, @@ -136,7 +137,7 @@ macro_rules! impl_signature_for_tuple { } #[inline] - fn ptrcall( + unsafe fn ptrcall( instance_ptr: sys::GDExtensionClassInstancePtr, args_ptr: *const sys::GDExtensionConstTypePtr, ret: sys::GDExtensionTypePtr, diff --git a/godot-core/src/builtin/variant/impls.rs b/godot-core/src/builtin/variant/impls.rs index c646ebb7b..e90195e03 100644 --- a/godot-core/src/builtin/variant/impls.rs +++ b/godot-core/src/builtin/variant/impls.rs @@ -128,6 +128,7 @@ macro_rules! impl_variant_traits_float { // General impls #[rustfmt::skip] +#[allow(clippy::module_inception)] mod impls { use super::*; diff --git a/godot-core/src/engine.rs b/godot-core/src/engine.rs index 5fdd36853..e0c1e3876 100644 --- a/godot-core/src/engine.rs +++ b/godot-core/src/engine.rs @@ -162,7 +162,7 @@ pub(crate) fn display_string( ptr: &Gd, f: &mut std::fmt::Formatter<'_>, ) -> std::fmt::Result { - let string: GodotString = ptr.as_object(|obj| Object::to_string(obj)); + let string: GodotString = ptr.as_object(Object::to_string); ::fmt(&string, f) } diff --git a/godot-core/src/init/mod.rs b/godot-core/src/init/mod.rs index 4a8b47588..a5e7b3b02 100644 --- a/godot-core/src/init/mod.rs +++ b/godot-core/src/init/mod.rs @@ -18,12 +18,13 @@ pub fn __gdext_load_library( #[cfg(not(feature = "unit-test"))] #[doc(hidden)] -pub fn __gdext_load_library( +// TODO consider body safe despite unsafe function, and explicitly mark unsafe {} locations +pub unsafe fn __gdext_load_library( interface: *const sys::GDExtensionInterface, library: sys::GDExtensionClassLibraryPtr, init: *mut sys::GDExtensionInitialization, ) -> sys::GDExtensionBool { - unsafe { sys::initialize(interface, library) }; + sys::initialize(interface, library); let mut handle = InitHandle::new(); @@ -38,10 +39,9 @@ pub fn __gdext_load_library( }; let is_success = /*handle.*/success as u8; - unsafe { - *init = godot_init_params; - INIT_HANDLE = Some(handle); - } + + *init = godot_init_params; + INIT_HANDLE = Some(handle); is_success } @@ -73,6 +73,36 @@ unsafe extern "C" fn ffi_deinitialize_layer( #[doc(hidden)] pub static mut INIT_HANDLE: Option = None; +/// Defines the entry point for a GDExtension Rust library. +/// +/// Every library should have exactly one implementation of this trait. It is always used in combination with the +/// [`#[gdextension]`][gdextension] proc-macro attribute. +/// +/// The simplest usage is as follows. This will automatically perform the necessary init and cleanup routines, and register +/// all classes marked with `#[derive(GodotClass)]`, without needing to mention them in a central list. The order in which +/// classes are registered is not specified. +/// +/// ``` +/// # use godot::init::*; +/// +/// // This is just a type tag without any functionality +/// struct MyExtension; +/// +/// #[gdextension] +/// unsafe impl ExtensionLibrary for MyExtension {} +/// ``` +/// +/// # Safety +/// By using godot-rust, you accept the safety considerations [as outlined in the book][safety]. +/// Please make sure you fully understand the implications. +/// +/// The library cannot enforce any safety guarantees outside Rust code, which means that **you as a user** are +/// responsible to uphold them: namely in GDScript code or other GDExtension bindings loaded by the engine. +/// Violating this may cause undefined behavior, even when invoking _safe_ functions. +/// +/// [gdextension]: crate::init::gdextension +/// [safety]: https://godot-rust.github.io/book/gdextension/safety.html +// FIXME intra-doc link pub unsafe trait ExtensionLibrary { fn load_library(handle: &mut InitHandle) -> bool { handle.register_layer(InitLevel::Scene, DefaultLayer); diff --git a/godot-core/src/lib.rs b/godot-core/src/lib.rs index 2168ef344..fccfe9af2 100644 --- a/godot-core/src/lib.rs +++ b/godot-core/src/lib.rs @@ -43,8 +43,9 @@ pub mod engine; // Output of generated code. Mimics the file structure, symbols are re-exported. #[rustfmt::skip] -#[allow(unused_imports, dead_code, non_upper_case_globals, non_snake_case)] -#[allow(clippy::too_many_arguments)] +#[allow(unused_imports, dead_code, non_upper_case_globals, non_snake_case, clippy::too_many_arguments, clippy::let_and_return, clippy::new_ret_no_self)] +#[allow(clippy::upper_case_acronyms)] // TODO remove this line once we transform names +#[allow(clippy::wrong_self_convention)] // TODO remove once to_string is const mod gen; #[cfg(feature = "unit-test")] diff --git a/godot-core/src/obj/gd.rs b/godot-core/src/obj/gd.rs index 002cacba6..29ea1f300 100644 --- a/godot-core/src/obj/gd.rs +++ b/godot-core/src/obj/gd.rs @@ -146,6 +146,8 @@ where } /// Storage obj associated with the extension instance + // FIXME proper + safe interior mutability, also that Clippy is happy + #[allow(clippy::mut_from_ref)] pub(crate) fn storage(&self) -> &mut InstanceStorage { let callbacks = crate::storage::nop_instance_callbacks(); @@ -467,6 +469,13 @@ impl GodotFfi for Gd { } impl Gd { + /// Runs `init_fn` on the address of a pointer (initialized to null). If that pointer is still null after the `init_fn` call, + /// then `None` will be returned; otherwise `from_obj_sys(ptr)`. + /// + /// # Safety + /// `init_fn` must be a function that correctly handles an "type pointer" pointing to an "object pointer" + #[doc(hidden)] + // TODO unsafe on init_fn instead of this fn? pub unsafe fn from_sys_init_opt(init_fn: impl FnOnce(sys::GDExtensionTypePtr)) -> Option { // Note: see _call_native_mb_ret_obj() in godot-cpp, which does things quite different (e.g. querying the instance binding). @@ -499,7 +508,7 @@ impl Drop for Gd { // No-op for manually managed objects out!("Gd::drop <{}>", std::any::type_name::()); - let is_last = T::Mem::maybe_dec_ref(&self); // may drop + let is_last = T::Mem::maybe_dec_ref(self); // may drop if is_last { unsafe { interface_fn!(object_destroy)(self.obj_sys()); @@ -549,7 +558,7 @@ impl FromVariant for Gd { impl ToVariant for Gd { fn to_variant(&self) -> Variant { - let variant = unsafe { + unsafe { Variant::from_var_sys_init(|variant_ptr| { let converter = sys::builtin_fn!(object_to_variant); @@ -562,9 +571,7 @@ impl ToVariant for Gd { ptr::addr_of!(type_ptr) as sys::GDExtensionTypePtr, ); }) - }; - - variant + } } } diff --git a/godot-core/src/registry.rs b/godot-core/src/registry.rs index 885a10637..8b5bbe40e 100644 --- a/godot-core/src/registry.rs +++ b/godot-core/src/registry.rs @@ -22,6 +22,7 @@ use std::collections::HashMap; use std::fmt::{Debug, Formatter, Result as FmtResult}; use std::ptr; +/// Piece of information that is gathered by the self-registration ("plugin") system. #[derive(Debug)] pub struct ClassPlugin { pub class_name: &'static str, @@ -39,10 +40,11 @@ pub struct ErasedRegisterFn { impl Debug for ErasedRegisterFn { fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { - write!(f, "0x{:0>16x}", self.raw as u64) + write!(f, "0x{:0>16x}", self.raw as usize) } } +/// Represents the data part of a [`ClassPlugin`] instance. #[derive(Debug, Clone)] pub enum PluginComponent { /// Class definition itself, must always be available @@ -110,6 +112,7 @@ struct ClassRegistrationInfo { godot_params: sys::GDExtensionClassCreationInfo, } +/// Registers a class with static type information. pub fn register_class() { // TODO: provide overloads with only some trait impls @@ -171,6 +174,7 @@ pub fn auto_register_classes() { out!("All classes auto-registered."); } +/// Populate `c` with all the relevant data from `component` (depending on component type). fn fill_class_info(component: PluginComponent, c: &mut ClassRegistrationInfo) { // out!("| reg (before): {c:?}"); // out!("| comp: {component:?}"); @@ -210,6 +214,7 @@ fn fill_class_info(component: PluginComponent, c: &mut ClassRegistrationInfo) { // out!(); } +/// If `src` is occupied, it moves the value into `dst`, while ensuring that no previous value is present in `dst`. fn fill_into(dst: &mut Option, src: Option) { match (dst, src) { (dst @ None, src) => *dst = src, @@ -218,6 +223,7 @@ fn fill_into(dst: &mut Option, src: Option) { } } +/// Registers a class with given the dynamic type information `info`. fn register_class_raw(info: ClassRegistrationInfo) { // First register class... @@ -253,7 +259,10 @@ fn register_class_raw(info: ClassRegistrationInfo) { } } -// Re-exported to crate::private +/// Callbacks that are passed as function pointers to Godot upon class registration. +/// +/// Re-exported to `crate::private` +#[allow(clippy::missing_safety_doc)] pub mod callbacks { use super::*; use crate::bind::GodotExt; diff --git a/godot-core/src/storage.rs b/godot-core/src/storage.rs index 98a0afb0e..b501d1de3 100644 --- a/godot-core/src/storage.rs +++ b/godot-core/src/storage.rs @@ -143,6 +143,9 @@ impl Drop for InstanceStorage { /// Interprets the opaque pointer as pointing to `InstanceStorage`. /// /// Note: returns reference with unbounded lifetime; intended for local usage +/// +/// # Safety +/// `instance_ptr` is assumed to point to a valid instance. // FIXME unbounded ref AND &mut out of thin air is a huge hazard -- consider using with_storage(ptr, closure) and drop_storage(ptr) pub unsafe fn as_storage<'u, T: GodotClass>( instance_ptr: sys::GDExtensionClassInstancePtr, diff --git a/godot-ffi/src/global_registry.rs b/godot-ffi/src/global_registry.rs index 438542cc7..0433cc614 100644 --- a/godot-ffi/src/global_registry.rs +++ b/godot-ffi/src/global_registry.rs @@ -31,7 +31,7 @@ pub struct GlobalRegistry { impl GlobalRegistry { pub fn c_string(&mut self, s: &str) -> *const i8 { - let value = CString::new(s).expect(&format!("Invalid string '{s}'")); + let value = CString::new(s).unwrap_or_else(|_| panic!("Invalid string '{s}'")); if let Some(existing) = self.c_strings.get(&value) { //println!("<<< Cache '{s}'"); diff --git a/godot-ffi/src/godot_ffi.rs b/godot-ffi/src/godot_ffi.rs index 319cb7030..96effeaab 100644 --- a/godot-ffi/src/godot_ffi.rs +++ b/godot-ffi/src/godot_ffi.rs @@ -48,9 +48,15 @@ pub trait GodotFuncMarshal: Sized { type Via: Debug; /// Used for function arguments. On failure, the argument which can't be converted to Self is returned. + /// + /// # Safety + /// The value behind `ptr` must be the C FFI type that corresponds to `Self`. unsafe fn try_from_sys(ptr: sys::GDExtensionTypePtr) -> Result; /// Used for function return values. On failure, `self` which can't be converted to Via is returned. + /// + /// # Safety + /// The value behind `ptr` must be the C FFI type that corresponds to `Self`. unsafe fn try_write_sys(&self, dst: sys::GDExtensionTypePtr) -> Result<(), Self>; } diff --git a/godot-ffi/src/lib.rs b/godot-ffi/src/lib.rs index 5bc09b271..4b60e420b 100644 --- a/godot-ffi/src/lib.rs +++ b/godot-ffi/src/lib.rs @@ -275,6 +275,7 @@ macro_rules! static_assert_eq_size { } /// Extract value from box before `into_inner()` is stable +#[allow(clippy::boxed_local)] // false positive pub fn unbox(value: Box) -> T { // Deref-move is a Box magic feature; see https://stackoverflow.com/a/42264074 *value diff --git a/godot-ffi/src/plugins.rs b/godot-ffi/src/plugins.rs index f54f8448e..c8faabff7 100644 --- a/godot-ffi/src/plugins.rs +++ b/godot-ffi/src/plugins.rs @@ -28,8 +28,8 @@ macro_rules! plugin_registry { #[doc(hidden)] #[macro_export] -#[allow(clippy::all)] -#[cfg_attr(rustfmt, rustfmt::skip)] +#[allow(clippy::deprecated_cfg_attr)] +#[cfg_attr(rustfmt, rustfmt::skip)] // ^ skip: paste's [< >] syntax chokes fmt // cfg_attr: workaround for https://github.com/rust-lang/rust/pull/52234#issuecomment-976702997 macro_rules! plugin_add_inner { diff --git a/godot-macros/src/derive_godot_class.rs b/godot-macros/src/derive_godot_class.rs index 742189287..a169527c8 100644 --- a/godot-macros/src/derive_godot_class.rs +++ b/godot-macros/src/derive_godot_class.rs @@ -4,9 +4,7 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -use crate::util::{ - bail, bail_error, ensure_kv_empty, ident, parse_kv_group, path_is_single, KvMap, KvValue, -}; +use crate::util::{bail, ensure_kv_empty, ident, parse_kv_group, path_is_single, KvMap, KvValue}; use crate::{util, ParseResult}; use proc_macro2::{Ident, Punct, Span, TokenStream}; use quote::spanned::Spanned; @@ -18,7 +16,7 @@ pub fn transform(input: TokenStream) -> ParseResult { let class = decl .as_struct() - .ok_or(venial::Error::new("Not a valid struct"))?; + .ok_or_else(|| venial::Error::new("Not a valid struct"))?; let struct_cfg = parse_struct_attributes(class)?; let fields = parse_fields(class)?; @@ -140,7 +138,7 @@ fn parse_fields(class: &Struct) -> ParseResult { is_base = true; if let Some(prev_base) = base_field { bail( - &format!( + format!( "#[base] allowed for at most 1 field, already applied to '{}'", prev_base.name ), @@ -152,7 +150,7 @@ fn parse_fields(class: &Struct) -> ParseResult { match parse_kv_group(&attr.value) { Ok(export_kv) => { let exported_field = - ExportedField::new_from_kv(Field::new(&field), &attr, export_kv)?; + ExportedField::new_from_kv(Field::new(&field), attr, export_kv)?; exported_fields.push(exported_field); } Err(error) => { @@ -177,7 +175,7 @@ fn parse_fields(class: &Struct) -> ParseResult { } /// Parses a `#[class(...)]` attribute -fn parse_class_attr(attributes: &Vec) -> ParseResult> { +fn parse_class_attr(attributes: &[Attribute]) -> ParseResult> { let mut godot_attr = None; for attr in attributes.iter() { let path = &attr.path; @@ -243,29 +241,29 @@ impl ExportedField { ensure_kv_empty(map, attr.__span())?; - return Ok(ExportedField { + Ok(ExportedField { field, getter, setter, variant_type, - }); + }) } fn require_key_value(map: &mut KvMap, key: &str, attr: &Attribute) -> ParseResult { if let Some(value) = map.remove(key) { if let KvValue::Lit(value) = value { - return Ok(value); + Ok(value) } else { - return bail( + bail( format!( "#[export] attribute {} with a non-literal variant_type", key ), attr, - )?; + )? } } else { - return bail(format!("#[export] attribute without a {}", key), attr); + bail(format!("#[export] attribute without a {}", key), attr) } } } diff --git a/godot-macros/src/gdextension.rs b/godot-macros/src/gdextension.rs index 8ee297eb8..aaf0e7e4e 100644 --- a/godot-macros/src/gdextension.rs +++ b/godot-macros/src/gdextension.rs @@ -38,13 +38,13 @@ pub fn transform(meta: TokenStream, input: TokenStream) -> Result entry_point = Some(f), - _ => return bail(&format!("#[gdextension]: invalid argument `{k}`"), attr), + _ => return bail(format!("#[gdextension]: invalid argument `{k}`"), attr), } } } } - let entry_point = entry_point.unwrap_or(ident("gdextension_rust_init")); + let entry_point = entry_point.unwrap_or_else(|| ident("gdextension_rust_init")); let impl_ty = &impl_decl.self_ty; Ok(quote! { diff --git a/godot-macros/src/godot_api.rs b/godot-macros/src/godot_api.rs index e1d146eb2..436e0bf13 100644 --- a/godot-macros/src/godot_api.rs +++ b/godot-macros/src/godot_api.rs @@ -11,7 +11,7 @@ use quote::quote; use venial::{AttributeValue, Declaration, Error, Function, Impl, ImplMember}; // Note: keep in sync with trait GodotExt -const VIRTUAL_METHOD_NAMES: [&'static str; 3] = ["ready", "process", "physics_process"]; +const VIRTUAL_METHOD_NAMES: [&str; 3] = ["ready", "process", "physics_process"]; pub fn transform(input: TokenStream) -> Result { let input_decl = venial::parse_declaration(input)?; @@ -126,7 +126,7 @@ fn process_godot_fns(decl: &mut Impl) -> Result<(Vec, Vec), Err continue; }; - if let Some(attr) = extract_attributes(&method)? { + if let Some(attr) = extract_attributes(method)? { // Remaining code no longer has attribute -- rest stays method.attributes.remove(attr.index); @@ -137,22 +137,22 @@ fn process_godot_fns(decl: &mut Impl) -> Result<(Vec, Vec), Err || method.qualifiers.tk_extern.is_some() || method.qualifiers.extern_abi.is_some() { - return attr.bail("fn qualifiers are not allowed", &method); + return attr.bail("fn qualifiers are not allowed", method); } if method.generic_params.is_some() { - return attr.bail("generic fn parameters are not supported", &method); + return attr.bail("generic fn parameters are not supported", method); } match attr.ty { BoundAttrType::Func(_attr) => { // Signatures are the same thing without body - let sig = util::reduce_to_signature(&method); + let sig = util::reduce_to_signature(method); func_signatures.push(sig); } BoundAttrType::Signal(ref _attr_val) => { if !method.params.is_empty() || method.return_ty.is_some() { - return attr.bail("parameters and return types not yet supported", &method); + return attr.bail("parameters and return types not yet supported", method); } signal_idents.push(method.name.clone()); diff --git a/godot-macros/src/itest.rs b/godot-macros/src/itest.rs index 1f7086528..570395177 100644 --- a/godot-macros/src/itest.rs +++ b/godot-macros/src/itest.rs @@ -24,7 +24,7 @@ pub fn transform(input: TokenStream) -> Result { || func.where_clause.is_some() { return bail( - &format!("#[itest] must be of form: fn {}() {{ ... }}", func.name), + format!("#[itest] must be of form: fn {}() {{ ... }}", func.name), &func, ); } diff --git a/godot-macros/src/lib.rs b/godot-macros/src/lib.rs index 9f9f634d0..8dbe00d04 100644 --- a/godot-macros/src/lib.rs +++ b/godot-macros/src/lib.rs @@ -31,22 +31,10 @@ pub fn itest(_meta: TokenStream, input: TokenStream) -> TokenStream { translate(input, itest::transform) } -/// Defines the global entry point for the GDExtension library. +/// Proc-macro attribute to be used in combination with the [`ExtensionLibrary`] trait. /// -/// Typical usage: -/// ``` -/// use godot::init::{gdextension, ExtensionLibrary}; -/// -/// // This is just a type tag without any functionality -/// struct MyExtension; -/// -/// #[gdextension] -/// unsafe impl ExtensionLibrary for MyExtension {} -/// ``` -/// -/// # Safety -/// By using godot-rust, you opt-in to the library's safety requirements (to be described in detail). -/// The library cannot enforce any guarantees outside Rust code, which means users need to adhere to certain rules for a safe usage. +/// [`ExtensionLibrary`]: crate::init::ExtensionLibrary +// FIXME intra-doc link #[proc_macro_attribute] pub fn gdextension(meta: TokenStream, input: TokenStream) -> TokenStream { translate_meta(meta, input, gdextension::transform) diff --git a/godot-macros/src/util.rs b/godot-macros/src/util.rs index 5073bd8bd..1f703d5fd 100644 --- a/godot-macros/src/util.rs +++ b/godot-macros/src/util.rs @@ -22,13 +22,6 @@ pub fn strlit(s: &str) -> Literal { Literal::string(s) } -pub fn bail_error(msg: impl AsRef, tokens: T) -> Error -where - T: Spanned, -{ - Error::new_at_span(tokens.__span(), msg.as_ref()) -} - pub fn bail(msg: impl AsRef, tokens: T) -> Result where T: Spanned, @@ -218,10 +211,10 @@ pub(crate) fn validate_impl( if let Some(expected_trait) = expected_trait { // impl Trait for Self -- validate Trait let trait_name = original_impl.trait_ty.as_ref().unwrap(); // unwrap: already checked outside - if !extract_typename(&trait_name).map_or(false, |seg| seg.ident == expected_trait) { + if !extract_typename(trait_name).map_or(false, |seg| seg.ident == expected_trait) { return bail( format!("#[{attr}] for trait impls requires trait to be `{expected_trait}`"), - &original_impl, + original_impl, ); } } @@ -233,13 +226,13 @@ pub(crate) fn validate_impl( } else { bail( format!("#[{attr}] for does currently not support generic arguments"), - &original_impl, + original_impl, ) } } else { bail( format!("#[{attr}] requires Self type to be a simple path"), - &original_impl, + original_impl, ) } } @@ -293,8 +286,6 @@ mod tests { let attr_value = &attrs[0].value; let mut parsed = parse_kv_group(attr_value).expect("parse"); - dbg!(&parsed); - for (key, value) in output_map { assert_eq!(parsed.remove(&key), Some(value)); } diff --git a/itest/rust/build.rs b/itest/rust/build.rs index fc6f3f47a..933ebbccc 100644 --- a/itest/rust/build.rs +++ b/itest/rust/build.rs @@ -124,7 +124,7 @@ struct Input { rust_val: TokenStream, } -fn generate_rust_methods(inputs: &Vec) -> Vec { +fn generate_rust_methods(inputs: &[Input]) -> Vec { inputs .iter() .map(|input| { @@ -174,9 +174,8 @@ fn write_gdscript_code( let mut last = 0; let ranges = find_repeated_ranges(&template); - dbg!(&ranges); for m in ranges { - file.write_all(&template[last..m.before_start].as_bytes())?; + file.write_all(template[last..m.before_start].as_bytes())?; replace_parts(&template[m.start..m.end], inputs, |replacement| { file.write_all(replacement.as_bytes())?; @@ -185,7 +184,7 @@ fn write_gdscript_code( last = m.after_end; } - file.write_all(&template[last..].as_bytes())?; + file.write_all(template[last..].as_bytes())?; Ok(()) } @@ -204,9 +203,9 @@ fn replace_parts( } = input; let replaced = repeat_part - .replace("IDENT", &ident) + .replace("IDENT", ident) .replace("TYPE", gdscript_ty) - .replace("VAL", &gdscript_val.to_string()); + .replace("VAL", gdscript_val.as_ref()); visitor(&replaced)?; } @@ -215,32 +214,28 @@ fn replace_parts( } fn find_repeated_ranges(entire: &str) -> Vec { - const START_PAT: &'static str = "#("; - const END_PAT: &'static str = "#)"; + const START_PAT: &str = "#("; + const END_PAT: &str = "#)"; let mut search_start = 0; let mut found = vec![]; - loop { - if let Some(start) = entire[search_start..].find(START_PAT) { - let before_start = search_start + start; - let start = before_start + START_PAT.len(); - if let Some(end) = entire[start..].find(END_PAT) { - let end = start + end; - let after_end = end + END_PAT.len(); - - println!("Found {start}..{end}"); - found.push(Match { - before_start, - start, - end, - after_end, - }); - search_start = after_end; - } else { - panic!("unmatched start pattern without end"); - } + while let Some(start) = entire[search_start..].find(START_PAT) { + let before_start = search_start + start; + let start = before_start + START_PAT.len(); + if let Some(end) = entire[start..].find(END_PAT) { + let end = start + end; + let after_end = end + END_PAT.len(); + + println!("Found {start}..{end}"); + found.push(Match { + before_start, + start, + end, + after_end, + }); + search_start = after_end; } else { - break; + panic!("unmatched start pattern without end"); } } diff --git a/itest/rust/src/export_test.rs b/itest/rust/src/export_test.rs index e3af0f727..1357b1356 100644 --- a/itest/rust/src/export_test.rs +++ b/itest/rust/src/export_test.rs @@ -7,10 +7,9 @@ use godot::prelude::*; pub(crate) fn run() -> bool { - let ok = true; // No tests currently, tests using HasProperty are in Godot scripts. - ok + true } #[derive(GodotClass)] @@ -42,7 +41,7 @@ struct HasProperty { impl HasProperty { #[func] pub fn get_int_val(&self) -> i32 { - return self.int_val; + self.int_val } #[func] @@ -52,7 +51,7 @@ impl HasProperty { #[func] pub fn get_string_val(&self) -> GodotString { - return self.string_val.clone(); + self.string_val.clone() } #[func] @@ -63,9 +62,9 @@ impl HasProperty { #[func] pub fn get_object_val(&self) -> Variant { if let Some(object_val) = self.object_val.as_ref() { - return object_val.to_variant(); + object_val.to_variant() } else { - return Variant::nil(); + Variant::nil() } } diff --git a/itest/rust/src/gdscript_ffi_test.rs b/itest/rust/src/gdscript_ffi_test.rs index 5961b7dc2..caf5bb3da 100644 --- a/itest/rust/src/gdscript_ffi_test.rs +++ b/itest/rust/src/gdscript_ffi_test.rs @@ -11,6 +11,5 @@ mod gen_ffi; pub(crate) fn run() -> bool { - let ok = true; - ok + true } diff --git a/itest/rust/src/lib.rs b/itest/rust/src/lib.rs index ca79593f7..00b62d55c 100644 --- a/itest/rust/src/lib.rs +++ b/itest/rust/src/lib.rs @@ -4,7 +4,7 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -#[all(cfg(test), not(cfg(gdext_clippy)))] +#[cfg(all(test, not(gdext_clippy)))] compile_error!("`cargo test` not supported for integration test -- use `cargo run`."); use godot::bind::{godot_api, GodotClass}; diff --git a/itest/rust/src/virtual_methods_test.rs b/itest/rust/src/virtual_methods_test.rs index 3ffbb1835..c51676ab3 100644 --- a/itest/rust/src/virtual_methods_test.rs +++ b/itest/rust/src/virtual_methods_test.rs @@ -54,5 +54,4 @@ pub(crate) fn run() -> bool { #[itest] fn test_to_string() { let _obj = Gd::::new_default(); - dbg!(_obj); } From 66a487afd1493186521191cc4b8fc77e3e465d28 Mon Sep 17 00:00:00 2001 From: Thomas ten Cate Date: Tue, 17 Jan 2023 12:29:30 +0100 Subject: [PATCH 08/54] Implement the basics of built-in vector types For Vector{2,3,4}{,i} this implements: - public fields - constructors - constants - operators - indexing by axis - (private) conversions to/from glam types - Display - a couple of functions like `abs()` and `length()` for demonstration See also #6. --- examples/dodge-the-creeps/rust/src/player.rs | 10 +- godot-core/src/builtin/mod.rs | 35 ++- godot-core/src/builtin/vector2.rs | 127 +++++---- godot-core/src/builtin/vector2i.rs | 105 +++++++ godot-core/src/builtin/vector3.rs | 119 +++++--- godot-core/src/builtin/vector3i.rs | 115 ++++++++ godot-core/src/builtin/vector4.rs | 105 ++++--- godot-core/src/builtin/vector4i.rs | 100 +++++++ godot-core/src/builtin/vector_macros.rs | 275 +++++++++++++++++++ 9 files changed, 866 insertions(+), 125 deletions(-) create mode 100644 godot-core/src/builtin/vector2i.rs create mode 100644 godot-core/src/builtin/vector3i.rs create mode 100644 godot-core/src/builtin/vector4i.rs create mode 100644 godot-core/src/builtin/vector_macros.rs diff --git a/examples/dodge-the-creeps/rust/src/player.rs b/examples/dodge-the-creeps/rust/src/player.rs index 0d8a1c563..f24265129 100644 --- a/examples/dodge-the-creeps/rust/src/player.rs +++ b/examples/dodge-the-creeps/rust/src/player.rs @@ -62,7 +62,7 @@ impl GodotExt for Player { .base .get_node_as::("AnimatedSprite2D"); - let mut velocity = Vector2::new(0.0, 0.0).inner(); + let mut velocity = Vector2::new(0.0, 0.0); // Note: exact=false by default, in Rust we have to provide it explicitly let input = Input::singleton(); @@ -80,7 +80,7 @@ impl GodotExt for Player { } if velocity.length() > 0.0 { - velocity = velocity.normalize() * self.speed; + velocity = velocity.normalized() * self.speed; let animation; @@ -101,10 +101,10 @@ impl GodotExt for Player { } let change = velocity * delta as f32; - let position = self.base.get_global_position().inner() + change; + let position = self.base.get_global_position() + change; let position = Vector2::new( - position.x.max(0.0).min(self.screen_size.inner().x), - position.y.max(0.0).min(self.screen_size.inner().y), + position.x.max(0.0).min(self.screen_size.x), + position.y.max(0.0).min(self.screen_size.y), ); self.base.set_global_position(position); } diff --git a/godot-core/src/builtin/mod.rs b/godot-core/src/builtin/mod.rs index 9f8c83c4a..56e3b8855 100644 --- a/godot-core/src/builtin/mod.rs +++ b/godot-core/src/builtin/mod.rs @@ -4,9 +4,36 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -//! Built-in types like `Vector2`, `GodotString` or `Variant`. +//! Built-in types like `Vector2`, `GodotString` and `Variant`. +//! +//! # Background on the design of vector algebra types +//! +//! The basic vector algebra types like `Vector2`, `Matrix4` and `Quaternion` are re-implemented +//! here, with an API similar to that in the Godot engine itself. There are other approaches, but +//! they all have their disadvantages: +//! +//! - We could invoke API methods from the engine. The implementations could be generated, but it +//! is slower and prevents inlining. +//! +//! - We could re-export types from an existing vector algebra crate, like `glam`. This removes the +//! duplication, but it would create a strong dependency on a volatile API outside our control. +//! The `gdnative` crate started out this way, using types from `euclid`, but [found it +//! impractical](https://github.com/godot-rust/gdnative/issues/594#issue-705061720). Moreover, +//! the API would not match Godot's own, which would make porting from GDScript (slightly) +//! harder. +//! +//! - We could opaquely wrap types from an existing vector algebra crate. This protects users of +//! `gdextension` from changes in the wrapped crate. However, direct field access using `.x`, +//! `.y`, `.z` is no longer possible. Instead of `v.y += a;` you would have to write +//! `v.set_y(v.get_y() + a);`. (A `union` could be used to add these fields in the public API, +//! but would make every field access unsafe, which is also not great.) +//! +//! - We could re-export types from the [`mint`](https://crates.io/crates/mint) crate, which was +//! explicitly designed to solve this problem. However, it falls short because [operator +//! overloading would become impossible](https://github.com/kvark/mint/issues/75). mod macros; +mod vector_macros; mod arrays; mod color; @@ -16,8 +43,11 @@ mod string; mod string_name; mod variant; mod vector2; +mod vector2i; mod vector3; +mod vector3i; mod vector4; +mod vector4i; pub mod meta; @@ -29,5 +59,8 @@ pub use string::*; pub use string_name::*; pub use variant::*; pub use vector2::*; +pub use vector2i::*; pub use vector3::*; +pub use vector3i::*; pub use vector4::*; +pub use vector4i::*; diff --git a/godot-core/src/builtin/vector2.rs b/godot-core/src/builtin/vector2.rs index 2e3eb6dc9..c9e559387 100644 --- a/godot-core/src/builtin/vector2.rs +++ b/godot-core/src/builtin/vector2.rs @@ -4,80 +4,111 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ +use std::fmt; + use godot_ffi as sys; use sys::{ffi_methods, GodotFfi}; -type Inner = glam::f32::Vec2; -//type Inner = glam::f64::DVec2; +use crate::builtin::Vector2i; -#[derive(Default, Copy, Clone, Debug, PartialEq)] +/// Vector used for 2D math using floating point coordinates. +/// +/// 2-element structure that can be used to represent positions in 2D space or any other pair of +/// numeric values. +/// +/// 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. +/// +/// See [`Vector2i`] for its integer counterpart. +#[derive(Debug, Default, Clone, Copy, PartialEq)] #[repr(C)] pub struct Vector2 { - inner: Inner, + /// The vector's X component. + pub x: f32, + /// The vector's Y component. + pub y: f32, } +impl_vector_operators!(Vector2, f32, (x, y)); +impl_vector_index!(Vector2, f32, (x, y), Vector2Axis, (X, Y)); +impl_common_vector_fns!(Vector2, f32); +impl_float_vector_fns!(Vector2, f32); + impl Vector2 { - pub fn new(x: f32, y: f32) -> Self { - Self { - inner: Inner::new(x, y), - } + /// Constructs a new `Vector2` from the given `x` and `y`. + pub const fn new(x: f32, y: f32) -> Self { + Self { x, y } } - pub fn from_inner(inner: Inner) -> Self { - Self { inner } + /// Constructs a new `Vector2` with all components set to `v`. + pub const fn splat(v: f32) -> Self { + Self { x: v, y: v } } - /// only for testing - pub fn inner(self) -> Inner { - self.inner + /// Constructs a new `Vector2` from a [`Vector2i`]. + pub const fn from_vector2i(v: Vector2i) -> Self { + Self { x: v.x as f32, y: v.y as f32 } } - // Hacks for example - // pub fn length(self) -> f32 { - // self.inner.length() - // } - // pub fn normalized(self) -> Vector2 { - // Self::from_inner(self.inner.normalize()) - // } - pub fn rotated(self, angle: f32) -> Self { - Self::from_inner(glam::Affine2::from_angle(angle).transform_vector2(self.inner)) - } -} + /// Zero vector, a vector with all components set to `0.0`. + pub const ZERO: Self = Self::splat(0.0); -impl GodotFfi for Vector2 { - ffi_methods! { type sys::GDExtensionTypePtr = *mut Self; .. } -} + /// One vector, a vector with all components set to `1.0`. + pub const ONE: Self = Self::splat(1.0); -impl std::fmt::Display for Vector2 { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - self.inner.fmt(f) - } -} + /// Infinity vector, a vector with all components set to `INFIINTY`. + pub const INF: Self = Self::splat(f32::INFINITY); -// ---------------------------------------------------------------------------------------------------------------------------------------------- + /// Left unit vector. Represents the direction of left. + pub const LEFT: Self = Self::new(-1.0, 0.0); -type IInner = glam::IVec2; + /// Right unit vector. Represents the direction of right. + pub const RIGHT: Self = Self::new(1.0, 0.0); -#[derive(Default, Copy, Clone, Debug, Eq, PartialEq)] -#[repr(C)] -pub struct Vector2i { - inner: IInner, + /// Up unit vector. Y is down in 2D, so this vector points -Y. + pub const UP: Self = Self::new(0.0, -1.0); + + /// Down unit vector. Y is down in 2D, so this vector points +Y. + pub const DOWN: Self = Self::new(0.0, 1.0); + + /// Returns the result of rotating this vector by `angle` (in radians). + pub fn rotated(self, angle: f32) -> Self { + Self::from_glam(glam::Affine2::from_angle(angle).transform_vector2(self.to_glam())) + } + + /// Converts the corresponding `glam` type to `Self`. + fn from_glam(v: glam::Vec2) -> Self { + Self::new(v.x, v.y) + } + + /// Converts `self` to the corresponding `glam` type. + fn to_glam(self) -> glam::Vec2 { + glam::Vec2::new(self.x, self.y) + } } -impl Vector2i { - pub fn new(x: i32, y: i32) -> Self { - Self { - inner: IInner::new(x, y), - } +/// Formats this vector in the same way the Godot engine would. +impl fmt::Display for Vector2 { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "({}, {})", self.x, self.y) } } -impl GodotFfi for Vector2i { +impl GodotFfi for Vector2 { ffi_methods! { type sys::GDExtensionTypePtr = *mut Self; .. } } -impl std::fmt::Display for Vector2i { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - self.inner.fmt(f) - } +/// Enumerates the axes in a [`Vector2`]. +#[derive(Copy, Clone, Debug, Eq, Ord, PartialEq, PartialOrd)] +#[repr(i32)] +pub enum Vector2Axis { + /// The X axis. + X, + /// The Y axis. + Y, +} + +impl GodotFfi for Vector2Axis { + ffi_methods! { type sys::GDExtensionTypePtr = *mut Self; .. } } diff --git a/godot-core/src/builtin/vector2i.rs b/godot-core/src/builtin/vector2i.rs new file mode 100644 index 000000000..ebce0bf4d --- /dev/null +++ b/godot-core/src/builtin/vector2i.rs @@ -0,0 +1,105 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +use std::fmt; + +use godot_ffi as sys; +use sys::{ffi_methods, GodotFfi}; + +use crate::builtin::Vector2; + +/// Vector used for 2D math using integer coordinates. +/// +/// 2-element structure that can be used to represent positions in 2D space or any other pair of +/// numeric values. +/// +/// It uses integer coordinates and is therefore preferable to [`Vector2`] when exact precision is +/// required. Note that the values are limited to 32 bits, and unlike [`Vector2`] this cannot be +/// configured with an engine build option. Use `i64` or [`PackedInt64Array`] if 64-bit values are +/// needed. +#[derive(Debug, Default, Clone, Copy, Eq, Ord, PartialEq, PartialOrd)] +#[repr(C)] +pub struct Vector2i { + /// The vector's X component. + pub x: i32, + /// The vector's Y component. + pub y: i32, +} + +impl_vector_operators!(Vector2i, i32, (x, y)); +impl_vector_index!(Vector2i, i32, (x, y), Vector2iAxis, (X, Y)); +impl_common_vector_fns!(Vector2i, i32); + +impl Vector2i { + /// Constructs a new `Vector2i` from the given `x` and `y`. + pub const fn new(x: i32, y: i32) -> Self { + Self { x, y } + } + + /// Constructs a new `Vector2i` with all components set to `v`. + pub const fn splat(v: i32) -> Self { + Self { x: v, y: v } + } + + /// Constructs a new `Vector2i` from a [`Vector2`]. The floating point coordinates will be + /// truncated. + pub const fn from_vector2(v: Vector2) -> Self { + Self { x: v.x as i32, y: v.y as i32 } + } + + /// Zero vector, a vector with all components set to `0`. + pub const ZERO: Self = Self::splat(0); + + /// One vector, a vector with all components set to `1`. + pub const ONE: Self = Self::splat(1); + + /// Left unit vector. Represents the direction of left. + pub const LEFT: Self = Self::new(-1, 0); + + /// Right unit vector. Represents the direction of right. + pub const RIGHT: Self = Self::new(1, 0); + + /// Up unit vector. Y is down in 2D, so this vector points -Y. + pub const UP: Self = Self::new(0, -1); + + /// Down unit vector. Y is down in 2D, so this vector points +Y. + pub const DOWN: Self = Self::new(0, 1); + + /// Converts the corresponding `glam` type to `Self`. + fn from_glam(v: glam::IVec2) -> Self { + Self::new(v.x, v.y) + } + + /// Converts `self` to the corresponding `glam` type. + fn to_glam(self) -> glam::IVec2 { + glam::IVec2::new(self.x, self.y) + } +} + +/// Formats this vector in the same way the Godot engine would. +impl fmt::Display for Vector2i { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "({}, {})", self.x, self.y) + } +} + +impl GodotFfi for Vector2i { + ffi_methods! { type sys::GDExtensionTypePtr = *mut Self; .. } +} + +/// Enumerates the axes in a [`Vector2i`]. +#[derive(Copy, Clone, Debug, Eq, Ord, PartialEq, PartialOrd)] +#[repr(i32)] +pub enum Vector2iAxis { + /// The X axis. + X, + /// The Y axis. + Y, +} + +impl GodotFfi for Vector2iAxis { + ffi_methods! { type sys::GDExtensionTypePtr = *mut Self; .. } +} diff --git a/godot-core/src/builtin/vector3.rs b/godot-core/src/builtin/vector3.rs index 0f2b94954..a01398281 100644 --- a/godot-core/src/builtin/vector3.rs +++ b/godot-core/src/builtin/vector3.rs @@ -4,75 +4,114 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ +use std::fmt; + use godot_ffi as sys; use sys::{ffi_methods, GodotFfi}; -type Inner = glam::f32::Vec3; -// type Inner = glam::f64::DVec3; - -#[derive(Default, Copy, Clone, Debug, PartialEq)] +use crate::builtin::Vector3i; + +/// Vector used for 3D math using floating point coordinates. +/// +/// 2-element structure that can be used to represent positions in 2D space or any other pair of +/// numeric values. +/// +/// 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. +/// +/// See [`Vector3i`] for its integer counterpart. +#[derive(Debug, Default, Clone, Copy, PartialEq)] #[repr(C)] pub struct Vector3 { - inner: Inner, + /// The vector's X component. + pub x: f32, + /// The vector's Y component. + pub y: f32, + /// The vector's Z component. + pub z: f32, } +impl_vector_operators!(Vector3, f32, (x, y, z)); +impl_vector_index!(Vector3, f32, (x, y, z), Vector3Axis, (X, Y, Z)); +impl_common_vector_fns!(Vector3, f32); +impl_float_vector_fns!(Vector3, f32); + impl Vector3 { - pub fn new(x: f32, y: f32, z: f32) -> Self { - Self { - inner: Inner::new(x, y, z), - } + /// Returns a `Vector3` with the given components. + pub const fn new(x: f32, y: f32, z: f32) -> Self { + Self { x, y, z } } -} -impl GodotFfi for Vector3 { - ffi_methods! { type sys::GDExtensionTypePtr = *mut Self; .. } -} + /// Returns a new `Vector3` with all components set to `v`. + pub const fn splat(v: f32) -> Self { + Self { x: v, y: v, z: v } + } -impl std::fmt::Display for Vector3 { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - //let Inner {x, y, z} = self.inner; - //write!(f, "({x}, {y}, {z})") - self.inner.fmt(f) + /// Constructs a new `Vector3` from a [`Vector3i`]. + pub const fn from_vector3i(v: Vector3i) -> Self { + Self { x: v.x as f32, y: v.y as f32, z: v.z as f32 } } -} -// ---------------------------------------------------------------------------------------------------------------------------------------------- + /// Zero vector, a vector with all components set to `0.0`. + pub const ZERO: Self = Self::splat(0.0); -type IInner = glam::IVec3; + /// One vector, a vector with all components set to `1.0`. + pub const ONE: Self = Self::splat(1.0); -#[derive(Default, Copy, Clone, Debug, Eq, PartialEq)] -#[repr(C)] -pub struct Vector3i { - inner: IInner, -} + /// Infinity vector, a vector with all components set to `INFIINTY`. + pub const INF: Self = Self::splat(f32::INFINITY); + + /// Left unit vector. Represents the local direction of left, and the global direction of west. + pub const LEFT: Self = Self::new(-1.0, 0.0, 0.0); -impl Vector3i { - pub fn new(x: i32, y: i32, z: i32) -> Self { - Self { - inner: IInner::new(x, y, z), - } + /// Right unit vector. Represents the local direction of right, and the global direction of east. + pub const RIGHT: Self = Self::new(1.0, 0.0, 0.0); + + /// Up unit vector. + pub const UP: Self = Self::new(0.0, -1.0, 0.0); + + /// Down unit vector. + pub const DOWN: Self = Self::new(0.0, 1.0, 0.0); + + /// Forward unit vector. Represents the local direction of forward, and the global direction of north. + pub const FORWARD: Self = Self::new(0.0, 0.0, -1.0); + + /// Back unit vector. Represents the local direction of back, and the global direction of south. + pub const BACK: Self = Self::new(0.0, 0.0, 1.0); + + /// Converts the corresponding `glam` type to `Self`. + fn from_glam(v: glam::Vec3) -> Self { + Self::new(v.x, v.y, v.z) } -} -impl GodotFfi for Vector3i { - ffi_methods! { type sys::GDExtensionTypePtr = *mut Self; .. } + /// Converts `self` to the corresponding `glam` type. + fn to_glam(self) -> glam::Vec3 { + glam::Vec3::new(self.x, self.y, self.z) + } } -impl std::fmt::Display for Vector3i { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - self.inner.fmt(f) +/// Formats this vector in the same way the Godot engine would. +impl fmt::Display for Vector3 { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "({}, {}, {})", self.x, self.y, self.z) } } -// ---------------------------------------------------------------------------------------------------------------------------------------------- +impl GodotFfi for Vector3 { + ffi_methods! { type sys::GDExtensionTypePtr = *mut Self; .. } +} +/// Enumerates the axes in a [`Vector3`]. // TODO auto-generate this, alongside all the other builtin type's enums - -#[derive(Copy, Clone, Debug, Eq, PartialEq)] +#[derive(Copy, Clone, Debug, Eq, Ord, PartialEq, PartialOrd)] #[repr(i32)] pub enum Vector3Axis { + /// The X axis. X, + /// The Y axis. Y, + /// The Z axis. Z, } diff --git a/godot-core/src/builtin/vector3i.rs b/godot-core/src/builtin/vector3i.rs new file mode 100644 index 000000000..808b645bf --- /dev/null +++ b/godot-core/src/builtin/vector3i.rs @@ -0,0 +1,115 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +use std::fmt; + +use godot_ffi as sys; +use sys::{ffi_methods, GodotFfi}; + +use crate::builtin::Vector3; + +/// Vector used for 3D math using integer coordinates. +/// +/// 3-element structure that can be used to represent positions in 3D space or any other pair of +/// numeric values. +/// +/// It uses integer coordinates and is therefore preferable to [`Vector3`] when exact precision is +/// required. Note that the values are limited to 32 bits, and unlike [`Vector3`] this cannot be +/// configured with an engine build option. Use `i64` or [`PackedInt64Array`] if 64-bit values are +/// needed. +#[derive(Debug, Default, Clone, Copy, Eq, Ord, PartialEq, PartialOrd)] +#[repr(C)] +pub struct Vector3i { + /// The vector's X component. + pub x: i32, + /// The vector's Y component. + pub y: i32, + /// The vector's Z component. + pub z: i32, +} + +impl_vector_operators!(Vector3i, i32, (x, y, z)); +impl_vector_index!(Vector3i, i32, (x, y, z), Vector3iAxis, (X, Y, Z)); +impl_common_vector_fns!(Vector3i, i32); + +impl Vector3i { + /// Returns a `Vector3i` with the given components. + pub const fn new(x: i32, y: i32, z: i32) -> Self { + Self { x, y, z } + } + + /// Constructs a new `Vector3i` with all components set to `v`. + pub const fn splat(v: i32) -> Self { + Self { x: v, y: v, z: v } + } + + /// Constructs a new `Vector3i` from a [`Vector3`]. The floating point coordinates will be + /// truncated. + pub const fn from_vector3(v: Vector3) -> Self { + Self { x: v.x as i32, y: v.y as i32, z: v.z as i32 } + } + + /// Zero vector, a vector with all components set to `0`. + pub const ZERO: Self = Self::splat(0); + + /// One vector, a vector with all components set to `1`. + pub const ONE: Self = Self::splat(1); + + /// Left unit vector. Represents the local direction of left, and the global direction of west. + pub const LEFT: Self = Self::new(-1, 0, 0); + + /// Right unit vector. Represents the local direction of right, and the global direction of east. + pub const RIGHT: Self = Self::new(1, 0, 0); + + /// Up unit vector. + pub const UP: Self = Self::new(0, -1, 0); + + /// Down unit vector. + pub const DOWN: Self = Self::new(0, 1, 0); + + /// Forward unit vector. Represents the local direction of forward, and the global direction of north. + pub const FORWARD: Self = Self::new(0, 0, -1); + + /// Back unit vector. Represents the local direction of back, and the global direction of south. + pub const BACK: Self = Self::new(0, 0, 1); + + /// Converts the corresponding `glam` type to `Self`. + fn from_glam(v: glam::IVec3) -> Self { + Self::new(v.x, v.y, v.z) + } + + /// Converts `self` to the corresponding `glam` type. + fn to_glam(self) -> glam::IVec3 { + glam::IVec3::new(self.x, self.y, self.z) + } +} + +/// Formats this vector in the same way the Godot engine would. +impl fmt::Display for Vector3i { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "({}, {}, {})", self.x, self.y, self.z) + } +} + +impl GodotFfi for Vector3i { + ffi_methods! { type sys::GDExtensionTypePtr = *mut Self; .. } +} + +/// Enumerates the axes in a [`Vector3i`]. +#[derive(Copy, Clone, Debug, Eq, Ord, PartialEq, PartialOrd)] +#[repr(i32)] +pub enum Vector3iAxis { + /// The X axis. + X, + /// The Y axis. + Y, + /// The Z axis. + Z, +} + +impl GodotFfi for Vector3iAxis { + ffi_methods! { type sys::GDExtensionTypePtr = *mut Self; .. } +} diff --git a/godot-core/src/builtin/vector4.rs b/godot-core/src/builtin/vector4.rs index 22e856ff8..73c365ba7 100644 --- a/godot-core/src/builtin/vector4.rs +++ b/godot-core/src/builtin/vector4.rs @@ -4,58 +4,101 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ +use std::fmt; + use godot_ffi as sys; use sys::{ffi_methods, GodotFfi}; -type Inner = glam::f32::Vec4; -//type Inner = glam::f64::DVec4; +use crate::builtin::Vector4i; -#[derive(Default, Copy, Clone, Debug, PartialEq)] +/// Vector used for 4D math using floating point coordinates. +/// +/// 4-element structure that can be used to represent any quadruplet of numeric values. +/// +/// 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. +/// +/// See [`Vector4i`] for its integer counterpart. +#[derive(Debug, Default, Clone, Copy, PartialEq)] #[repr(C)] pub struct Vector4 { - inner: Inner, + /// The vector's X component. + pub x: f32, + /// The vector's Y component. + pub y: f32, + /// The vector's Z component. + pub z: f32, + /// The vector's W component. + pub w: f32, } +impl_vector_operators!(Vector4, f32, (x, y, z, w)); +impl_vector_index!(Vector4, f32, (x, y, z, w), Vector4Axis, (X, Y, Z, W)); +impl_common_vector_fns!(Vector4, f32); +impl_float_vector_fns!(Vector4, f32); + impl Vector4 { - pub fn new(x: f32, y: f32, z: f32, w: f32) -> Self { - Self { - inner: Inner::new(x, y, z, w), - } + /// Returns a `Vector4` with the given components. + pub const fn new(x: f32, y: f32, z: f32, w: f32) -> Self { + Self { x, y, z, w } } -} -impl GodotFfi for Vector4 { - ffi_methods! { type sys::GDExtensionTypePtr = *mut Self; .. } -} + /// Returns a new `Vector4` with all components set to `v`. + pub const fn splat(v: f32) -> Self { + Self { x: v, y: v, z: v, w: v } + } -impl std::fmt::Display for Vector4 { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - self.inner.fmt(f) + /// Constructs a new `Vector3` from a [`Vector3i`]. + pub const fn from_vector4i(v: Vector4i) -> Self { + Self { x: v.x as f32, y: v.y as f32, z: v.z as f32, w: v.w as f32 } } -} -type IInner = glam::IVec4; + /// Zero vector, a vector with all components set to `0.0`. + pub const ZERO: Self = Self::splat(0.0); -#[derive(Default, Copy, Clone, Debug)] -#[repr(C)] -pub struct Vector4i { - inner: IInner, + /// One vector, a vector with all components set to `1.0`. + pub const ONE: Self = Self::splat(1.0); + + /// Infinity vector, a vector with all components set to `INFIINTY`. + pub const INF: Self = Self::splat(f32::INFINITY); + + /// Converts the corresponding `glam` type to `Self`. + fn from_glam(v: glam::Vec4) -> Self { + Self::new(v.x, v.y, v.z, v.w) + } + + /// Converts `self` to the corresponding `glam` type. + fn to_glam(self) -> glam::Vec4 { + glam::Vec4::new(self.x, self.y, self.z, self.w) + } } -impl Vector4i { - pub fn new(x: i32, y: i32, z: i32, w: i32) -> Self { - Self { - inner: IInner::new(x, y, z, w), - } +/// Formats this vector in the same way the Godot engine would. +impl fmt::Display for Vector4 { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "({}, {}, {}, {})", self.x, self.y, self.z, self.w) } } -impl GodotFfi for Vector4i { +impl GodotFfi for Vector4 { ffi_methods! { type sys::GDExtensionTypePtr = *mut Self; .. } } -impl std::fmt::Display for Vector4i { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - self.inner.fmt(f) - } +/// Enumerates the axes in a [`Vector4`]. +#[derive(Copy, Clone, Debug, Eq, Ord, PartialEq, PartialOrd)] +#[repr(i32)] +pub enum Vector4Axis { + /// The X axis. + X, + /// The Y axis. + Y, + /// The Z axis. + Z, + /// The W axis. + W, +} + +impl GodotFfi for Vector4Axis { + ffi_methods! { type sys::GDExtensionTypePtr = *mut Self; .. } } diff --git a/godot-core/src/builtin/vector4i.rs b/godot-core/src/builtin/vector4i.rs new file mode 100644 index 000000000..33937d848 --- /dev/null +++ b/godot-core/src/builtin/vector4i.rs @@ -0,0 +1,100 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +use std::fmt; + +use godot_ffi as sys; +use sys::{ffi_methods, GodotFfi}; + +use crate::builtin::Vector4; + +/// Vector used for 4D math using integer coordinates. +/// +/// 4-element structure that can be used to represent 4D grid coordinates or sets of integers. +/// +/// It uses integer coordinates and is therefore preferable to [`Vector4`] when exact precision is +/// required. Note that the values are limited to 32 bits, and unlike [`Vector4`] this cannot be +/// configured with an engine build option. Use `i64` or [`PackedInt64Array`] if 64-bit values are +/// needed. +#[derive(Debug, Default, Clone, Copy, Eq, Ord, PartialEq, PartialOrd)] +#[repr(C)] +pub struct Vector4i { + /// The vector's X component. + pub x: i32, + /// The vector's Y component. + pub y: i32, + /// The vector's Z component. + pub z: i32, + /// The vector's W component. + pub w: i32, +} + +impl_vector_operators!(Vector4i, i32, (x, y, z, w)); +impl_vector_index!(Vector4i, i32, (x, y, z, w), Vector4iAxis, (X, Y, Z, W)); +impl_common_vector_fns!(Vector4i, i32); + +impl Vector4i { + /// Returns a `Vector4i` with the given components. + pub const fn new(x: i32, y: i32, z: i32, w: i32) -> Self { + Self { x, y, z, w } + } + + /// Constructs a new `Vector4i` with all components set to `v`. + pub const fn splat(v: i32) -> Self { + Self { x: v, y: v, z: v, w: v } + } + + /// Constructs a new `Vector4i` from a [`Vector4`]. The floating point coordinates will be + /// truncated. + pub const fn from_vector3(v: Vector4) -> Self { + Self { x: v.x as i32, y: v.y as i32, z: v.z as i32, w: v.w as i32 } + } + + /// Zero vector, a vector with all components set to `0`. + pub const ZERO: Self = Self::splat(0); + + /// One vector, a vector with all components set to `1`. + pub const ONE: Self = Self::splat(1); + + /// Converts the corresponding `glam` type to `Self`. + fn from_glam(v: glam::IVec4) -> Self { + Self::new(v.x, v.y, v.z, v.w) + } + + /// Converts `self` to the corresponding `glam` type. + fn to_glam(self) -> glam::IVec4 { + glam::IVec4::new(self.x, self.y, self.z, self.w) + } +} + +/// Formats this vector in the same way the Godot engine would. +impl fmt::Display for Vector4i { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "({}, {}, {}, {})", self.x, self.y, self.z, self.w) + } +} + +impl GodotFfi for Vector4i { + ffi_methods! { type sys::GDExtensionTypePtr = *mut Self; .. } +} + +/// Enumerates the axes in a [`Vector4i`]. +#[derive(Copy, Clone, Debug, Eq, Ord, PartialEq, PartialOrd)] +#[repr(i32)] +pub enum Vector4iAxis { + /// The X axis. + X, + /// The Y axis. + Y, + /// The Z axis. + Z, + /// The W axis. + W, +} + +impl GodotFfi for Vector4iAxis { + ffi_methods! { type sys::GDExtensionTypePtr = *mut Self; .. } +} diff --git a/godot-core/src/builtin/vector_macros.rs b/godot-core/src/builtin/vector_macros.rs new file mode 100644 index 000000000..a1de527af --- /dev/null +++ b/godot-core/src/builtin/vector_macros.rs @@ -0,0 +1,275 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +#![macro_use] + +/// Implements a single unary operator for a vector type. Only used for `Neg` at the moment. +macro_rules! impl_vector_unary_operator { + ( + // Name of the vector type. + $Vector:ty, + // Type of each individual component, for example `i32`. + $Scalar:ty, + // Names of the components, with parentheses, for example `(x, y)`. + ($($components:ident),*), + // Name of the operator trait, for example `Neg`. + $Operator:ident, + // Name of the function on the operator trait, for example `neg`. + $func:ident$(,)? + ) => { + impl std::ops::$Operator for $Vector { + type Output = Self; + fn $func(mut self) -> Self::Output { + $( + self.$components = self.$components.$func(); + )* + self + } + } + } +} + +/// Implements a component-wise single infix binary operator between two vectors. +macro_rules! impl_vector_vector_binary_operator { + ( + // Name of the vector type. + $Vector:ty, + // Type of each individual component, for example `i32`. + $Scalar:ty, + // Names of the components, with parentheses, for example `(x, y)`. + ($($components:ident),*), + // Name of the operator trait, for example `Add`. + $Operator:ident, + // Name of the function on the operator trait, for example `add`. + $func:ident$(,)? + ) => { + impl std::ops::$Operator for $Vector { + type Output = Self; + fn $func(mut self, rhs: $Vector) -> Self::Output { + $( + self.$components = self.$components.$func(rhs.$components); + )* + self + } + } + } +} + +/// Implements a component-wise single infix binary operator between a vector on the left and a +/// scalar on the right-hand side. +macro_rules! impl_vector_scalar_binary_operator { + ( + // Name of the vector type. + $Vector:ty, + // Type of each individual component, for example `i32`. + $Scalar:ty, + // Names of the components, with parentheses, for example `(x, y)`. + ($($components:ident),*), + // Name of the operator trait, for example `Add`. + $Operator:ident, + // Name of the function on the operator trait, for example `add`. + $func:ident$(,)? + ) => { + impl std::ops::$Operator<$Scalar> for $Vector { + type Output = Self; + fn $func(mut self, rhs: $Scalar) -> Self::Output { + $( + self.$components = self.$components.$func(rhs); + )* + self + } + } + } +} + +/// Implements a component-wise single infix binary operator between a scalar on the left and a +/// vector on the right-hand side. +macro_rules! impl_scalar_vector_binary_operator { + ( + // Name of the vector type. + $Vector:ty, + // Type of each individual component, for example `i32`. + $Scalar:ty, + // Names of the components, with parentheses, for example `(x, y)`. + ($($components:ident),*), + // Name of the operator trait, for example `Add`. + $Operator:ident, + // Name of the function on the operator trait, for example `add`. + $func:ident$(,)? + ) => { + impl std::ops::$Operator<$Vector> for $Scalar { + type Output = $Vector; + fn $func(self, mut rhs: $Vector) -> Self::Output { + $( + rhs.$components = rhs.$components.$func(self); + )* + rhs + } + } + } +} + +/// Implements a single arithmetic assignment operator for a vector type, with a vector on the +/// right-hand side. +macro_rules! impl_vector_vector_assign_operator { + ( + // Name of the vector type. + $Vector:ty, + // Type of each individual component, for example `i32`. + $Scalar:ty, + // Names of the components, with parentheses, for example `(x, y)`. + ($($components:ident),*), + // Name of the operator trait, for example `AddAssign`. + $Operator:ident, + // Name of the function on the operator trait, for example `add_assign`. + $func:ident$(,)? + ) => { + impl std::ops::$Operator for $Vector { + fn $func(&mut self, rhs: $Vector) { + $( + self.$components.$func(rhs.$components); + )* + } + } + } +} + +/// Implements a single arithmetic assignment operator for a vector type, with a scalar on the +/// right-hand side. +macro_rules! impl_vector_scalar_assign_operator { + ( + // Name of the vector type. + $Vector:ty, + // Type of each individual component, for example `i32`. + $Scalar:ty, + // Names of the components, with parentheses, for example `(x, y)`. + ($($components:ident),*), + // Name of the operator trait, for example `AddAssign`. + $Operator:ident, + // Name of the function on the operator trait, for example `add_assign`. + $func:ident$(,)? + ) => { + impl std::ops::$Operator<$Scalar> for $Vector { + fn $func(&mut self, rhs: $Scalar) { + $( + self.$components.$func(rhs); + )* + } + } + } +} + +/// Implements all common arithmetic operators on a built-in vector type. +macro_rules! impl_vector_operators { + ( + // Name of the vector type to be implemented, for example `Vector2`. + $Vector:ty, + // Type of each individual component, for example `f32`. + $Scalar:ty, + // Names of the components, with parentheses, for example `(x, y)`. + ($($components:ident),*)$(,)? + ) => { + impl_vector_unary_operator!($Vector, $Scalar, ($($components),*), Neg, neg); + impl_vector_vector_binary_operator!($Vector, $Scalar, ($($components),*), Add, add); + impl_vector_vector_binary_operator!($Vector, $Scalar, ($($components),*), Sub, sub); + impl_vector_vector_binary_operator!($Vector, $Scalar, ($($components),*), Mul, mul); + impl_vector_scalar_binary_operator!($Vector, $Scalar, ($($components),*), Mul, mul); + impl_scalar_vector_binary_operator!($Vector, $Scalar, ($($components),*), Mul, mul); + impl_vector_vector_binary_operator!($Vector, $Scalar, ($($components),*), Div, div); + impl_vector_scalar_binary_operator!($Vector, $Scalar, ($($components),*), Div, div); + impl_vector_vector_binary_operator!($Vector, $Scalar, ($($components),*), Rem, rem); + impl_vector_scalar_binary_operator!($Vector, $Scalar, ($($components),*), Rem, rem); + impl_vector_vector_assign_operator!($Vector, $Scalar, ($($components),*), AddAssign, add_assign); + impl_vector_vector_assign_operator!($Vector, $Scalar, ($($components),*), SubAssign, sub_assign); + impl_vector_vector_assign_operator!($Vector, $Scalar, ($($components),*), MulAssign, mul_assign); + impl_vector_scalar_assign_operator!($Vector, $Scalar, ($($components),*), MulAssign, mul_assign); + impl_vector_vector_assign_operator!($Vector, $Scalar, ($($components),*), DivAssign, div_assign); + impl_vector_scalar_assign_operator!($Vector, $Scalar, ($($components),*), DivAssign, div_assign); + impl_vector_vector_assign_operator!($Vector, $Scalar, ($($components),*), RemAssign, rem_assign); + impl_vector_scalar_assign_operator!($Vector, $Scalar, ($($components),*), RemAssign, rem_assign); + } +} + +/// Implements `Index` and `IndexMut` for a vector type, using an enum to indicate the desired axis. +macro_rules! impl_vector_index { + ( + // Name of the vector type to be implemented, for example `Vector2`. + $Vector:ty, + // Type of each individual component, for example `f32`. + $Scalar:ty, + // Names of the components, with parentheses, for example `(x, y)`. + ($($components:ident),*), + // Name of the enum type for the axes, for example `Vector2Axis`. + $AxisEnum:ty, + // Names of the enum variants, with parenthes, for example `(X, Y)`. + ($($AxisVariants:ident),*)$(,)? + ) => { + impl std::ops::Index<$AxisEnum> for $Vector { + type Output = $Scalar; + fn index(&self, axis: $AxisEnum) -> &$Scalar { + match axis { + $(<$AxisEnum>::$AxisVariants => &self.$components),* + } + } + } + + impl std::ops::IndexMut<$AxisEnum> for $Vector { + fn index_mut(&mut self, axis: $AxisEnum) -> &mut $Scalar { + match axis { + $(<$AxisEnum>::$AxisVariants => &mut self.$components),* + } + } + } + } +} + +/// Implements functions on vector types which make sense for both floating-point and integer +/// vectors. +macro_rules! impl_common_vector_fns { + ( + // Name of the vector type. + $Vector:ty, + // Type of target component, for example `f32`. + $Scalar:ty + ) => { + impl $Vector { + /// Returns a new vector with all components in absolute values (i.e. positive or + /// zero). + #[inline] + pub fn abs(self) -> Self { + Self::from_glam(self.to_glam().abs()) + } + } + } +} + +/// Implements common constants and methods for floating-point type vectors. Works for any vector +/// type that has `to_glam` and `from_glam` functions. +macro_rules! impl_float_vector_fns { + ( + // Name of the vector type. + $Vector:ty, + // Type of target component, for example `f32`. + $Scalar:ty + ) => { + impl $Vector { + /// Returns the length (magnitude) of this vector. + #[inline] + pub fn length(self) -> $Scalar { + self.to_glam().length() + } + + /// Returns the vector scaled to unit length. Equivalent to `self / self.length()`. See + /// also `is_normalized()`. + /// + /// If the vector is zero, the result is also zero. + #[inline] + pub fn normalized(self) -> Self { + Self::from_glam(self.to_glam().normalize_or_zero()) + } + } + } +} From 4eb97f92b86728d7a40f1721fb128a649cf05eff Mon Sep 17 00:00:00 2001 From: Jan Haller Date: Thu, 19 Jan 2023 15:25:02 +0100 Subject: [PATCH 09/54] Fix static GDScript errors introduced by stricter checking Since the following Godot commit, some runtime errors were elevated to parse errors. This caused the CI to fail in code that was unreachable at runtime. https://github.com/godotengine/godot/commit/def592114f64262d7323644b92648d83ae8f6a51 --- itest/godot/ManualFfiTests.gd | 18 +++++++++++++++--- itest/godot/project.godot | 2 +- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/itest/godot/ManualFfiTests.gd b/itest/godot/ManualFfiTests.gd index e9d87e40b..bb37bff33 100644 --- a/itest/godot/ManualFfiTests.gd +++ b/itest/godot/ManualFfiTests.gd @@ -7,7 +7,7 @@ extends Node func run() -> bool: print("[GD] Test ManualFfi...") var ok = true - #ok = ok && test_missing_init() + ok = ok && test_missing_init() ok = ok && test_to_string() ok = ok && test_export() @@ -15,8 +15,20 @@ func run() -> bool: return ok func test_missing_init() -> bool: - var obj = WithoutInit.new() - print("[GD] WithoutInit is: ", obj) + return true # TODO: fix dynamic eval + + var expr = Expression.new() + var error = expr.parse("WithoutInit.new()") + if error != OK: + print("Failed to parse dynamic expression") + return false + + var instance = expr.execute() + if expr.has_execute_failed(): + print("Failed to evaluate dynamic expression") + return false + + print("[GD] WithoutInit is: ", instance) return true func test_to_string() -> bool: diff --git a/itest/godot/project.godot b/itest/godot/project.godot index e55e3e69a..d2c4c01ed 100644 --- a/itest/godot/project.godot +++ b/itest/godot/project.godot @@ -12,4 +12,4 @@ config_version=5 config/name="IntegrationTests" run/main_scene="res://TestRunner.tscn" -config/features=PackedStringArray("4.0", "Vulkan Clustered") +config/features=PackedStringArray("4.0") From dce60d36bdd442a4f53afcb2b05d7cfafe33ae7a Mon Sep 17 00:00:00 2001 From: Jan Haller Date: Thu, 19 Jan 2023 17:19:28 +0100 Subject: [PATCH 10/54] Add Clippy to CI (as Godot integration test for now) --- .github/workflows/full-ci.yml | 40 +++++++++++++++++++++++++++++ .github/workflows/minimal-ci.yml | 43 +++++++++++++++++++++++++++++--- 2 files changed, 79 insertions(+), 4 deletions(-) diff --git a/.github/workflows/full-ci.yml b/.github/workflows/full-ci.yml index d9e4511eb..8edd180e0 100644 --- a/.github/workflows/full-ci.yml +++ b/.github/workflows/full-ci.yml @@ -31,15 +31,54 @@ jobs: runs-on: ubuntu-20.04 steps: - uses: actions/checkout@v3 + - name: "Install Rust" uses: ./.github/composite/rust with: rust: stable components: rustfmt + - name: "Check rustfmt" run: cargo fmt --all -- --check + clippy: + runs-on: ubuntu-20.04 + steps: + - uses: actions/checkout@v3 + + - name: "Install Rust" + uses: ./.github/composite/rust + + # TODO get rid of Godot binary, once the JSON is either versioned or fetched from somewhere + # Replaces also backspaces on Windows, since they cause problems in Bash + - name: "Store variable to Godot binary" + run: | + runnerDir=$(echo "${{ runner.temp }}" | sed "s!\\\\!/!") + echo "RUNNER_DIR=$runnerDir" >> $GITHUB_ENV + echo "GODOT4_BIN=$runnerDir/godot_bin/godot.linuxbsd.editor.dev.x86_64" >> $GITHUB_ENV + + # - name: "Check cache for installed Godot version" + # id: "cache-godot" + # uses: actions/cache@v3 + # with: + # path: ${{ runner.temp }}/godot_bin + # key: ${{ inputs.artifact-name }}-v${{ inputs.godot-ver }} + + - name: "Download Godot artifact" + # if: steps.cache-godot.outputs.cache-hit != 'true' + run: | + curl https://nightly.link/Bromeon/godot4-nightly/workflows/compile-godot/master/godot-linux.zip -Lo artifact.zip + unzip artifact.zip -d $RUNNER_DIR/godot_bin + + - name: "Prepare Godot executable" + run: | + chmod +x $GODOT4_BIN + + - name: "Check clippy" + run: cargo clippy --features $GDEXT_FEATURES $GDEXT_CRATE_ARGS -- --cfg gdext_clippy -D clippy::style -D clippy::complexity -D clippy::perf -D clippy::dbg_macro -D clippy::todo -D clippy::unimplemented + + unit-test: name: unit-test (${{ matrix.name }}) runs-on: ${{ matrix.os }} @@ -161,6 +200,7 @@ jobs: if: github.event_name == 'push' && success() needs: - rustfmt + - clippy - unit-test - itest-godot - license-guard diff --git a/.github/workflows/minimal-ci.yml b/.github/workflows/minimal-ci.yml index ae869eb5e..6334d966b 100644 --- a/.github/workflows/minimal-ci.yml +++ b/.github/workflows/minimal-ci.yml @@ -31,15 +31,54 @@ jobs: runs-on: ubuntu-20.04 steps: - uses: actions/checkout@v3 + - name: "Install Rust" uses: ./.github/composite/rust with: rust: stable components: rustfmt + - name: "Check rustfmt" run: cargo fmt --all -- --check + clippy: + runs-on: ubuntu-20.04 + steps: + - uses: actions/checkout@v3 + + - name: "Install Rust" + uses: ./.github/composite/rust + + # TODO get rid of Godot binary, once the JSON is either versioned or fetched from somewhere + # Replaces also backspaces on Windows, since they cause problems in Bash + - name: "Store variable to Godot binary" + run: | + runnerDir=$(echo "${{ runner.temp }}" | sed "s!\\\\!/!") + echo "RUNNER_DIR=$runnerDir" >> $GITHUB_ENV + echo "GODOT4_BIN=$runnerDir/godot_bin/godot.linuxbsd.editor.dev.x86_64" >> $GITHUB_ENV + +# - name: "Check cache for installed Godot version" +# id: "cache-godot" +# uses: actions/cache@v3 +# with: +# path: ${{ runner.temp }}/godot_bin +# key: ${{ inputs.artifact-name }}-v${{ inputs.godot-ver }} + + - name: "Download Godot artifact" +# if: steps.cache-godot.outputs.cache-hit != 'true' + run: | + curl https://nightly.link/Bromeon/godot4-nightly/workflows/compile-godot/master/godot-linux.zip -Lo artifact.zip + unzip artifact.zip -d $RUNNER_DIR/godot_bin + + - name: "Prepare Godot executable" + run: | + chmod +x $GODOT4_BIN + + - name: "Check clippy" + run: cargo clippy --features $GDEXT_FEATURES $GDEXT_CRATE_ARGS -- --cfg gdext_clippy -D clippy::style -D clippy::complexity -D clippy::perf -D clippy::dbg_macro -D clippy::todo -D clippy::unimplemented + + unit-test: name: unit-test runs-on: ubuntu-20.04 @@ -49,10 +88,6 @@ jobs: - name: "Install Rust" uses: ./.github/composite/rust - - name: "Install LLVM" - uses: ./.github/composite/llvm - if: matrix.name == 'macos' - - name: "Compile tests" run: cargo test $GDEXT_CRATE_ARGS --features unit-test,$GDEXT_FEATURES --no-run From 7ef9c759c459edd0688278600b0ce7535327c97f Mon Sep 17 00:00:00 2001 From: Jan Haller Date: Thu, 19 Jan 2023 17:29:16 +0100 Subject: [PATCH 11/54] Replace `--feature unit-test` with `--cfg test` This is not a feature that users should specify. Using `--cfg gdext_test` makes code slightly more readable and consistent with `--cfg gdext_clippy`. For some buggy reason, during doctest, the --cfg flag is not always considered, leading to monstrosities such as #[cfg(not(any(gdext_test, doctest)))]. --- .github/workflows/full-ci.yml | 8 ++++++-- .github/workflows/minimal-ci.yml | 8 ++++++-- godot-core/Cargo.toml | 4 +--- godot-core/build.rs | 2 +- godot-core/src/builtin/others.rs | 2 +- godot-core/src/init/mod.rs | 4 ++-- godot-core/src/lib.rs | 18 ++++++++++-------- godot-ffi/Cargo.toml | 1 - godot-ffi/build.rs | 2 +- godot-ffi/src/lib.rs | 16 ++++++++-------- godot-macros/Cargo.toml | 3 ++- godot/Cargo.toml | 4 ---- godot/src/lib.rs | 4 ++-- 13 files changed, 40 insertions(+), 36 deletions(-) diff --git a/.github/workflows/full-ci.yml b/.github/workflows/full-ci.yml index 8edd180e0..22ca4e7cc 100644 --- a/.github/workflows/full-ci.yml +++ b/.github/workflows/full-ci.yml @@ -134,10 +134,14 @@ jobs: if: matrix.name == 'macos' - name: "Compile tests" - run: cargo test $GDEXT_CRATE_ARGS --features unit-test,$GDEXT_FEATURES --no-run + run: cargo test $GDEXT_CRATE_ARGS --features $GDEXT_FEATURES --no-run + env: + RUSTFLAGS: --cfg=gdext_test - name: "Test" - run: cargo test $GDEXT_CRATE_ARGS --features unit-test,$GDEXT_FEATURES ${{ matrix.testflags }} + run: cargo test $GDEXT_CRATE_ARGS --features $GDEXT_FEATURES + env: + RUSTFLAGS: --cfg=gdext_test itest-godot: diff --git a/.github/workflows/minimal-ci.yml b/.github/workflows/minimal-ci.yml index 6334d966b..36234c016 100644 --- a/.github/workflows/minimal-ci.yml +++ b/.github/workflows/minimal-ci.yml @@ -89,10 +89,14 @@ jobs: uses: ./.github/composite/rust - name: "Compile tests" - run: cargo test $GDEXT_CRATE_ARGS --features unit-test,$GDEXT_FEATURES --no-run + run: cargo test $GDEXT_CRATE_ARGS --features $GDEXT_FEATURES --no-run + env: + RUSTFLAGS: --cfg=gdext_test - name: "Test" - run: cargo test $GDEXT_CRATE_ARGS --features unit-test,$GDEXT_FEATURES ${{ matrix.testflags }} + run: cargo test $GDEXT_CRATE_ARGS --features $GDEXT_FEATURES + env: + RUSTFLAGS: --cfg=gdext_test itest-godot: diff --git a/godot-core/Cargo.toml b/godot-core/Cargo.toml index d41ef31af..07fb887d2 100644 --- a/godot-core/Cargo.toml +++ b/godot-core/Cargo.toml @@ -13,7 +13,6 @@ trace = [] codegen-fmt = ["godot-ffi/codegen-fmt"] codegen-full = ["godot-codegen/codegen-full"] convenience = [] -unit-test = ["godot-ffi/unit-test"] # If this crate is built for a downstream unit test [dependencies] godot-ffi = { path = "../godot-ffi" } @@ -24,8 +23,7 @@ glam = { version = "0.22", features = ["debug-glam-assert", "scalar-math"] } # Reverse dev dependencies so doctests can use `godot::` prefix [dev-dependencies] -godot = { path = "../godot" } # when re-enabling, add unit-test feature to `godot`, or it will mess things up -#godot-ffi = { path = "../godot-ffi", features = ["unit-test"] } # unit-test +godot = { path = "../godot" } [build-dependencies] godot-codegen = { path = "../godot-codegen" } diff --git a/godot-core/build.rs b/godot-core/build.rs index 3660c0a14..037c23c39 100644 --- a/godot-core/build.rs +++ b/godot-core/build.rs @@ -15,6 +15,6 @@ fn main() { // Note: cannot use cfg!(test) because that isn't recognizable from build files. // See https://github.com/rust-lang/cargo/issues/1581, which was closed without a solution. - let stubs_only = cfg!(feature = "unit-test"); + let stubs_only = cfg!(gdext_test); godot_codegen::generate_core_files(gen_path, stubs_only); } diff --git a/godot-core/src/builtin/others.rs b/godot-core/src/builtin/others.rs index 5e54533b2..2aff9fa1c 100644 --- a/godot-core/src/builtin/others.rs +++ b/godot-core/src/builtin/others.rs @@ -33,7 +33,7 @@ struct InnerRect { size: Vector2, } -#[cfg(not(feature = "unit-test"))] +#[cfg(not(gdext_test))] impl Rect2 { pub fn size(self) -> Vector2 { self.inner().size diff --git a/godot-core/src/init/mod.rs b/godot-core/src/init/mod.rs index a5e7b3b02..718600816 100644 --- a/godot-core/src/init/mod.rs +++ b/godot-core/src/init/mod.rs @@ -7,7 +7,7 @@ use godot_ffi as sys; use std::collections::btree_map::BTreeMap; -#[cfg(feature = "unit-test")] +#[cfg(gdext_test)] pub fn __gdext_load_library( interface: *const sys::GDExtensionInterface, library: sys::GDExtensionClassLibraryPtr, @@ -16,7 +16,7 @@ pub fn __gdext_load_library( sys::panic_no_godot!(__gdext_load_library) } -#[cfg(not(feature = "unit-test"))] +#[cfg(not(gdext_test))] #[doc(hidden)] // TODO consider body safe despite unsafe function, and explicitly mark unsafe {} locations pub unsafe fn __gdext_load_library( diff --git a/godot-core/src/lib.rs b/godot-core/src/lib.rs index fccfe9af2..7e25ad54c 100644 --- a/godot-core/src/lib.rs +++ b/godot-core/src/lib.rs @@ -5,22 +5,22 @@ */ // If running in tests, a lot of symbols are unused or panic early -#![cfg_attr(feature = "unit-test", allow(unreachable_code, unused))] +#![cfg_attr(gdext_test, allow(unreachable_code, unused))] // More test hacks... // // Technically, `cargo test -p godot-core` *could* be supported by this abomination: -// #[cfg(not(any(test, doctest, feature = "unit-test"))] +// #[cfg(not(any(test, doctest, gdext_test))] // which would be necessary because `cargo test` runs both test/doctest, and downstream crates may need the feature as // workaround https://github.com/rust-lang/rust/issues/59168#issuecomment-962214945. However, this *also* does not work, // as #[cfg(doctest)] is currently near-useless for conditional compilation: https://github.com/rust-lang/rust/issues/67295. // Yet even then, our compile error here is only one of many, as the compiler tries to build doctest without hitting this. #[cfg(all( test, // `cargo test` - not(feature = "unit-test"), // but forgot `--features unit-test` - not(gdext_clippy) // and not `cargo clippy --cfg gdext_clippy` (this implicitly enables `test`) + not(gdext_test), // but forgot `--cfg gdext_test` + not(gdext_clippy) // and is not `cargo clippy --cfg gdext_clippy` (this implicitly enables `test`) ))] -compile_error!("Running `cargo test` requires `--features unit-test`; `cargo clippy` requires `--cfg gdext_clippy`"); +compile_error!("Running `cargo test` requires `--cfg gdext_test`; `cargo clippy` requires `--cfg gdext_clippy`"); // ---------------------------------------------------------------------------------------------------------------------------------------------- @@ -38,7 +38,7 @@ pub mod obj; pub use godot_ffi as sys; pub use registry::*; -#[cfg(not(feature = "unit-test"))] +#[cfg(not(any(gdext_test, doctest)))] pub mod engine; // Output of generated code. Mimics the file structure, symbols are re-exported. @@ -48,9 +48,11 @@ pub mod engine; #[allow(clippy::wrong_self_convention)] // TODO remove once to_string is const mod gen; -#[cfg(feature = "unit-test")] +// For some buggy reason, during doctest, the --cfg flag is not always considered, leading to monstrosities +// such as #[cfg(not(any(gdext_test, doctest)))]. +#[cfg(any(gdext_test, doctest))] mod test_stubs; -#[cfg(feature = "unit-test")] +#[cfg(any(gdext_test, doctest))] pub use test_stubs::*; #[doc(hidden)] diff --git a/godot-ffi/Cargo.toml b/godot-ffi/Cargo.toml index e42e4be7b..14ddf8433 100644 --- a/godot-ffi/Cargo.toml +++ b/godot-ffi/Cargo.toml @@ -12,7 +12,6 @@ categories = ["game-engines", "graphics"] [features] codegen-fmt = ["godot-codegen/codegen-fmt"] #codegen-full = ["godot-codegen/codegen-full"] -unit-test = [] # If this crate is built for a downstream unit test [dependencies] paste = "1" diff --git a/godot-ffi/build.rs b/godot-ffi/build.rs index a611918d8..b8a39b5d8 100644 --- a/godot-ffi/build.rs +++ b/godot-ffi/build.rs @@ -17,7 +17,7 @@ fn main() { run_bindgen(&gen_path.join("gdextension_interface.rs")); - let stubs_only = cfg!(feature = "unit-test"); + let stubs_only = cfg!(gdext_test); godot_codegen::generate_sys_files(gen_path, stubs_only); } diff --git a/godot-ffi/src/lib.rs b/godot-ffi/src/lib.rs index 4b60e420b..a182b5696 100644 --- a/godot-ffi/src/lib.rs +++ b/godot-ffi/src/lib.rs @@ -9,7 +9,7 @@ #![cfg_attr(test, allow(unused))] // Output of generated code. Mimics the file structure, symbols are re-exported. -// Note: accessing `gen` *may* still work without explicitly specifying `unit-test` feature, +// Note: accessing `gen` *may* still work without explicitly specifying `--cfg gdext_test` flag, // but stubs are generated for consistency with how godot-core depends on godot-codegen. #[rustfmt::skip] #[allow( @@ -36,18 +36,18 @@ pub use crate::godot_ffi::{GodotFfi, GodotFuncMarshal}; pub use gen::central::*; pub use gen::gdextension_interface::*; // needs `crate::` -#[cfg(not(feature = "unit-test"))] +#[cfg(not(any(gdext_test, doctest)))] #[doc(inline)] pub use real_impl::*; -#[cfg(feature = "unit-test")] +#[cfg(gdext_test)] #[doc(inline)] pub use test_impl::*; // ---------------------------------------------------------------------------------------------------------------------------------------------- // Real implementation, when Godot engine is running -#[cfg(not(feature = "unit-test"))] +#[cfg(not(any(gdext_test, doctest)))] mod real_impl { use super::global_registry::GlobalRegistry; use super::*; @@ -171,7 +171,7 @@ mod real_impl { // ---------------------------------------------------------------------------------------------------------------------------------------------- // Stubs when in unit-test (without Godot) -#[cfg(feature = "unit-test")] +#[cfg(gdext_test)] mod test_impl { use super::gen::gdextension_interface::*; use super::global_registry::GlobalRegistry; @@ -205,7 +205,7 @@ mod test_impl { ($name:ident) => {{ #[allow(unreachable_code)] fn panic2(t: T, u: U) -> () { - panic!("builtin_fn! unavailable in unit-tests; needs Godot engine"); + panic!("builtin_fn! unavailable in unit tests; needs Godot engine"); () } panic2 @@ -213,7 +213,7 @@ mod test_impl { ($name:ident @1) => {{ #[allow(unreachable_code)] fn panic1(t: T) -> () { - panic!("builtin_fn! unavailable in unit-tests; needs Godot engine"); + panic!("builtin_fn! unavailable in unit tests; needs Godot engine"); () } panic1 @@ -227,7 +227,7 @@ mod test_impl { ($symbol:expr) => { panic!(concat!( stringify!($symbol), - " unavailable in unit-tests; needs Godot engine" + " unavailable in unit tests; needs Godot engine" )) }; } diff --git a/godot-macros/Cargo.toml b/godot-macros/Cargo.toml index 237dcfbbc..191dcdecb 100644 --- a/godot-macros/Cargo.toml +++ b/godot-macros/Cargo.toml @@ -10,8 +10,9 @@ categories = ["game-engines", "graphics"] [lib] proc-macro = true +# Reverse dev dependencies so doctests can use `godot::` prefix [dev-dependencies] -godot = { path = "../godot" } #, features = ["unit-test"] } # doctest +godot = { path = "../godot" } [dependencies] quote = "1" diff --git a/godot/Cargo.toml b/godot/Cargo.toml index 976b30c42..eebda8353 100644 --- a/godot/Cargo.toml +++ b/godot/Cargo.toml @@ -15,11 +15,7 @@ trace = ["godot-core/trace"] # Private features, they are under no stability guarantee codegen-full = ["godot-core/codegen-full"] -unit-test = [] [dependencies] godot-core = { path = "../godot-core" } godot-macros = { path = "../godot-macros" } - -#[dev-dependencies] -#godot-core = { path = "../godot-core", features = ["unit-test"] } \ No newline at end of file diff --git a/godot/src/lib.rs b/godot/src/lib.rs index 9b58a9b69..4eae6b9ef 100644 --- a/godot/src/lib.rs +++ b/godot/src/lib.rs @@ -36,7 +36,7 @@ pub use godot_core::private; pub mod prelude { pub use super::bind::{godot_api, GodotClass, GodotExt}; pub use super::builtin::*; - #[cfg(not(feature = "unit-test"))] + #[cfg(not(any(gdext_test, doctest)))] pub use super::engine::{ load, try_load, utilities, AudioStreamPlayer, Camera2D, Camera3D, Input, Node, Node2D, Node3D, Object, PackedScene, RefCounted, Resource, SceneTree, @@ -46,7 +46,7 @@ pub mod prelude { pub use super::obj::{Base, Gd, GdMut, GdRef, GodotClass, Inherits, InstanceId, Share}; // Make trait methods available - #[cfg(not(feature = "unit-test"))] + #[cfg(not(any(gdext_test, doctest)))] pub use super::engine::NodeExt as _; pub use super::obj::EngineEnum as _; } From 32f2f950366c03b5423a0f669515e1359ce2762b Mon Sep 17 00:00:00 2001 From: Jan Haller Date: Thu, 19 Jan 2023 22:14:55 +0100 Subject: [PATCH 12/54] Update extension header (including GDExtensionBool fix) --- godot-codegen/input/gdextension_interface.h | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/godot-codegen/input/gdextension_interface.h b/godot-codegen/input/gdextension_interface.h index bf6f419aa..12437814b 100644 --- a/godot-codegen/input/gdextension_interface.h +++ b/godot-codegen/input/gdextension_interface.h @@ -503,6 +503,26 @@ typedef struct { char32_t *(*string_operator_index)(GDExtensionStringPtr p_self, GDExtensionInt p_index); const char32_t *(*string_operator_index_const)(GDExtensionConstStringPtr p_self, GDExtensionInt p_index); + void (*string_operator_plus_eq_string)(GDExtensionStringPtr p_self, GDExtensionConstStringPtr p_b); + void (*string_operator_plus_eq_char)(GDExtensionStringPtr p_self, char32_t p_b); + void (*string_operator_plus_eq_cstr)(GDExtensionStringPtr p_self, const char *p_b); + void (*string_operator_plus_eq_wcstr)(GDExtensionStringPtr p_self, const wchar_t *p_b); + void (*string_operator_plus_eq_c32str)(GDExtensionStringPtr p_self, const char32_t *p_b); + + /* XMLParser extra utilities */ + + GDExtensionInt (*xml_parser_open_buffer)(GDExtensionObjectPtr p_instance, const uint8_t *p_buffer, size_t p_size); + + /* FileAccess extra utilities */ + + void (*file_access_store_buffer)(GDExtensionObjectPtr p_instance, const uint8_t *p_src, uint64_t p_length); + uint64_t (*file_access_get_buffer)(GDExtensionConstObjectPtr p_instance, uint8_t *p_dst, uint64_t p_length); + + /* WorkerThreadPool extra utilities */ + + int64_t (*worker_thread_pool_add_native_group_task)(GDExtensionObjectPtr p_instance, void (*p_func)(void *, uint32_t), void *p_userdata, int p_elements, int p_tasks, GDExtensionBool p_high_priority, GDExtensionConstStringPtr p_description); + int64_t (*worker_thread_pool_add_native_task)(GDExtensionObjectPtr p_instance, void (*p_func)(void *), void *p_userdata, GDExtensionBool p_high_priority, GDExtensionConstStringPtr p_description); + /* Packed array functions */ uint8_t *(*packed_byte_array_operator_index)(GDExtensionTypePtr p_self, GDExtensionInt p_index); // p_self should be a PackedByteArray From 4a03f48e61b75ebbc05e235fcd0ae14bdfb450fc Mon Sep 17 00:00:00 2001 From: mhoff Date: Sun, 8 Jan 2023 23:52:06 -0800 Subject: [PATCH 13/54] Add support for vararg utility functions, like print or max. Mostly duplicated from how vararg methods work for classes. --- godot-codegen/src/class_generator.rs | 88 +++++++++++++++++++++------- itest/rust/src/utilities_test.rs | 18 ++++++ 2 files changed, 86 insertions(+), 20 deletions(-) diff --git a/godot-codegen/src/class_generator.rs b/godot-codegen/src/class_generator.rs index 078bbc7d2..67886813b 100644 --- a/godot-codegen/src/class_generator.rs +++ b/godot-codegen/src/class_generator.rs @@ -444,8 +444,7 @@ pub(crate) fn make_function_definition( function: &UtilityFunction, ctx: &mut Context, ) -> TokenStream { - // TODO support vararg functions - if function.is_vararg || is_function_excluded(function, ctx) { + if is_function_excluded(function, ctx) { return TokenStream::new(); } @@ -456,21 +455,49 @@ pub(crate) fn make_function_definition( let function_name = safe_ident(function_name_str); let hash = function.hash; - let (return_decl, call) = make_utility_return(&function.return_type, ctx); + let (return_decl, call) = make_utility_return(&function.return_type, is_vararg, ctx); - quote! { - pub fn #function_name( #( #params ),* ) #return_decl { - unsafe { - let __function_name = StringName::from(#function_name_str); - let __call_fn = sys::interface_fn!(variant_get_ptr_utility_function)(__function_name.string_sys(), #hash); - let __call_fn = __call_fn.unwrap_unchecked(); - - let __args = [ - #( #arg_exprs ),* - ]; - let __args_ptr = __args.as_ptr(); - - #call + if is_vararg { + quote! { + pub fn #function_name( #( #params , )* varargs: &[Variant]) #return_decl { + unsafe { + let __function_name = StringName::from(#function_name_str); + let __call_fn = sys::interface_fn!(variant_get_ptr_utility_function)(__function_name.string_sys(), #hash); + let __call_fn = __call_fn.unwrap_unchecked(); + + let __explicit_args = [ + #( #arg_exprs ),* + ]; + let mut __args = Vec::new(); + { + use godot_ffi::GodotFfi; + __args.extend(__explicit_args.iter().map(|variant| { Variant::sys_const(variant) })); + __args.extend(varargs.iter().map(|variant| { Variant::sys_const(variant) })); + } + + let __args_ptr = __args.as_ptr(); + + #call + } + } + } + } else { + quote! { + pub fn #function_name( #( #params ),* ) #return_decl { + let result = unsafe { + let __function_name = StringName::from(#function_name_str); + let __call_fn = sys::interface_fn!(variant_get_ptr_utility_function)(__function_name.string_sys(), #hash); + let __call_fn = __call_fn.unwrap_unchecked(); + + let __args = [ + #( #arg_exprs ),* + ]; + let __args_ptr = __args.as_ptr(); + + #call + }; + + result } } } @@ -579,6 +606,7 @@ fn make_method_return( fn make_utility_return( return_value: &Option, + is_vararg: bool, ctx: &mut Context, ) -> (TokenStream, TokenStream) { let return_decl; @@ -593,22 +621,42 @@ fn make_utility_return( return_ty = None; } - let call = match return_ty { - Some(RustTy::EngineClass(return_ty)) => { + let call = match (is_vararg, return_ty) { + (true, Some(return_ty)) => { + // If the return type is not Variant, then convert to concrete target type + let return_expr = match return_ty { + RustTy::BuiltinIdent(ident) if ident == "Variant" => quote! { variant }, + _ => quote! { variant.to() }, + }; + + quote! { + use godot_ffi::GodotFfi; + let variant = Variant::from_sys_init(|return_ptr| { + __call_fn(return_ptr, __args_ptr, __args.len() as i32); + }); + #return_expr + } + } + (true, None) => { + quote! { + __call_fn(std::ptr::null_mut(), __args_ptr, __args.len() as i32); + } + } + (false, Some(RustTy::EngineClass(return_ty))) => { quote! { <#return_ty>::from_sys_init_opt(|return_ptr| { __call_fn(return_ptr, __args_ptr, __args.len() as i32); }) } } - Some(return_ty) => { + (false, Some(return_ty)) => { quote! { <#return_ty as sys::GodotFfi>::from_sys_init(|return_ptr| { __call_fn(return_ptr, __args_ptr, __args.len() as i32); }) } } - None => { + (false, None) => { quote! { __call_fn(std::ptr::null_mut(), __args_ptr, __args.len() as i32); } diff --git a/itest/rust/src/utilities_test.rs b/itest/rust/src/utilities_test.rs index df1d965d6..7135bf8eb 100644 --- a/itest/rust/src/utilities_test.rs +++ b/itest/rust/src/utilities_test.rs @@ -14,6 +14,7 @@ pub fn run() -> bool { ok &= utilities_abs(); ok &= utilities_sign(); ok &= utilities_wrap(); + ok &= utilities_max(); ok } @@ -45,3 +46,20 @@ fn utilities_wrap() { ); assert_eq!(output, Variant::from(-2.7)); } + +#[itest] +fn utilities_max() { + let output = max( + Variant::from(1.0), + Variant::from(3.0), + &[Variant::from(5.0), Variant::from(7.0)], + ); + assert_eq!(output, Variant::from(7.0)); + + let output = max( + Variant::from(-1.0), + Variant::from(-3.0), + &[Variant::from(-5.0), Variant::from(-7.0)], + ); + assert_eq!(output, Variant::from(-1.0)); +} From fceb420b317b69d7ae531f66ad1f66727e2c68de Mon Sep 17 00:00:00 2001 From: Jan Haller Date: Fri, 20 Jan 2023 17:07:56 +0100 Subject: [PATCH 14/54] Small clippy amendments --- godot-codegen/src/class_generator.rs | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/godot-codegen/src/class_generator.rs b/godot-codegen/src/class_generator.rs index 67886813b..8e3f3fd97 100644 --- a/godot-codegen/src/class_generator.rs +++ b/godot-codegen/src/class_generator.rs @@ -471,8 +471,8 @@ pub(crate) fn make_function_definition( let mut __args = Vec::new(); { use godot_ffi::GodotFfi; - __args.extend(__explicit_args.iter().map(|variant| { Variant::sys_const(variant) })); - __args.extend(varargs.iter().map(|variant| { Variant::sys_const(variant) })); + __args.extend(__explicit_args.iter().map(Variant::sys_const)); + __args.extend(varargs.iter().map(Variant::sys_const)); } let __args_ptr = __args.as_ptr(); @@ -484,7 +484,7 @@ pub(crate) fn make_function_definition( } else { quote! { pub fn #function_name( #( #params ),* ) #return_decl { - let result = unsafe { + unsafe { let __function_name = StringName::from(#function_name_str); let __call_fn = sys::interface_fn!(variant_get_ptr_utility_function)(__function_name.string_sys(), #hash); let __call_fn = __call_fn.unwrap_unchecked(); @@ -495,9 +495,7 @@ pub(crate) fn make_function_definition( let __args_ptr = __args.as_ptr(); #call - }; - - result + } } } } From e7fa8a812a98d5313e3b319e86bbf5c2ec776c4f Mon Sep 17 00:00:00 2001 From: Jan Haller Date: Fri, 20 Jan 2023 23:36:39 +0100 Subject: [PATCH 15/54] Apply rustfmt and minor review feedback --- godot-core/src/builtin/vector2.rs | 61 +++++++++++----------- godot-core/src/builtin/vector2i.rs | 58 +++++++++++---------- godot-core/src/builtin/vector3.rs | 69 +++++++++++++------------ godot-core/src/builtin/vector3i.rs | 65 ++++++++++++----------- godot-core/src/builtin/vector4.rs | 15 ++++-- godot-core/src/builtin/vector4i.rs | 13 +++-- godot-core/src/builtin/vector_macros.rs | 10 ++-- 7 files changed, 153 insertions(+), 138 deletions(-) diff --git a/godot-core/src/builtin/vector2.rs b/godot-core/src/builtin/vector2.rs index c9e559387..9506086be 100644 --- a/godot-core/src/builtin/vector2.rs +++ b/godot-core/src/builtin/vector2.rs @@ -19,7 +19,7 @@ use crate::builtin::Vector2i; /// 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. -/// +/// /// See [`Vector2i`] for its integer counterpart. #[derive(Debug, Default, Clone, Copy, PartialEq)] #[repr(C)] @@ -30,48 +30,46 @@ pub struct Vector2 { pub y: f32, } -impl_vector_operators!(Vector2, f32, (x, y)); -impl_vector_index!(Vector2, f32, (x, y), Vector2Axis, (X, Y)); -impl_common_vector_fns!(Vector2, f32); -impl_float_vector_fns!(Vector2, f32); - impl Vector2 { - /// Constructs a new `Vector2` from the given `x` and `y`. - pub const fn new(x: f32, y: f32) -> Self { - Self { x, y } - } - - /// Constructs a new `Vector2` with all components set to `v`. - pub const fn splat(v: f32) -> Self { - Self { x: v, y: v } - } - - /// Constructs a new `Vector2` from a [`Vector2i`]. - pub const fn from_vector2i(v: Vector2i) -> Self { - Self { x: v.x as f32, y: v.y as f32 } - } - - /// Zero vector, a vector with all components set to `0.0`. + /// Vector with all components set to `0.0`. pub const ZERO: Self = Self::splat(0.0); - /// One vector, a vector with all components set to `1.0`. + /// Vector with all components set to `1.0`. pub const ONE: Self = Self::splat(1.0); - /// Infinity vector, a vector with all components set to `INFIINTY`. + /// Vector with all components set to `f32::INFINITY`. pub const INF: Self = Self::splat(f32::INFINITY); - /// Left unit vector. Represents the direction of left. + /// Unit vector in -X direction (right in 2D coordinate system). pub const LEFT: Self = Self::new(-1.0, 0.0); - /// Right unit vector. Represents the direction of right. + /// Unit vector in +X direction (right in 2D coordinate system). pub const RIGHT: Self = Self::new(1.0, 0.0); - /// Up unit vector. Y is down in 2D, so this vector points -Y. + /// Unit vector in -Y direction (up in 2D coordinate system). pub const UP: Self = Self::new(0.0, -1.0); - /// Down unit vector. Y is down in 2D, so this vector points +Y. + /// Unit vector in +Y direction (down in 2D coordinate system). pub const DOWN: Self = Self::new(0.0, 1.0); + /// Constructs a new `Vector2` from the given `x` and `y`. + pub const fn new(x: f32, y: f32) -> Self { + Self { x, y } + } + + /// Constructs a new `Vector2` with both components set to `v`. + pub const fn splat(v: f32) -> Self { + Self::new(v, v) + } + + /// Constructs a new `Vector2` from a [`Vector2i`]. + pub const fn from_vector2i(v: Vector2i) -> Self { + Self { + x: v.x as f32, + y: v.y as f32, + } + } + /// Returns the result of rotating this vector by `angle` (in radians). pub fn rotated(self, angle: f32) -> Self { Self::from_glam(glam::Affine2::from_angle(angle).transform_vector2(self.to_glam())) @@ -88,13 +86,18 @@ impl Vector2 { } } -/// Formats this vector in the same way the Godot engine would. +/// Formats the vector like Godot: `(x, y)`. impl fmt::Display for Vector2 { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "({}, {})", self.x, self.y) } } +impl_common_vector_fns!(Vector2, f32); +impl_float_vector_fns!(Vector2, f32); +impl_vector_operators!(Vector2, f32, (x, y)); +impl_vector_index!(Vector2, f32, (x, y), Vector2Axis, (X, Y)); + impl GodotFfi for Vector2 { ffi_methods! { type sys::GDExtensionTypePtr = *mut Self; .. } } diff --git a/godot-core/src/builtin/vector2i.rs b/godot-core/src/builtin/vector2i.rs index ebce0bf4d..b20a2dd87 100644 --- a/godot-core/src/builtin/vector2i.rs +++ b/godot-core/src/builtin/vector2i.rs @@ -15,7 +15,7 @@ use crate::builtin::Vector2; /// /// 2-element structure that can be used to represent positions in 2D space or any other pair of /// numeric values. -/// +/// /// It uses integer coordinates and is therefore preferable to [`Vector2`] when exact precision is /// required. Note that the values are limited to 32 bits, and unlike [`Vector2`] this cannot be /// configured with an engine build option. Use `i64` or [`PackedInt64Array`] if 64-bit values are @@ -29,45 +29,43 @@ pub struct Vector2i { pub y: i32, } -impl_vector_operators!(Vector2i, i32, (x, y)); -impl_vector_index!(Vector2i, i32, (x, y), Vector2iAxis, (X, Y)); -impl_common_vector_fns!(Vector2i, i32); - impl Vector2i { - /// Constructs a new `Vector2i` from the given `x` and `y`. - pub const fn new(x: i32, y: i32) -> Self { - Self { x, y } - } - - /// Constructs a new `Vector2i` with all components set to `v`. - pub const fn splat(v: i32) -> Self { - Self { x: v, y: v } - } - - /// Constructs a new `Vector2i` from a [`Vector2`]. The floating point coordinates will be - /// truncated. - pub const fn from_vector2(v: Vector2) -> Self { - Self { x: v.x as i32, y: v.y as i32 } - } - - /// Zero vector, a vector with all components set to `0`. + /// Vector with all components set to `0`. pub const ZERO: Self = Self::splat(0); - /// One vector, a vector with all components set to `1`. + /// Vector with all components set to `1`. pub const ONE: Self = Self::splat(1); - /// Left unit vector. Represents the direction of left. + /// Unit vector in -X direction (right in 2D coordinate system). pub const LEFT: Self = Self::new(-1, 0); - /// Right unit vector. Represents the direction of right. + /// Unit vector in +X direction (right in 2D coordinate system). pub const RIGHT: Self = Self::new(1, 0); - /// Up unit vector. Y is down in 2D, so this vector points -Y. + /// Unit vector in -Y direction (up in 2D coordinate system). pub const UP: Self = Self::new(0, -1); - /// Down unit vector. Y is down in 2D, so this vector points +Y. + /// Unit vector in +Y direction (down in 2D coordinate system). pub const DOWN: Self = Self::new(0, 1); + /// Constructs a new `Vector2i` from the given `x` and `y`. + pub const fn new(x: i32, y: i32) -> Self { + Self { x, y } + } + + /// Constructs a new `Vector2i` with both components set to `v`. + pub const fn splat(v: i32) -> Self { + Self::new(v, v) + } + + /// Constructs a new `Vector2i` from a [`Vector2`]. The floating point coordinates will be truncated. + pub const fn from_vector2(v: Vector2) -> Self { + Self { + x: v.x as i32, + y: v.y as i32, + } + } + /// Converts the corresponding `glam` type to `Self`. fn from_glam(v: glam::IVec2) -> Self { Self::new(v.x, v.y) @@ -79,13 +77,17 @@ impl Vector2i { } } -/// Formats this vector in the same way the Godot engine would. +/// Formats the vector like Godot: `(x, y)`. impl fmt::Display for Vector2i { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "({}, {})", self.x, self.y) } } +impl_common_vector_fns!(Vector2i, i32); +impl_vector_operators!(Vector2i, i32, (x, y)); +impl_vector_index!(Vector2i, i32, (x, y), Vector2iAxis, (X, Y)); + impl GodotFfi for Vector2i { ffi_methods! { type sys::GDExtensionTypePtr = *mut Self; .. } } diff --git a/godot-core/src/builtin/vector3.rs b/godot-core/src/builtin/vector3.rs index a01398281..29b84b48d 100644 --- a/godot-core/src/builtin/vector3.rs +++ b/godot-core/src/builtin/vector3.rs @@ -13,13 +13,13 @@ use crate::builtin::Vector3i; /// Vector used for 3D math using floating point coordinates. /// -/// 2-element structure that can be used to represent positions in 2D space or any other pair of +/// 3-element structure that can be used to represent positions in 2D space or any other triple of /// numeric values. /// /// 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. -/// +/// /// See [`Vector3i`] for its integer counterpart. #[derive(Debug, Default, Clone, Copy, PartialEq)] #[repr(C)] @@ -32,54 +32,50 @@ pub struct Vector3 { pub z: f32, } -impl_vector_operators!(Vector3, f32, (x, y, z)); -impl_vector_index!(Vector3, f32, (x, y, z), Vector3Axis, (X, Y, Z)); -impl_common_vector_fns!(Vector3, f32); -impl_float_vector_fns!(Vector3, f32); - impl Vector3 { - /// Returns a `Vector3` with the given components. - pub const fn new(x: f32, y: f32, z: f32) -> Self { - Self { x, y, z } - } - - /// Returns a new `Vector3` with all components set to `v`. - pub const fn splat(v: f32) -> Self { - Self { x: v, y: v, z: v } - } - - /// Constructs a new `Vector3` from a [`Vector3i`]. - pub const fn from_vector3i(v: Vector3i) -> Self { - Self { x: v.x as f32, y: v.y as f32, z: v.z as f32 } - } - - /// Zero vector, a vector with all components set to `0.0`. + /// Vector with all components set to `0.0`. pub const ZERO: Self = Self::splat(0.0); - /// One vector, a vector with all components set to `1.0`. + /// Vector with all components set to `1.0`. pub const ONE: Self = Self::splat(1.0); - /// Infinity vector, a vector with all components set to `INFIINTY`. - pub const INF: Self = Self::splat(f32::INFINITY); - - /// Left unit vector. Represents the local direction of left, and the global direction of west. + /// Unit vector in -X direction. Can be interpreted as left in an untransformed 3D world. pub const LEFT: Self = Self::new(-1.0, 0.0, 0.0); - /// Right unit vector. Represents the local direction of right, and the global direction of east. + /// Unit vector in +X direction. Can be interpreted as right in an untransformed 3D world. pub const RIGHT: Self = Self::new(1.0, 0.0, 0.0); - /// Up unit vector. + /// Unit vector in -Y direction. Typically interpreted as down in a 3D world. pub const UP: Self = Self::new(0.0, -1.0, 0.0); - /// Down unit vector. + /// Unit vector in +Y direction. Typically interpreted as up in a 3D world. pub const DOWN: Self = Self::new(0.0, 1.0, 0.0); - /// Forward unit vector. Represents the local direction of forward, and the global direction of north. + /// Unit vector in -Z direction. Can be interpreted as "into the screen" in an untransformed 3D world. pub const FORWARD: Self = Self::new(0.0, 0.0, -1.0); - /// Back unit vector. Represents the local direction of back, and the global direction of south. + /// Unit vector in +Z direction. Can be interpreted as "out of the screen" in an untransformed 3D world. pub const BACK: Self = Self::new(0.0, 0.0, 1.0); + /// Returns a `Vector3` with the given components. + pub const fn new(x: f32, y: f32, z: f32) -> Self { + Self { x, y, z } + } + + /// Returns a new `Vector3` with all components set to `v`. + pub const fn splat(v: f32) -> Self { + Self::new(v, v, v) + } + + /// Constructs a new `Vector3` from a [`Vector3i`]. + pub const fn from_vector3i(v: Vector3i) -> Self { + Self { + x: v.x as f32, + y: v.y as f32, + z: v.z as f32, + } + } + /// Converts the corresponding `glam` type to `Self`. fn from_glam(v: glam::Vec3) -> Self { Self::new(v.x, v.y, v.z) @@ -91,13 +87,18 @@ impl Vector3 { } } -/// Formats this vector in the same way the Godot engine would. +/// Formats the vector like Godot: `(x, y, z)`. impl fmt::Display for Vector3 { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "({}, {}, {})", self.x, self.y, self.z) } } +impl_common_vector_fns!(Vector3, f32); +impl_float_vector_fns!(Vector3, f32); +impl_vector_operators!(Vector3, f32, (x, y, z)); +impl_vector_index!(Vector3, f32, (x, y, z), Vector3Axis, (X, Y, Z)); + impl GodotFfi for Vector3 { ffi_methods! { type sys::GDExtensionTypePtr = *mut Self; .. } } diff --git a/godot-core/src/builtin/vector3i.rs b/godot-core/src/builtin/vector3i.rs index 808b645bf..f42036412 100644 --- a/godot-core/src/builtin/vector3i.rs +++ b/godot-core/src/builtin/vector3i.rs @@ -13,9 +13,9 @@ use crate::builtin::Vector3; /// Vector used for 3D math using integer coordinates. /// -/// 3-element structure that can be used to represent positions in 3D space or any other pair of +/// 3-element structure that can be used to represent positions in 3D space or any other triple of /// numeric values. -/// +/// /// It uses integer coordinates and is therefore preferable to [`Vector3`] when exact precision is /// required. Note that the values are limited to 32 bits, and unlike [`Vector3`] this cannot be /// configured with an engine build option. Use `i64` or [`PackedInt64Array`] if 64-bit values are @@ -31,51 +31,50 @@ pub struct Vector3i { pub z: i32, } -impl_vector_operators!(Vector3i, i32, (x, y, z)); -impl_vector_index!(Vector3i, i32, (x, y, z), Vector3iAxis, (X, Y, Z)); -impl_common_vector_fns!(Vector3i, i32); - impl Vector3i { - /// Returns a `Vector3i` with the given components. - pub const fn new(x: i32, y: i32, z: i32) -> Self { - Self { x, y, z } - } - - /// Constructs a new `Vector3i` with all components set to `v`. - pub const fn splat(v: i32) -> Self { - Self { x: v, y: v, z: v } - } - - /// Constructs a new `Vector3i` from a [`Vector3`]. The floating point coordinates will be - /// truncated. - pub const fn from_vector3(v: Vector3) -> Self { - Self { x: v.x as i32, y: v.y as i32, z: v.z as i32 } - } - - /// Zero vector, a vector with all components set to `0`. + /// Vector with all components set to `0`. pub const ZERO: Self = Self::splat(0); - /// One vector, a vector with all components set to `1`. + /// Vector with all components set to `1`. pub const ONE: Self = Self::splat(1); - /// Left unit vector. Represents the local direction of left, and the global direction of west. + /// Unit vector in -X direction. pub const LEFT: Self = Self::new(-1, 0, 0); - /// Right unit vector. Represents the local direction of right, and the global direction of east. + /// Unit vector in +X direction. pub const RIGHT: Self = Self::new(1, 0, 0); - /// Up unit vector. + /// Unit vector in -Y direction. pub const UP: Self = Self::new(0, -1, 0); - /// Down unit vector. + /// Unit vector in +Y direction. pub const DOWN: Self = Self::new(0, 1, 0); - /// Forward unit vector. Represents the local direction of forward, and the global direction of north. + /// Unit vector in -Z direction. pub const FORWARD: Self = Self::new(0, 0, -1); - /// Back unit vector. Represents the local direction of back, and the global direction of south. + /// Unit vector in +Z direction. pub const BACK: Self = Self::new(0, 0, 1); + /// Returns a `Vector3i` with the given components. + pub const fn new(x: i32, y: i32, z: i32) -> Self { + Self { x, y, z } + } + + /// Constructs a new `Vector3i` with all components set to `v`. + pub const fn splat(v: i32) -> Self { + Self::new(v, v, v) + } + + /// Constructs a new `Vector3i` from a [`Vector3`]. The floating point coordinates will be truncated. + pub const fn from_vector3(v: Vector3) -> Self { + Self { + x: v.x as i32, + y: v.y as i32, + z: v.z as i32, + } + } + /// Converts the corresponding `glam` type to `Self`. fn from_glam(v: glam::IVec3) -> Self { Self::new(v.x, v.y, v.z) @@ -87,13 +86,17 @@ impl Vector3i { } } -/// Formats this vector in the same way the Godot engine would. +/// Formats the vector like Godot: `(x, y, z)`. impl fmt::Display for Vector3i { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "({}, {}, {})", self.x, self.y, self.z) } } +impl_common_vector_fns!(Vector3i, i32); +impl_vector_operators!(Vector3i, i32, (x, y, z)); +impl_vector_index!(Vector3i, i32, (x, y, z), Vector3iAxis, (X, Y, Z)); + impl GodotFfi for Vector3i { ffi_methods! { type sys::GDExtensionTypePtr = *mut Self; .. } } diff --git a/godot-core/src/builtin/vector4.rs b/godot-core/src/builtin/vector4.rs index 73c365ba7..9a8274010 100644 --- a/godot-core/src/builtin/vector4.rs +++ b/godot-core/src/builtin/vector4.rs @@ -18,7 +18,7 @@ use crate::builtin::Vector4i; /// 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. -/// +/// /// See [`Vector4i`] for its integer counterpart. #[derive(Debug, Default, Clone, Copy, PartialEq)] #[repr(C)] @@ -46,12 +46,17 @@ impl Vector4 { /// Returns a new `Vector4` with all components set to `v`. pub const fn splat(v: f32) -> Self { - Self { x: v, y: v, z: v, w: v } + Self::new(v, v, v, v) } /// Constructs a new `Vector3` from a [`Vector3i`]. pub const fn from_vector4i(v: Vector4i) -> Self { - Self { x: v.x as f32, y: v.y as f32, z: v.z as f32, w: v.w as f32 } + Self { + x: v.x as f32, + y: v.y as f32, + z: v.z as f32, + w: v.w as f32, + } } /// Zero vector, a vector with all components set to `0.0`. @@ -60,7 +65,7 @@ impl Vector4 { /// One vector, a vector with all components set to `1.0`. pub const ONE: Self = Self::splat(1.0); - /// Infinity vector, a vector with all components set to `INFIINTY`. + /// Infinity vector, a vector with all components set to `f32::INFINITY`. pub const INF: Self = Self::splat(f32::INFINITY); /// Converts the corresponding `glam` type to `Self`. @@ -74,7 +79,7 @@ impl Vector4 { } } -/// Formats this vector in the same way the Godot engine would. +/// Formats the vector like Godot: `(x, y, z, w)`. impl fmt::Display for Vector4 { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "({}, {}, {}, {})", self.x, self.y, self.z, self.w) diff --git a/godot-core/src/builtin/vector4i.rs b/godot-core/src/builtin/vector4i.rs index 33937d848..d8c341736 100644 --- a/godot-core/src/builtin/vector4i.rs +++ b/godot-core/src/builtin/vector4i.rs @@ -14,7 +14,7 @@ use crate::builtin::Vector4; /// Vector used for 4D math using integer coordinates. /// /// 4-element structure that can be used to represent 4D grid coordinates or sets of integers. -/// +/// /// It uses integer coordinates and is therefore preferable to [`Vector4`] when exact precision is /// required. Note that the values are limited to 32 bits, and unlike [`Vector4`] this cannot be /// configured with an engine build option. Use `i64` or [`PackedInt64Array`] if 64-bit values are @@ -44,13 +44,18 @@ impl Vector4i { /// Constructs a new `Vector4i` with all components set to `v`. pub const fn splat(v: i32) -> Self { - Self { x: v, y: v, z: v, w: v } + Self::new(v, v, v, v) } /// Constructs a new `Vector4i` from a [`Vector4`]. The floating point coordinates will be /// truncated. pub const fn from_vector3(v: Vector4) -> Self { - Self { x: v.x as i32, y: v.y as i32, z: v.z as i32, w: v.w as i32 } + Self { + x: v.x as i32, + y: v.y as i32, + z: v.z as i32, + w: v.w as i32, + } } /// Zero vector, a vector with all components set to `0`. @@ -70,7 +75,7 @@ impl Vector4i { } } -/// Formats this vector in the same way the Godot engine would. +/// Formats the vector like Godot: `(x, y, z, w)`. impl fmt::Display for Vector4i { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "({}, {}, {}, {})", self.x, self.y, self.z, self.w) diff --git a/godot-core/src/builtin/vector_macros.rs b/godot-core/src/builtin/vector_macros.rs index a1de527af..addfde689 100644 --- a/godot-core/src/builtin/vector_macros.rs +++ b/godot-core/src/builtin/vector_macros.rs @@ -180,16 +180,12 @@ macro_rules! impl_vector_operators { impl_scalar_vector_binary_operator!($Vector, $Scalar, ($($components),*), Mul, mul); impl_vector_vector_binary_operator!($Vector, $Scalar, ($($components),*), Div, div); impl_vector_scalar_binary_operator!($Vector, $Scalar, ($($components),*), Div, div); - impl_vector_vector_binary_operator!($Vector, $Scalar, ($($components),*), Rem, rem); - impl_vector_scalar_binary_operator!($Vector, $Scalar, ($($components),*), Rem, rem); impl_vector_vector_assign_operator!($Vector, $Scalar, ($($components),*), AddAssign, add_assign); impl_vector_vector_assign_operator!($Vector, $Scalar, ($($components),*), SubAssign, sub_assign); impl_vector_vector_assign_operator!($Vector, $Scalar, ($($components),*), MulAssign, mul_assign); impl_vector_scalar_assign_operator!($Vector, $Scalar, ($($components),*), MulAssign, mul_assign); impl_vector_vector_assign_operator!($Vector, $Scalar, ($($components),*), DivAssign, div_assign); impl_vector_scalar_assign_operator!($Vector, $Scalar, ($($components),*), DivAssign, div_assign); - impl_vector_vector_assign_operator!($Vector, $Scalar, ($($components),*), RemAssign, rem_assign); - impl_vector_scalar_assign_operator!($Vector, $Scalar, ($($components),*), RemAssign, rem_assign); } } @@ -205,7 +201,7 @@ macro_rules! impl_vector_index { // Name of the enum type for the axes, for example `Vector2Axis`. $AxisEnum:ty, // Names of the enum variants, with parenthes, for example `(X, Y)`. - ($($AxisVariants:ident),*)$(,)? + ($($AxisVariants:ident),*) ) => { impl std::ops::Index<$AxisEnum> for $Vector { type Output = $Scalar; @@ -243,7 +239,7 @@ macro_rules! impl_common_vector_fns { Self::from_glam(self.to_glam().abs()) } } - } + }; } /// Implements common constants and methods for floating-point type vectors. Works for any vector @@ -271,5 +267,5 @@ macro_rules! impl_float_vector_fns { Self::from_glam(self.to_glam().normalize_or_zero()) } } - } + }; } From c2e95fb18dd775b4d442f4bba3ffd53a0f6c38a5 Mon Sep 17 00:00:00 2001 From: Jan Haller Date: Sat, 21 Jan 2023 00:31:27 +0100 Subject: [PATCH 16/54] Add Gd::with_base() --- godot-core/src/obj/gd.rs | 40 ++++++++++++++++++++++++++++++++----- itest/rust/src/base_test.rs | 17 ++++++++++++++++ 2 files changed, 52 insertions(+), 5 deletions(-) diff --git a/godot-core/src/obj/gd.rs b/godot-core/src/obj/gd.rs index 29ea1f300..0436bbf4f 100644 --- a/godot-core/src/obj/gd.rs +++ b/godot-core/src/obj/gd.rs @@ -81,11 +81,7 @@ where result.storage().initialize(user_object);*/ - let object_ptr = callbacks::create_custom(move |_base| user_object); - let result = unsafe { Gd::from_obj_sys(object_ptr) }; - - T::Mem::maybe_init_ref(&result); - result + Self::with_base(move |_base| user_object) } /// Creates a default-constructed instance of `T` inside a smart pointer. @@ -114,6 +110,40 @@ where result } + // FIXME use ```no_run instead of ```ignore, as soon as unit test #[cfg] mess is cleaned up + /// Creates a `Gd` using a function that constructs a `T` from a provided base. + /// + /// Imagine you have a type `T`, which has a `#[base]` field that you cannot default-initialize. + /// The `init` function provides you with a `Base` object that you can use inside your `T`, which + /// is then wrapped in a `Gd`. + /// + /// Example: + /// ```ignore + /// # use godot::prelude::*; + /// #[derive(GodotClass)] + /// #[class(init, base=Node2D)] + /// struct MyClass { + /// #[base] + /// my_base: Base, + /// other_field: i32, + /// } + /// + /// let obj = Gd::::with_base(|my_base| { + /// // accepts the base and returns a constructed object containing it + /// MyClass { my_base, other_field: 732 } + /// }); + /// ``` + pub fn with_base(init: F) -> Self + where + F: FnOnce(crate::obj::Base) -> T, + { + let object_ptr = callbacks::create_custom(init); + let result = unsafe { Gd::from_obj_sys(object_ptr) }; + + T::Mem::maybe_init_ref(&result); + result + } + /// Hands out a guard for a shared borrow, through which the user instance can be read. /// /// The pattern is very similar to interior mutability with standard [`RefCell`][std::cell::RefCell]. diff --git a/itest/rust/src/base_test.rs b/itest/rust/src/base_test.rs index 6273b7667..b7e503c8f 100644 --- a/itest/rust/src/base_test.rs +++ b/itest/rust/src/base_test.rs @@ -13,6 +13,7 @@ pub(crate) fn run() -> bool { ok &= base_deref(); ok &= base_display(); ok &= base_debug(); + ok &= base_with_init(); ok } @@ -81,9 +82,25 @@ fn base_debug() { obj.free(); } +#[itest] +fn base_with_init() { + let obj = Gd::::with_base(|mut base| { + base.set_rotation(11.0); + BaseHolder { base, i: 732 } + }); + + { + let guard = obj.bind(); + assert_eq!(guard.i, 732); + assert_eq!(guard.get_rotation(), 11.0); + } + obj.free(); +} + #[derive(GodotClass)] #[class(init, base=Node2D)] struct BaseHolder { #[base] base: Base, + i: i32, } From 8f5ed268e7667cfe76cfe5fa39b83983944e0547 Mon Sep 17 00:00:00 2001 From: Thomas ten Cate Date: Sun, 22 Jan 2023 11:42:44 +0100 Subject: [PATCH 17/54] Vector3D is not 2D, omit needless trailing commas in macros --- godot-core/src/builtin/vector3.rs | 2 +- godot-core/src/builtin/vector_macros.rs | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/godot-core/src/builtin/vector3.rs b/godot-core/src/builtin/vector3.rs index 29b84b48d..4d00aac33 100644 --- a/godot-core/src/builtin/vector3.rs +++ b/godot-core/src/builtin/vector3.rs @@ -13,7 +13,7 @@ use crate::builtin::Vector3i; /// Vector used for 3D math using floating point coordinates. /// -/// 3-element structure that can be used to represent positions in 2D space or any other triple of +/// 3-element structure that can be used to represent positions in 3D space or any other triple of /// numeric values. /// /// It uses floating-point coordinates of 32-bit precision, unlike the engine's `float` type which diff --git a/godot-core/src/builtin/vector_macros.rs b/godot-core/src/builtin/vector_macros.rs index addfde689..1585363cf 100644 --- a/godot-core/src/builtin/vector_macros.rs +++ b/godot-core/src/builtin/vector_macros.rs @@ -18,7 +18,7 @@ macro_rules! impl_vector_unary_operator { // Name of the operator trait, for example `Neg`. $Operator:ident, // Name of the function on the operator trait, for example `neg`. - $func:ident$(,)? + $func:ident ) => { impl std::ops::$Operator for $Vector { type Output = Self; @@ -44,7 +44,7 @@ macro_rules! impl_vector_vector_binary_operator { // Name of the operator trait, for example `Add`. $Operator:ident, // Name of the function on the operator trait, for example `add`. - $func:ident$(,)? + $func:ident ) => { impl std::ops::$Operator for $Vector { type Output = Self; @@ -71,7 +71,7 @@ macro_rules! impl_vector_scalar_binary_operator { // Name of the operator trait, for example `Add`. $Operator:ident, // Name of the function on the operator trait, for example `add`. - $func:ident$(,)? + $func:ident ) => { impl std::ops::$Operator<$Scalar> for $Vector { type Output = Self; @@ -98,7 +98,7 @@ macro_rules! impl_scalar_vector_binary_operator { // Name of the operator trait, for example `Add`. $Operator:ident, // Name of the function on the operator trait, for example `add`. - $func:ident$(,)? + $func:ident ) => { impl std::ops::$Operator<$Vector> for $Scalar { type Output = $Vector; @@ -125,7 +125,7 @@ macro_rules! impl_vector_vector_assign_operator { // Name of the operator trait, for example `AddAssign`. $Operator:ident, // Name of the function on the operator trait, for example `add_assign`. - $func:ident$(,)? + $func:ident ) => { impl std::ops::$Operator for $Vector { fn $func(&mut self, rhs: $Vector) { @@ -150,7 +150,7 @@ macro_rules! impl_vector_scalar_assign_operator { // Name of the operator trait, for example `AddAssign`. $Operator:ident, // Name of the function on the operator trait, for example `add_assign`. - $func:ident$(,)? + $func:ident ) => { impl std::ops::$Operator<$Scalar> for $Vector { fn $func(&mut self, rhs: $Scalar) { @@ -170,7 +170,7 @@ macro_rules! impl_vector_operators { // Type of each individual component, for example `f32`. $Scalar:ty, // Names of the components, with parentheses, for example `(x, y)`. - ($($components:ident),*)$(,)? + ($($components:ident),*) ) => { impl_vector_unary_operator!($Vector, $Scalar, ($($components),*), Neg, neg); impl_vector_vector_binary_operator!($Vector, $Scalar, ($($components),*), Add, add); From b9b3ffb98ef5af3163f49d861dc913dd60f2d2ca Mon Sep 17 00:00:00 2001 From: Jan Haller Date: Sat, 21 Jan 2023 17:16:35 +0100 Subject: [PATCH 18/54] JSON model: merge GlobalEnum + ClassEnum --- godot-codegen/src/api_parser.rs | 52 +++----------------------- godot-codegen/src/central_generator.rs | 2 +- godot-codegen/src/class_generator.rs | 2 +- godot-codegen/src/util.rs | 10 ++--- 4 files changed, 12 insertions(+), 54 deletions(-) diff --git a/godot-codegen/src/api_parser.rs b/godot-codegen/src/api_parser.rs index a22d8756c..591139bea 100644 --- a/godot-codegen/src/api_parser.rs +++ b/godot-codegen/src/api_parser.rs @@ -20,7 +20,7 @@ pub struct ExtensionApi { pub builtin_class_sizes: Vec, pub builtin_classes: Vec, pub classes: Vec, - pub global_enums: Vec, + pub global_enums: Vec, pub utility_functions: Vec, pub singletons: Vec, } @@ -60,7 +60,7 @@ pub struct Class { pub inherits: Option, // pub api_type: String, // pub constants: Option>, - pub enums: Option>, + pub enums: Option>, pub methods: Option>, // pub properties: Option>, // pub signals: Option>, @@ -75,21 +75,14 @@ pub struct Singleton { } #[derive(DeJson)] -pub struct ClassEnum { +pub struct Enum { pub name: String, pub is_bitfield: bool, - pub values: Vec, + pub values: Vec, } -// Same as above, but no bitfield #[derive(DeJson)] -pub struct GlobalEnum { - pub name: String, - pub values: Vec, -} - -#[derive(DeJson)] -pub struct Constant { +pub struct EnumConstant { pub name: String, pub value: i32, } @@ -160,41 +153,6 @@ pub struct MethodReturn { pub type_: String, } -pub trait Enum { - fn name(&self) -> &str; - fn values(&self) -> &Vec; - fn is_bitfield(&self) -> bool; -} - -impl Enum for ClassEnum { - fn name(&self) -> &str { - &self.name - } - - fn values(&self) -> &Vec { - &self.values - } - - fn is_bitfield(&self) -> bool { - self.is_bitfield - } -} - -impl Enum for GlobalEnum { - fn name(&self) -> &str { - &self.name - } - - fn values(&self) -> &Vec { - &self.values - } - - fn is_bitfield(&self) -> bool { - // Hack until this is exported in the JSON - self.name.contains("Flag") - } -} - // ---------------------------------------------------------------------------------------------------------------------------------------------- // Implementation diff --git a/godot-codegen/src/central_generator.rs b/godot-codegen/src/central_generator.rs index 63658516e..60fa38a71 100644 --- a/godot-codegen/src/central_generator.rs +++ b/godot-codegen/src/central_generator.rs @@ -456,7 +456,7 @@ fn collect_builtin_types<'a>( builtin_types_map } -fn collect_variant_operators(api: &ExtensionApi) -> Vec<&Constant> { +fn collect_variant_operators(api: &ExtensionApi) -> Vec<&EnumConstant> { let variant_operator_enum = api .global_enums .iter() diff --git a/godot-codegen/src/class_generator.rs b/godot-codegen/src/class_generator.rs index 8e3f3fd97..c72ed40ca 100644 --- a/godot-codegen/src/class_generator.rs +++ b/godot-codegen/src/class_generator.rs @@ -264,7 +264,7 @@ fn make_methods(methods: &Option>, class_name: &str, ctx: &mut Conte } } -fn make_enums(enums: &Option>, _class_name: &str, _ctx: &Context) -> TokenStream { +fn make_enums(enums: &Option>, _class_name: &str, _ctx: &Context) -> TokenStream { let enums = match enums { Some(e) => e, None => return TokenStream::new(), diff --git a/godot-codegen/src/util.rs b/godot-codegen/src/util.rs index 3639ce5b7..c83528152 100644 --- a/godot-codegen/src/util.rs +++ b/godot-codegen/src/util.rs @@ -9,20 +9,20 @@ use crate::{Context, RustTy}; use proc_macro2::{Ident, Literal, TokenStream}; use quote::{format_ident, quote}; -pub fn make_enum_definition(enum_: &dyn Enum) -> TokenStream { +pub fn make_enum_definition(enum_: &Enum) -> TokenStream { // TODO enums which have unique ords could be represented as Rust enums // This would allow exhaustive matches (or at least auto-completed matches + #[non_exhaustive]). But even without #[non_exhaustive], // this might be a forward compatibility hazard, if Godot deprecates enumerators and adds new ones with existing ords. - let enum_name = ident(enum_.name()); + let enum_name = ident(&enum_.name); - let values = enum_.values(); + let values = &enum_.values; let mut enumerators = Vec::with_capacity(values.len()); // let mut matches = Vec::with_capacity(values.len()); let mut unique_ords = Vec::with_capacity(values.len()); for enumerator in values { - let name = make_enumerator_name(&enumerator.name, enum_.name()); + let name = make_enumerator_name(&enumerator.name, &enum_.name); let ordinal = Literal::i32_unsuffixed(enumerator.value); enumerators.push(quote! { @@ -38,7 +38,7 @@ pub fn make_enum_definition(enum_: &dyn Enum) -> TokenStream { unique_ords.sort(); unique_ords.dedup(); - let bitfield_ops = if enum_.is_bitfield() { + let bitfield_ops = if enum_.is_bitfield { let tokens = quote! { // impl #enum_name { // pub const UNSET: Self = Self { ord: 0 }; From b5077bef4e5297e729e560bd237f0d08141f8725 Mon Sep 17 00:00:00 2001 From: Jan Haller Date: Sat, 21 Jan 2023 21:09:12 +0100 Subject: [PATCH 19/54] Extend some JSON models (or foresee future additions) --- godot-codegen/src/api_parser.rs | 78 +++++++++++++++++++++++----- godot-codegen/src/class_generator.rs | 16 ++++-- 2 files changed, 78 insertions(+), 16 deletions(-) diff --git a/godot-codegen/src/api_parser.rs b/godot-codegen/src/api_parser.rs index 591139bea..ca1445b6e 100644 --- a/godot-codegen/src/api_parser.rs +++ b/godot-codegen/src/api_parser.rs @@ -40,16 +40,15 @@ pub struct ClassSize { #[derive(DeJson)] pub struct BuiltinClass { pub name: String, + pub indexing_return_type: Option, + pub is_keyed: bool, + pub members: Option>, + pub constants: Option>, + pub enums: Option>, // no bitfield + pub operators: Vec, + pub methods: Option>, pub constructors: Vec, pub has_destructor: bool, - pub operators: Vec, -} - -#[derive(DeJson)] -pub struct Operator { - pub name: String, - pub right_type: Option, // null if unary - pub return_type: String, } #[derive(DeJson)] @@ -61,7 +60,7 @@ pub struct Class { // pub api_type: String, // pub constants: Option>, pub enums: Option>, - pub methods: Option>, + pub methods: Option>, // pub properties: Option>, // pub signals: Option>, } @@ -82,11 +81,49 @@ pub struct Enum { } #[derive(DeJson)] +pub struct BuiltinClassEnum { + pub name: String, + pub values: Vec, +} + +impl BuiltinClassEnum { + pub(crate) fn to_enum(&self) -> Enum { + Enum { + name: self.name.clone(), + is_bitfield: false, + values: self.values.clone(), + } + } +} + +#[derive(DeJson, Clone)] pub struct EnumConstant { pub name: String, pub value: i32, } +#[derive(DeJson)] +pub struct Constant { + pub name: String, + #[nserde(rename = "type")] + pub type_: String, + pub value: String, +} + +#[derive(DeJson)] +pub struct Operator { + pub name: String, + pub right_type: Option, // null if unary + pub return_type: String, +} + +#[derive(DeJson)] +pub struct Member { + pub name: String, + #[nserde(rename = "type")] + pub type_: String, +} + #[derive(DeJson)] pub struct Property { #[nserde(rename = "type")] @@ -120,18 +157,29 @@ pub struct UtilityFunction { } #[derive(DeJson)] -pub struct Method { +pub struct BuiltinClassMethod { + pub name: String, + pub return_type: Option, + pub is_vararg: bool, + pub is_const: bool, + pub is_static: bool, + pub hash: Option, + pub arguments: Option>, +} + +#[derive(DeJson)] +pub struct ClassMethod { pub name: String, pub is_const: bool, pub is_vararg: bool, //pub is_static: bool, pub is_virtual: bool, pub hash: Option, - pub arguments: Option>, pub return_value: Option, + pub arguments: Option>, } -impl Method { +impl ClassMethod { pub fn map_args(&self, f: impl FnOnce(&Vec) -> R) -> R { match self.arguments.as_ref() { Some(args) => f(args), @@ -140,17 +188,23 @@ impl Method { } } +// Example: set_point_weight_scale -> +// [ {name: "id", type: "int", meta: "int64"}, +// {name: "weight_scale", type: "float", meta: "float"}, #[derive(DeJson, Clone)] pub struct MethodArg { pub name: String, #[nserde(rename = "type")] pub type_: String, + // pub meta: Option, } +// Example: get_available_point_id -> {type: "int", meta: "int64"} #[derive(DeJson)] pub struct MethodReturn { #[nserde(rename = "type")] pub type_: String, + // pub meta: Option, } // ---------------------------------------------------------------------------------------------------------------------------------------------- diff --git a/godot-codegen/src/class_generator.rs b/godot-codegen/src/class_generator.rs index c72ed40ca..c62c5d41c 100644 --- a/godot-codegen/src/class_generator.rs +++ b/godot-codegen/src/class_generator.rs @@ -249,7 +249,11 @@ fn make_module_file(classes_and_modules: Vec) -> TokenStream { } } -fn make_methods(methods: &Option>, class_name: &str, ctx: &mut Context) -> TokenStream { +fn make_methods( + methods: &Option>, + class_name: &str, + ctx: &mut Context, +) -> TokenStream { let methods = match methods { Some(m) => m, None => return TokenStream::new(), @@ -270,7 +274,7 @@ fn make_enums(enums: &Option>, _class_name: &str, _ctx: &Context) -> T None => return TokenStream::new(), }; - let definitions = enums.iter().map(|e| util::make_enum_definition(e)); + let definitions = enums.iter().map(util::make_enum_definition); quote! { #( #definitions )* @@ -295,7 +299,7 @@ fn is_type_excluded(ty: &str, ctx: &mut Context) -> bool { } } -fn is_method_excluded(method: &Method, #[allow(unused_variables)] ctx: &mut Context) -> bool { +fn is_method_excluded(method: &ClassMethod, #[allow(unused_variables)] ctx: &mut Context) -> bool { // Currently excluded: // // * Private virtual methods designed for override; skip for now @@ -350,7 +354,11 @@ fn is_function_excluded(function: &UtilityFunction, ctx: &mut Context) -> bool { }) } -fn make_method_definition(method: &Method, class_name: &str, ctx: &mut Context) -> TokenStream { +fn make_method_definition( + method: &ClassMethod, + class_name: &str, + ctx: &mut Context, +) -> TokenStream { if is_method_excluded(method, ctx) || special_cases::is_deleted(class_name, &method.name) { return TokenStream::new(); } From 89aebad455d775d1ed198187ef1f29e4f3f80a01 Mon Sep 17 00:00:00 2001 From: RealAstolfo Date: Mon, 16 Jan 2023 13:17:41 -0700 Subject: [PATCH 20/54] Implemented Scalar Godot Functions --- godot-core/src/builtin/math.rs | 110 +++++++++++++++++++++++++++++++++ 1 file changed, 110 insertions(+) create mode 100644 godot-core/src/builtin/math.rs diff --git a/godot-core/src/builtin/math.rs b/godot-core/src/builtin/math.rs new file mode 100644 index 000000000..d2d8966fc --- /dev/null +++ b/godot-core/src/builtin/math.rs @@ -0,0 +1,110 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +pub const CMP_EPSILON: f32 = 0.00001; + +pub fn lerp(a: f32, b: f32, t: f32) -> f32 { + a + ((b - a) * t) +} + +pub fn is_equal_approx(a: f32, b: f32) -> bool { + if a == b { + return true; + } + let mut tolerance = CMP_EPSILON * a.abs(); + if tolerance < CMP_EPSILON { + tolerance = CMP_EPSILON; + } + (a - b).abs() < tolerance +} + +pub fn is_zero_approx(s: f32) -> bool { + s.abs() < CMP_EPSILON +} + +pub fn fposmod(x: f32, y: f32) -> f32 { + let mut value = x % y; + if ((value < 0.0) && (y > 0.0)) || ((value > 0.0) && (y < 0.0)) { + value += y; + } + value += 0.0; + value +} + +pub fn snapped(mut value: f32, step: f32) -> f32 { + if step != 0.0 { + value = ((value / step + 0.5) * step).floor() + } + value +} + +pub fn bezier_derivative(start: f32, control_1: f32, control_2: f32, end: f32, t: f32) -> f32 { + let omt = 1.0 - t; + let omt2 = omt * omt; + let t2 = t * t; + (control_1 - start) * 3.0 * omt2 + + (control_2 - control_1) * 6.0 * omt * t + + (end - control_2) * 3.0 * t2 +} + +pub fn bezier_interpolate(start: f32, control_1: f32, control_2: f32, end: f32, t: f32) -> f32 { + let omt = 1.0 - t; + let omt2 = omt * omt; + let omt3 = omt2 * omt; + let t2 = t * t; + let t3 = t2 * t; + start * omt3 + control_1 * omt2 * t * 3.0 + control_2 * omt * t2 * 3.0 + end * t3 +} + +pub fn cubic_interpolate(from: f32, to: f32, pre: f32, post: f32, weight: f32) -> f32 { + 0.5 * ((from * 2.0) + + (-pre + to) * weight + + (2.0 * pre - 5.0 * from + 4.0 * to - post) * (weight * weight) + + (-pre + 3.0 * from - 3.0 * to + post) * (weight * weight * weight)) +} + +pub fn cubic_interpolate_in_time( + from: f32, + to: f32, + pre: f32, + post: f32, + weight: f32, + to_t: f32, + pre_t: f32, + post_t: f32, +) -> f32 { + let t = lerp(0.0, to_t, weight); + let a1 = lerp( + pre, + from, + if pre_t == 0.0 { + 0.0 + } else { + (t - pre_t) / -pre_t + }, + ); + let a2 = lerp(from, to, if to_t == 0.0 { 0.5 } else { t / to_t }); + let a3 = lerp( + to, + post, + if post_t - to_t == 0.0 { + 1.0 + } else { + (t - to_t) / (post_t - to_t) + }, + ); + let b1 = lerp( + a1, + a2, + if to_t - pre_t == 0.0 { + 0.0 + } else { + (t - pre_t) / (to_t - pre_t) + }, + ); + let b2 = lerp(a2, a3, if post_t == 0.0 { 1.0 } else { t / post_t }); + lerp(b1, b2, if to_t == 0.0 { 0.5 } else { t / to_t }) +} From b5c720a371302f45ef9c16218b0b0243836663d3 Mon Sep 17 00:00:00 2001 From: Thomas ten Cate Date: Tue, 24 Jan 2023 09:03:24 +0100 Subject: [PATCH 21/54] Mention crate-type cdylib requirement in readme --- ReadMe.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ReadMe.md b/ReadMe.md index e5fcba045..3fea2ed9e 100644 --- a/ReadMe.md +++ b/ReadMe.md @@ -41,6 +41,9 @@ In your Cargo.toml, add: ```toml [dependencies] godot = { git = "https://github.com/godot-rust/gdextension", branch = "master" } + +[lib] +crate-type = ["cdylib"] ``` To get the latest changes, you can regularly run a `cargo update` (possibly breaking). Keep your `Cargo.lock` file under version control, so that it's easy to revert updates. From 74cf57b39aa0223d88fa94d9bb994643e30d601f Mon Sep 17 00:00:00 2001 From: Jan Haller Date: Sat, 21 Jan 2023 22:37:42 +0100 Subject: [PATCH 22/54] Refactor codegen for function definitions Generation of class method and utility function was mostly copy-pasted. This commit introduces proper code reuse, as we're going to add support for builtin class methods as well. --- godot-codegen/src/class_generator.rs | 328 +++++++++++------------ godot-codegen/src/special_cases.rs | 3 +- godot-codegen/src/utilities_generator.rs | 5 +- 3 files changed, 158 insertions(+), 178 deletions(-) diff --git a/godot-codegen/src/class_generator.rs b/godot-codegen/src/class_generator.rs index c62c5d41c..69125d2b8 100644 --- a/godot-codegen/src/class_generator.rs +++ b/godot-codegen/src/class_generator.rs @@ -6,7 +6,7 @@ //! Generates a file for each Godot class -use proc_macro2::{Literal, TokenStream}; +use proc_macro2::{Ident, Literal, TokenStream}; use quote::{format_ident, quote}; use std::path::{Path, PathBuf}; @@ -139,6 +139,7 @@ fn make_class(class: &Class, ctx: &mut Context) -> GeneratedClass { use crate::engine::*; use crate::builtin::*; use crate::obj::{AsArg, Gd}; + use sys::GodotFfi as _; pub(super) mod re_export { use super::*; @@ -362,11 +363,6 @@ fn make_method_definition( if is_method_excluded(method, ctx) || special_cases::is_deleted(class_name, &method.name) { return TokenStream::new(); } - - let is_varcall = method.is_vararg; - let (params, arg_exprs) = make_params(&method.arguments, is_varcall, ctx); - - let method_name_str = special_cases::maybe_renamed(class_name, &method.name); /*if method.map_args(|args| args.is_empty()) { // Getters (i.e. 0 arguments) will be stripped of their `get_` prefix, to conform to Rust convention if let Some(remainder) = method_name.strip_prefix("get_") { @@ -377,78 +373,60 @@ fn make_method_definition( } } }*/ - let method_name = safe_ident(method_name_str); - let hash = method.hash; - // TODO &mut safety + let method_name_str = special_cases::maybe_renamed(class_name, &method.name); let receiver = if method.is_const { - quote!(&self) + quote! { &self, } } else { - quote!(&mut self) + quote! { &mut self, } }; - - let (return_decl, call) = make_method_return(&method.return_value, is_varcall, ctx); - - let vis = if special_cases::is_private(class_name, &method.name) { - quote! { pub(crate) } - } else { - quote! { pub } - }; - + let hash = method.hash; + let is_varcall = method.is_vararg; + let variant_ffi; + let function_provider; if is_varcall { - // varcall (using varargs) - quote! { - #vis fn #method_name( #receiver #(, #params )*, varargs: &[Variant]) #return_decl { - unsafe { - let __class_name = StringName::from(#class_name); - let __method_name = StringName::from(#method_name_str); - let __method_bind = sys::interface_fn!(classdb_get_method_bind)( - __class_name.string_sys(), - __method_name.string_sys(), - #hash - ); - let __call_fn = sys::interface_fn!(object_method_bind_call); - - let __explicit_args = [ - #( #arg_exprs ),* - ]; - let mut __args = Vec::new(); - __args.extend(__explicit_args.iter().map(Variant::var_sys_const)); - __args.extend(varargs.iter().map(Variant::var_sys_const)); - - let __args_ptr = __args.as_ptr(); - - #call - } - } - } + variant_ffi = Some(VariantFfi { + sys_method: ident("var_sys_const"), + from_sys_init_method: ident("from_var_sys_init"), + }); + function_provider = ident("object_method_bind_call"); } else { - // ptrcall - quote! { - #vis fn #method_name( #receiver, #( #params ),* ) #return_decl { - unsafe { - let __class_name = StringName::from(#class_name); - let __method_name = StringName::from(#method_name_str); - let __method_bind = sys::interface_fn!(classdb_get_method_bind)( - __class_name.string_sys(), - __method_name.string_sys(), - #hash - ); - let __call_fn = sys::interface_fn!(object_method_bind_ptrcall); + variant_ffi = None; + function_provider = ident("object_method_bind_ptrcall"); + } - let __args = [ - #( #arg_exprs ),* - ]; - let __args_ptr = __args.as_ptr(); + let init_code = quote! { + let __class_name = StringName::from(#class_name); + let __method_name = StringName::from(#method_name_str); + let __method_bind = sys::interface_fn!(classdb_get_method_bind)( + __class_name.string_sys(), + __method_name.string_sys(), + #hash + ); + let __call_fn = sys::interface_fn!(#function_provider); + }; + let varcall_invocation = quote! { + __call_fn(__method_bind, self.object_ptr, __args_ptr, __args.len() as i64, return_ptr, std::ptr::addr_of_mut!(__err)); + }; + let ptrcall_invocation = quote! { + __call_fn(__method_bind, self.object_ptr, __args_ptr, return_ptr); + }; - #call - } - } - } - } + make_function_definition( + method_name_str, + special_cases::is_private(class_name, &method.name), + receiver, + &method.arguments, + method.return_value.as_ref(), + variant_ffi, + init_code, + &varcall_invocation, + &ptrcall_invocation, + ctx, + ) } -pub(crate) fn make_function_definition( +pub(crate) fn make_utility_function_definition( function: &UtilityFunction, ctx: &mut Context, ) -> TokenStream { @@ -456,53 +434,110 @@ pub(crate) fn make_function_definition( return TokenStream::new(); } - let is_vararg = function.is_vararg; - let (params, arg_exprs) = make_params(&function.arguments, is_vararg, ctx); - let function_name_str = &function.name; - let function_name = safe_ident(function_name_str); + let return_value = function.return_type.as_ref().map(|type_| MethodReturn { + type_: type_.clone(), + }); let hash = function.hash; + let variant_ffi = function.is_vararg.then_some(VariantFfi { + sys_method: ident("sys_const"), + from_sys_init_method: ident("from_sys_init"), + }); + let init_code = quote! { + let __function_name = StringName::from(#function_name_str); + let __call_fn = sys::interface_fn!(variant_get_ptr_utility_function)(__function_name.string_sys(), #hash); + let __call_fn = __call_fn.unwrap_unchecked(); + }; + let invocation = quote! { + __call_fn(return_ptr, __args_ptr, __args.len() as i32); + }; + + make_function_definition( + &function.name, + false, + TokenStream::new(), + &function.arguments, + return_value.as_ref(), + variant_ffi, + init_code, + &invocation, + &invocation, + ctx, + ) +} + +/// Defines which methods to use to convert between `Variant` and FFI (either variant ptr or type ptr) +struct VariantFfi { + sys_method: Ident, + from_sys_init_method: Ident, +} - let (return_decl, call) = make_utility_return(&function.return_type, is_vararg, ctx); +#[allow(clippy::too_many_arguments)] // adding a struct/trait that's used only here, one time, reduces complexity by precisely 0% +fn make_function_definition( + function_name: &str, + is_private: bool, + receiver: TokenStream, + method_args: &Option>, + return_value: Option<&MethodReturn>, + variant_ffi: Option, + init_code: TokenStream, + varcall_invocation: &TokenStream, + ptrcall_invocation: &TokenStream, + ctx: &mut Context, +) -> TokenStream { + let vis = if is_private { + quote! { pub(crate) } + } else { + quote! { pub } + }; - if is_vararg { + 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 (return_decl, call_code) = make_return( + return_value, + variant_ffi.as_ref(), + varcall_invocation, + ptrcall_invocation, + ctx, + ); + + if let Some(variant_ffi) = variant_ffi.as_ref() { + // varcall (using varargs) + let sys_method = &variant_ffi.sys_method; quote! { - pub fn #function_name( #( #params , )* varargs: &[Variant]) #return_decl { + #vis fn #fn_name( #receiver #( #params, )* varargs: &[Variant]) #return_decl { unsafe { - let __function_name = StringName::from(#function_name_str); - let __call_fn = sys::interface_fn!(variant_get_ptr_utility_function)(__function_name.string_sys(), #hash); - let __call_fn = __call_fn.unwrap_unchecked(); + #init_code let __explicit_args = [ #( #arg_exprs ),* ]; + let mut __args = Vec::new(); - { - use godot_ffi::GodotFfi; - __args.extend(__explicit_args.iter().map(Variant::sys_const)); - __args.extend(varargs.iter().map(Variant::sys_const)); - } + __args.extend(__explicit_args.iter().map(Variant::#sys_method)); + __args.extend(varargs.iter().map(Variant::#sys_method)); let __args_ptr = __args.as_ptr(); - #call + #call_code } } } } else { + // ptrcall quote! { - pub fn #function_name( #( #params ),* ) #return_decl { + #vis fn #fn_name( #receiver #( #params, )* ) #return_decl { unsafe { - let __function_name = StringName::from(#function_name_str); - let __call_fn = sys::interface_fn!(variant_get_ptr_utility_function)(__function_name.string_sys(), #hash); - let __call_fn = __call_fn.unwrap_unchecked(); + #init_code let __args = [ #( #arg_exprs ),* ]; + let __args_ptr = __args.as_ptr(); - #call + #call_code } } } @@ -541,85 +576,18 @@ fn make_params( (params, arg_exprs) } -fn make_method_return( - return_value: &Option, - is_varcall: bool, +fn make_return( + return_value: Option<&MethodReturn>, + variant_ffi: Option<&VariantFfi>, + varcall_invocation: &TokenStream, + ptrcall_invocation: &TokenStream, ctx: &mut Context, ) -> (TokenStream, TokenStream) { let return_decl: TokenStream; let return_ty: Option; - match return_value { - Some(ret) => { - let ty = to_rust_type(&ret.type_, ctx); - return_decl = ty.return_decl(); - return_ty = Some(ty); - } - None => { - return_decl = TokenStream::new(); - return_ty = None; - } - }; - - let call = match (is_varcall, return_ty) { - (true, Some(return_ty)) => { - // If the return type is not Variant, then convert to concrete target type - let return_expr = match return_ty { - RustTy::BuiltinIdent(ident) if ident == "Variant" => quote! { variant }, - _ => quote! { variant.to() }, - }; - - // TODO use Result instead of panic on error - quote! { - let variant = Variant::from_var_sys_init(|return_ptr| { - let mut __err = sys::default_call_error(); - __call_fn(__method_bind, self.object_ptr, __args_ptr, __args.len() as i64, return_ptr, std::ptr::addr_of_mut!(__err)); - assert_eq!(__err.error, sys::GDEXTENSION_CALL_OK); - }); - #return_expr - } - } - (true, None) => { - // TODO use Result instead of panic on error - quote! { - let mut __err = sys::default_call_error(); - __call_fn(__method_bind, self.object_ptr, __args_ptr, __args.len() as i64, std::ptr::null_mut(), std::ptr::addr_of_mut!(__err)); - assert_eq!(__err.error, sys::GDEXTENSION_CALL_OK); - } - } - (false, Some(RustTy::EngineClass(return_ty))) => { - quote! { - <#return_ty>::from_sys_init_opt(|return_ptr| { - __call_fn(__method_bind, self.object_ptr, __args_ptr, return_ptr); - }) - } - } - (false, Some(return_ty)) => { - quote! { - <#return_ty as sys::GodotFfi>::from_sys_init(|return_ptr| { - __call_fn(__method_bind, self.object_ptr, __args_ptr, return_ptr); - }) - } - } - (false, None) => { - quote! { - __call_fn(__method_bind, self.object_ptr, __args_ptr, std::ptr::null_mut()); - } - } - }; - - (return_decl, call) -} - -fn make_utility_return( - return_value: &Option, - is_vararg: bool, - ctx: &mut Context, -) -> (TokenStream, TokenStream) { - let return_decl; - let return_ty; if let Some(ret) = return_value { - let ty = to_rust_type(ret, ctx); + let ty = to_rust_type(&ret.type_, ctx); return_decl = ty.return_decl(); return_ty = Some(ty); } else { @@ -627,44 +595,54 @@ fn make_utility_return( return_ty = None; } - let call = match (is_vararg, return_ty) { - (true, Some(return_ty)) => { + let call = match (variant_ffi, return_ty) { + (Some(variant_ffi), Some(return_ty)) => { // If the return type is not Variant, then convert to concrete target type let return_expr = match return_ty { RustTy::BuiltinIdent(ident) if ident == "Variant" => quote! { variant }, _ => quote! { variant.to() }, }; + let from_sys_init_method = &variant_ffi.from_sys_init_method; + // Note: __err may remain unused if the #call does not handle errors (e.g. utility fn, ptrcall, ...) + // TODO use Result instead of panic on error quote! { - use godot_ffi::GodotFfi; - let variant = Variant::from_sys_init(|return_ptr| { - __call_fn(return_ptr, __args_ptr, __args.len() as i32); + let variant = Variant::#from_sys_init_method(|return_ptr| { + let mut __err = sys::default_call_error(); + #varcall_invocation + assert_eq!(__err.error, sys::GDEXTENSION_CALL_OK); }); #return_expr } } - (true, None) => { + (Some(_), None) => { + // Note: __err may remain unused if the #call does not handle errors (e.g. utility fn, ptrcall, ...) + // TODO use Result instead of panic on error quote! { - __call_fn(std::ptr::null_mut(), __args_ptr, __args.len() as i32); + let mut __err = sys::default_call_error(); + let return_ptr = std::ptr::null_mut(); + #varcall_invocation + assert_eq!(__err.error, sys::GDEXTENSION_CALL_OK); } } - (false, Some(RustTy::EngineClass(return_ty))) => { + (None, Some(RustTy::EngineClass(return_ty))) => { quote! { <#return_ty>::from_sys_init_opt(|return_ptr| { - __call_fn(return_ptr, __args_ptr, __args.len() as i32); + #ptrcall_invocation }) } } - (false, Some(return_ty)) => { + (None, Some(return_ty)) => { quote! { <#return_ty as sys::GodotFfi>::from_sys_init(|return_ptr| { - __call_fn(return_ptr, __args_ptr, __args.len() as i32); + #ptrcall_invocation }) } } - (false, None) => { + (None, None) => { quote! { - __call_fn(std::ptr::null_mut(), __args_ptr, __args.len() as i32); + let return_ptr = std::ptr::null_mut(); + #ptrcall_invocation } } }; diff --git a/godot-codegen/src/special_cases.rs b/godot-codegen/src/special_cases.rs index 686f2283f..bcdebd0b8 100644 --- a/godot-codegen/src/special_cases.rs +++ b/godot-codegen/src/special_cases.rs @@ -57,7 +57,8 @@ pub fn is_private(class_name: &str, method_name: &str) -> bool { pub fn maybe_renamed<'m>(class_name: &str, method_name: &'m str) -> &'m str { match (class_name, method_name) { - ("GDScript", "new") => "instantiate", + // GDScript, GDScriptNativeClass, possibly more in the future + (_, "new") => "instantiate", _ => method_name, } } diff --git a/godot-codegen/src/utilities_generator.rs b/godot-codegen/src/utilities_generator.rs index aef935817..0c6fa13d5 100644 --- a/godot-codegen/src/utilities_generator.rs +++ b/godot-codegen/src/utilities_generator.rs @@ -8,7 +8,7 @@ use quote::quote; use std::path::{Path, PathBuf}; use crate::api_parser::*; -use crate::class_generator::make_function_definition; +use crate::class_generator::make_utility_function_definition; use crate::Context; pub(crate) fn generate_utilities_file( @@ -21,7 +21,7 @@ pub(crate) fn generate_utilities_file( for utility_fn in &api.utility_functions { // note: category unused -> could be their own mod - let def = make_function_definition(utility_fn, ctx); + let def = make_utility_function_definition(utility_fn, ctx); utility_fn_defs.push(def); } @@ -30,6 +30,7 @@ pub(crate) fn generate_utilities_file( use crate::builtin::*; use crate::obj::Gd; use crate::engine::Object; + use sys::GodotFfi as _; #(#utility_fn_defs)* }; From d3253c2648ddb5ccd3f6e1a9128df1720999bf6b Mon Sep 17 00:00:00 2001 From: Jan Haller Date: Mon, 23 Jan 2023 20:03:41 +0100 Subject: [PATCH 23/54] Implement codegen for builtin classes (Array, Callable, Rect2i, ...) --- godot-codegen/src/api_parser.rs | 8 + godot-codegen/src/central_generator.rs | 35 ++-- godot-codegen/src/class_generator.rs | 241 ++++++++++++++++++++++--- godot-codegen/src/lib.rs | 30 ++- godot-codegen/src/special_cases.rs | 4 + godot-codegen/src/util.rs | 7 +- godot-core/src/builtin/arrays.rs | 73 +++++--- godot-core/src/builtin/macros.rs | 32 +++- godot-core/src/builtin/mod.rs | 20 ++ godot-core/src/builtin/others.rs | 14 +- godot-core/src/builtin/vector2.rs | 8 +- godot-ffi/src/lib.rs | 1 - itest/rust/src/builtin_test.rs | 69 +++++++ itest/rust/src/lib.rs | 6 +- 14 files changed, 461 insertions(+), 87 deletions(-) create mode 100644 itest/rust/src/builtin_test.rs diff --git a/godot-codegen/src/api_parser.rs b/godot-codegen/src/api_parser.rs index ca1445b6e..e4852fde0 100644 --- a/godot-codegen/src/api_parser.rs +++ b/godot-codegen/src/api_parser.rs @@ -207,6 +207,14 @@ pub struct MethodReturn { // pub meta: Option, } +impl MethodReturn { + pub fn from_type(type_: &str) -> Self { + Self { + type_: type_.to_owned(), + } + } +} + // ---------------------------------------------------------------------------------------------------------------------------------------------- // Implementation diff --git a/godot-codegen/src/central_generator.rs b/godot-codegen/src/central_generator.rs index 60fa38a71..0fa08af5e 100644 --- a/godot-codegen/src/central_generator.rs +++ b/godot-codegen/src/central_generator.rs @@ -25,30 +25,30 @@ struct CentralItems { global_enum_defs: Vec, } -struct TypeNames { +pub(crate) struct TypeNames { /// "int" or "PackedVector2Array" - pascal_case: String, + pub pascal_case: String, /// "packed_vector2_array" - snake_case: String, + pub snake_case: String, /// "PACKED_VECTOR2_ARRAY" - //shout_case: String, + //pub shout_case: String, /// GDEXTENSION_VARIANT_TYPE_PACKED_VECTOR2_ARRAY - sys_variant_type: Ident, + pub sys_variant_type: Ident, } /// Allows collecting all builtin TypeNames before generating methods -struct BuiltinTypeInfo<'a> { - value: i32, - type_names: TypeNames, +pub(crate) struct BuiltinTypeInfo<'a> { + pub value: i32, + pub type_names: TypeNames, /// If `variant_get_ptr_destructor` returns a non-null function pointer for this type. /// List is directly sourced from extension_api.json (information would also be in variant_destruct.cpp). - has_destructor: bool, - constructors: Option<&'a Vec>, - operators: Option<&'a Vec>, + pub has_destructor: bool, + pub constructors: Option<&'a Vec>, + pub operators: Option<&'a Vec>, } pub(crate) fn generate_sys_central_file( @@ -103,12 +103,14 @@ pub(crate) fn generate_core_mod_file( pub mod class_macros {} } + pub mod builtin_classes {} pub mod utilities {} } } else { quote! { pub mod central; pub mod classes; + pub mod builtin_classes; pub mod utilities; } }; @@ -304,8 +306,7 @@ fn make_central_items(api: &ExtensionApi, build_config: &str, ctx: &mut Context) } } - let class_map = collect_builtin_classes(api); - let builtin_types_map = collect_builtin_types(api, &class_map); + let builtin_types_map = collect_builtin_types(api); let variant_operators = collect_variant_operators(api); // Generate builtin methods, now with info for all types available. @@ -389,10 +390,10 @@ fn collect_builtin_classes(api: &ExtensionApi) -> HashMap class_map } -fn collect_builtin_types<'a>( - api: &'a ExtensionApi, - class_map: &HashMap, -) -> HashMap> { +/// Returns map from "PackedStringArray" to all the info +pub(crate) fn collect_builtin_types(api: &ExtensionApi) -> HashMap> { + let class_map = collect_builtin_classes(api); + let variant_type_enum = api .global_enums .iter() diff --git a/godot-codegen/src/class_generator.rs b/godot-codegen/src/class_generator.rs index 69125d2b8..3289a6994 100644 --- a/godot-codegen/src/class_generator.rs +++ b/godot-codegen/src/class_generator.rs @@ -11,8 +11,12 @@ use quote::{format_ident, quote}; use std::path::{Path, PathBuf}; use crate::api_parser::*; +use crate::central_generator::{collect_builtin_types, BuiltinTypeInfo}; use crate::util::{ident, safe_ident, strlit, to_module_name, to_rust_type}; -use crate::{special_cases, util, Context, GeneratedClass, GeneratedModule, RustTy}; +use crate::{ + special_cases, util, Context, GeneratedBuiltin, GeneratedBuiltinModule, GeneratedClass, + GeneratedClassModule, RustTy, +}; pub(crate) fn generate_class_files( api: &ExtensionApi, @@ -45,7 +49,7 @@ pub(crate) fn generate_class_files( let class_ident = ident(&class.name); let module_ident = ident(&module_name); - modules.push(GeneratedModule { + modules.push(GeneratedClassModule { class_ident, module_ident, inherits_macro_ident: generated_class.inherits_macro_ident, @@ -53,8 +57,51 @@ pub(crate) fn generate_class_files( }); } + let out_path = gen_path.join("mod.rs"); let mod_contents = make_module_file(modules).to_string(); + std::fs::write(&out_path, mod_contents).expect("failed to write mod.rs file"); + out_files.push(out_path); +} + +pub(crate) fn generate_builtin_class_files( + api: &ExtensionApi, + ctx: &mut Context, + _build_config: &str, + gen_path: &Path, + out_files: &mut Vec, +) { + let _ = std::fs::remove_dir_all(gen_path); + std::fs::create_dir_all(gen_path).expect("create classes directory"); + + let builtin_types_map = collect_builtin_types(api); + + let mut modules = vec![]; + for class in api.builtin_classes.iter() { + if special_cases::is_builtin_type_deleted(&class.name) { + continue; + } + + let type_info = builtin_types_map + .get(&class.name) + .unwrap_or_else(|| panic!("builtin type not found: {}", class.name)); + let inner_class = format_ident!("Inner{}", class.name); + let generated_class = make_builtin_class(class, &inner_class, type_info, ctx); + let file_contents = generated_class.tokens.to_string(); + + let module_name = to_module_name(&class.name); + let out_path = gen_path.join(format!("{module_name}.rs")); + std::fs::write(&out_path, file_contents).expect("failed to write class file"); + out_files.push(out_path); + + let module_ident = ident(&module_name); + modules.push(GeneratedBuiltinModule { + class_ident: inner_class, + module_ident, + }); + } + let out_path = gen_path.join("mod.rs"); + let mod_contents = make_builtin_module_file(modules).to_string(); std::fs::write(&out_path, mod_contents).expect("failed to write mod.rs file"); out_files.push(out_path); } @@ -209,9 +256,63 @@ fn make_class(class: &Class, ctx: &mut Context) -> GeneratedClass { } } -fn make_module_file(classes_and_modules: Vec) -> TokenStream { +fn make_builtin_class( + class: &BuiltinClass, + inner_class: &Ident, + type_info: &BuiltinTypeInfo, + ctx: &mut Context, +) -> GeneratedBuiltin { + let outer_class = if let RustTy::BuiltinIdent(ident) = to_rust_type(&class.name, ctx) { + ident + } else { + panic!("Rust type `{}` categorized wrong", class.name) + }; + // let opaque_name = format_ident!("Opaque{}", class.name); + + // let constructor = make_constructor(class, ctx, &name_str); + let constructor = quote! {}; + let class_enums = class.enums.as_ref().map(|class_enums| { + class_enums + .iter() + .map(BuiltinClassEnum::to_enum) + .collect::>() + }); + + let methods = make_builtin_methods(&class.methods, &class.name, type_info, ctx); + let enums = make_enums(&class_enums, &class.name, ctx); + + // mod re_export needed, because class should not appear inside the file module, and we can't re-export private struct as pub + let tokens = quote! { + use godot_ffi as sys; + use crate::builtin::*; + use crate::obj::{AsArg, Gd}; + use crate::sys::GodotFfi as _; + use crate::engine::Object; + + // #[derive(Debug)] + #[repr(transparent)] + pub struct #inner_class<'a> { + // opaque: sys::types::#opaque_name, + pub outer: &'a mut #outer_class + } + impl<'a> #inner_class<'a> { + #constructor + #methods + } + // impl sys::GodotFfi for #class_name { + // sys::ffi_methods! { type sys::GDExtensionTypePtr = *mut Self; .. } + // } + + #enums + }; + // note: TypePtr -> ObjectPtr conversion OK? + + GeneratedBuiltin { tokens } +} + +fn make_module_file(classes_and_modules: Vec) -> TokenStream { let decls = classes_and_modules.iter().map(|m| { - let GeneratedModule { + let GeneratedClassModule { module_ident, class_ident, is_pub, @@ -227,7 +328,7 @@ fn make_module_file(classes_and_modules: Vec) -> TokenStream { }); let macros = classes_and_modules.iter().map(|m| { - let GeneratedModule { + let GeneratedClassModule { inherits_macro_ident, .. } = m; @@ -250,6 +351,25 @@ fn make_module_file(classes_and_modules: Vec) -> TokenStream { } } +fn make_builtin_module_file(classes_and_modules: Vec) -> TokenStream { + let decls = classes_and_modules.iter().map(|m| { + let GeneratedBuiltinModule { + module_ident, + class_ident, + .. + } = m; + + quote! { + mod #module_ident; + pub use #module_ident::#class_ident; + } + }); + + quote! { + #( #decls )* + } +} + fn make_methods( methods: &Option>, class_name: &str, @@ -269,6 +389,26 @@ fn make_methods( } } +fn make_builtin_methods( + methods: &Option>, + class_name: &str, + type_info: &BuiltinTypeInfo, + ctx: &mut Context, +) -> TokenStream { + let methods = match methods { + Some(m) => m, + None => return TokenStream::new(), + }; + + let definitions = methods + .iter() + .map(|method| make_builtin_method_definition(method, class_name, type_info, ctx)); + + quote! { + #( #definitions )* + } +} + fn make_enums(enums: &Option>, _class_name: &str, _ctx: &Context) -> TokenStream { let enums = match enums { Some(e) => e, @@ -382,18 +522,13 @@ fn make_method_definition( }; let hash = method.hash; let is_varcall = method.is_vararg; - let variant_ffi; - let function_provider; - if is_varcall { - variant_ffi = Some(VariantFfi { - sys_method: ident("var_sys_const"), - from_sys_init_method: ident("from_var_sys_init"), - }); - function_provider = ident("object_method_bind_call"); + + let variant_ffi = is_varcall.then(VariantFfi::variant_ptr); + let function_provider = if is_varcall { + ident("object_method_bind_call") } else { - variant_ffi = None; - function_provider = ident("object_method_bind_ptrcall"); - } + ident("object_method_bind_ptrcall") + }; let init_code = quote! { let __class_name = StringName::from(#class_name); @@ -426,6 +561,57 @@ fn make_method_definition( ) } +fn make_builtin_method_definition( + method: &BuiltinClassMethod, + class_name_str: &str, + type_info: &BuiltinTypeInfo, + ctx: &mut Context, +) -> TokenStream { + // TODO implement varcalls + if method.is_vararg { + return TokenStream::new(); + } + + let method_name_str = &method.name; + let receiver = if method.is_const { + quote! { &self, } + } else { + quote! { &mut self, } + }; + let return_value = method.return_type.as_deref().map(MethodReturn::from_type); + let hash = method.hash; + let is_varcall = method.is_vararg; + let variant_ffi = is_varcall.then(VariantFfi::type_ptr); + + let variant_type = &type_info.type_names.sys_variant_type; + let init_code = quote! { + let __variant_type = sys::#variant_type; + let __method_name = StringName::from(#method_name_str); + let __call_fn = sys::interface_fn!(variant_get_ptr_builtin_method)( + __variant_type, + __method_name.string_sys(), + #hash + ); + let __call_fn = __call_fn.unwrap_unchecked(); + }; + let ptrcall_invocation = quote! { + __call_fn(self.outer.sys(), __args_ptr, return_ptr, __args.len() as i32); + }; + + make_function_definition( + method_name_str, + special_cases::is_private(class_name_str, &method.name), + receiver, + &method.arguments, + return_value.as_ref(), + variant_ffi, + init_code, + &ptrcall_invocation, + &ptrcall_invocation, + ctx, + ) +} + pub(crate) fn make_utility_function_definition( function: &UtilityFunction, ctx: &mut Context, @@ -435,14 +621,9 @@ pub(crate) fn make_utility_function_definition( } let function_name_str = &function.name; - let return_value = function.return_type.as_ref().map(|type_| MethodReturn { - type_: type_.clone(), - }); + let return_value = function.return_type.as_deref().map(MethodReturn::from_type); let hash = function.hash; - let variant_ffi = function.is_vararg.then_some(VariantFfi { - sys_method: ident("sys_const"), - from_sys_init_method: ident("from_sys_init"), - }); + let variant_ffi = function.is_vararg.then_some(VariantFfi::type_ptr()); let init_code = quote! { let __function_name = StringName::from(#function_name_str); let __call_fn = sys::interface_fn!(variant_get_ptr_utility_function)(__function_name.string_sys(), #hash); @@ -471,6 +652,20 @@ struct VariantFfi { sys_method: Ident, from_sys_init_method: Ident, } +impl VariantFfi { + fn variant_ptr() -> Self { + Self { + sys_method: ident("var_sys_const"), + from_sys_init_method: ident("from_var_sys_init"), + } + } + fn type_ptr() -> Self { + Self { + sys_method: ident("sys_const"), + from_sys_init_method: ident("from_sys_init"), + } + } +} #[allow(clippy::too_many_arguments)] // adding a struct/trait that's used only here, one time, reduces complexity by precisely 0% fn make_function_definition( diff --git a/godot-codegen/src/lib.rs b/godot-codegen/src/lib.rs index 02c78656c..61356ce2f 100644 --- a/godot-codegen/src/lib.rs +++ b/godot-codegen/src/lib.rs @@ -23,7 +23,7 @@ use central_generator::{ generate_core_central_file, generate_core_mod_file, generate_sys_central_file, generate_sys_mod_file, }; -use class_generator::generate_class_files; +use class_generator::{generate_builtin_class_files, generate_class_files}; use context::Context; use util::ident; use utilities_generator::generate_utilities_file; @@ -86,12 +86,21 @@ pub fn generate_core_files(core_gen_path: &Path, stubs_only: bool) { ); watch.record("generate_class_files"); + generate_builtin_class_files( + &api, + &mut ctx, + build_config, + &core_gen_path.join("builtin_classes"), + &mut out_files, + ); + watch.record("generate_builtin_class_files"); + rustfmt_if_needed(out_files); watch.record("rustfmt"); watch.write_stats_to(&core_gen_path.join("codegen-stats.txt")); } -#[cfg(feature = "codegen-fmt")] +// #[cfg(feature = "codegen-fmt")] fn rustfmt_if_needed(out_files: Vec) { println!("Format {} generated files...", out_files.len()); @@ -111,9 +120,9 @@ fn rustfmt_if_needed(out_files: Vec) { println!("Rustfmt completed."); } - -#[cfg(not(feature = "codegen-fmt"))] -fn rustfmt_if_needed(_out_files: Vec) {} +// +// #[cfg(not(feature = "codegen-fmt"))] +// fn rustfmt_if_needed(_out_files: Vec) {} // ---------------------------------------------------------------------------------------------------------------------------------------------- // Shared utility types @@ -173,13 +182,22 @@ struct GeneratedClass { has_pub_module: bool, } -struct GeneratedModule { +struct GeneratedBuiltin { + tokens: TokenStream, +} + +struct GeneratedClassModule { class_ident: Ident, module_ident: Ident, inherits_macro_ident: Ident, is_pub: bool, } +struct GeneratedBuiltinModule { + class_ident: Ident, + module_ident: Ident, +} + // ---------------------------------------------------------------------------------------------------------------------------------------------- // Shared config diff --git a/godot-codegen/src/special_cases.rs b/godot-codegen/src/special_cases.rs index bcdebd0b8..4ed1a4262 100644 --- a/godot-codegen/src/special_cases.rs +++ b/godot-codegen/src/special_cases.rs @@ -55,6 +55,10 @@ pub fn is_private(class_name: &str, method_name: &str) -> bool { } } +pub fn is_builtin_type_deleted(class_name: &str) -> bool { + class_name == "Nil" || class_name.chars().next().unwrap().is_ascii_lowercase() +} + pub fn maybe_renamed<'m>(class_name: &str, method_name: &'m str) -> &'m str { match (class_name, method_name) { // GDScript, GDScriptNativeClass, possibly more in the future diff --git a/godot-codegen/src/util.rs b/godot-codegen/src/util.rs index c83528152..61c009ba0 100644 --- a/godot-codegen/src/util.rs +++ b/godot-codegen/src/util.rs @@ -282,11 +282,12 @@ fn to_rust_type_uncached(ty: &str, ctx: &mut Context) -> RustTy { } else if let Some(packed_arr_ty) = ty.strip_prefix("Packed") { // Don't trigger on PackedScene ;P if packed_arr_ty.ends_with("Array") { - return RustTy::BuiltinIdent(ident(packed_arr_ty)); + return RustTy::BuiltinIdent(ident(ty)); + //return RustTy::BuiltinIdent(ident(packed_arr_ty)); } } else if let Some(elem_ty) = ty.strip_prefix("typedarray::") { - if let Some(packed_arr_ty) = elem_ty.strip_prefix("Packed") { - return RustTy::BuiltinIdent(ident(packed_arr_ty)); + if let Some(_packed_arr_ty) = elem_ty.strip_prefix("Packed") { + return RustTy::BuiltinIdent(ident(elem_ty)); } let rust_elem_ty = to_rust_type(elem_ty, ctx); diff --git a/godot-core/src/builtin/arrays.rs b/godot-core/src/builtin/arrays.rs index 8e3763845..af08513b8 100644 --- a/godot-core/src/builtin/arrays.rs +++ b/godot-core/src/builtin/arrays.rs @@ -6,42 +6,43 @@ use godot_ffi as sys; -use crate::builtin::{FromVariant, Variant}; +use crate::builtin::{inner, FromVariant, Variant}; use std::marker::PhantomData; -use sys::{ffi_methods, interface_fn, types::*, GodotFfi}; +use sys::types::*; +use sys::{ffi_methods, interface_fn, GodotFfi}; impl_builtin_stub!(Array, OpaqueArray); -impl_builtin_stub!(ByteArray, OpaquePackedByteArray); -impl_builtin_stub!(ColorArray, OpaquePackedColorArray); -impl_builtin_stub!(Float32Array, OpaquePackedFloat32Array); -impl_builtin_stub!(Float64Array, OpaquePackedFloat64Array); -impl_builtin_stub!(Int32Array, OpaquePackedInt32Array); -impl_builtin_stub!(Int64Array, OpaquePackedInt64Array); -impl_builtin_stub!(StringArray, OpaquePackedStringArray); -impl_builtin_stub!(Vector2Array, OpaquePackedVector2Array); -impl_builtin_stub!(Vector3Array, OpaquePackedVector3Array); +impl_builtin_stub!(PackedByteArray, OpaquePackedByteArray); +impl_builtin_stub!(PackedColorArray, OpaquePackedColorArray); +impl_builtin_stub!(PackedFloat32Array, OpaquePackedFloat32Array); +impl_builtin_stub!(PackedFloat64Array, OpaquePackedFloat64Array); +impl_builtin_stub!(PackedInt32Array, OpaquePackedInt32Array); +impl_builtin_stub!(PackedInt64Array, OpaquePackedInt64Array); +impl_builtin_stub!(PackedStringArray, OpaquePackedStringArray); +impl_builtin_stub!(PackedVector2Array, OpaquePackedVector2Array); +impl_builtin_stub!(PackedVector3Array, OpaquePackedVector3Array); impl_builtin_froms!(Array; - ByteArray => array_from_packed_byte_array, - ColorArray => array_from_packed_color_array, - Float32Array => array_from_packed_float32_array, - Float64Array => array_from_packed_float64_array, - Int32Array => array_from_packed_int32_array, - Int64Array => array_from_packed_int64_array, - StringArray => array_from_packed_string_array, - Vector2Array => array_from_packed_vector2_array, - Vector3Array => array_from_packed_vector3_array, + PackedByteArray => array_from_packed_byte_array, + PackedColorArray => array_from_packed_color_array, + PackedFloat32Array => array_from_packed_float32_array, + PackedFloat64Array => array_from_packed_float64_array, + PackedInt32Array => array_from_packed_int32_array, + PackedInt64Array => array_from_packed_int64_array, + PackedStringArray => array_from_packed_string_array, + PackedVector2Array => array_from_packed_vector2_array, + PackedVector3Array => array_from_packed_vector3_array, ); -impl_builtin_froms!(ByteArray; Array => packed_byte_array_from_array); -impl_builtin_froms!(ColorArray; Array => packed_color_array_from_array); -impl_builtin_froms!(Float32Array; Array => packed_float32_array_from_array); -impl_builtin_froms!(Float64Array; Array => packed_float64_array_from_array); -impl_builtin_froms!(Int32Array; Array => packed_int32_array_from_array); -impl_builtin_froms!(Int64Array; Array => packed_int64_array_from_array); -impl_builtin_froms!(StringArray; Array => packed_string_array_from_array); -impl_builtin_froms!(Vector2Array; Array => packed_vector2_array_from_array); -impl_builtin_froms!(Vector3Array; Array => packed_vector3_array_from_array); +impl_builtin_froms!(PackedByteArray; Array => packed_byte_array_from_array); +impl_builtin_froms!(PackedColorArray; Array => packed_color_array_from_array); +impl_builtin_froms!(PackedFloat32Array; Array => packed_float32_array_from_array); +impl_builtin_froms!(PackedFloat64Array; Array => packed_float64_array_from_array); +impl_builtin_froms!(PackedInt32Array; Array => packed_int32_array_from_array); +impl_builtin_froms!(PackedInt64Array; Array => packed_int64_array_from_array); +impl_builtin_froms!(PackedStringArray; Array => packed_string_array_from_array); +impl_builtin_froms!(PackedVector2Array; Array => packed_vector2_array_from_array); +impl_builtin_froms!(PackedVector3Array; Array => packed_vector3_array_from_array); impl Array { pub fn get(&self, index: i64) -> Option { @@ -53,6 +54,20 @@ impl Array { Some((*ptr).clone()) } } + + #[cfg(not(any(gdext_test, doctest)))] + #[doc(hidden)] + pub fn as_inner(&mut self) -> inner::InnerArray { + inner::InnerArray { outer: self } + } +} + +impl_builtin_traits! { + for Array { + Default => array_construct_default; + Clone => array_construct_copy; + Drop => array_destroy; + } } #[repr(C)] diff --git a/godot-core/src/builtin/macros.rs b/godot-core/src/builtin/macros.rs index 24d5d6699..7bda1c20c 100644 --- a/godot-core/src/builtin/macros.rs +++ b/godot-core/src/builtin/macros.rs @@ -11,10 +11,18 @@ macro_rules! impl_builtin_traits_inner { impl Default for $Type { #[inline] fn default() -> Self { + // Note: can't use from_sys_init(), as that calls the default constructor + // (because most assignments expect initialized target type) + + let mut uninit = std::mem::MaybeUninit::<$Type>::uninit(); + unsafe { - let mut gd_val = sys::$GdType::default(); - ::godot_ffi::builtin_fn!($gd_method)(&mut gd_val); - <$Type>::from_sys(gd_val) + let self_ptr = (*uninit.as_mut_ptr()).sys_mut(); + sys::builtin_call! { + $gd_method(self_ptr, std::ptr::null_mut()) + }; + + uninit.assume_init() } } } @@ -99,6 +107,21 @@ macro_rules! impl_builtin_traits_inner { } } }; + + ( FromVariant for $Type:ty => $gd_method:ident ) => { + impl $crate::builtin::variant::FromVariant for $Type { + fn try_from_variant(variant: &$crate::builtin::Variant) -> Result { + let result = unsafe { + Self::from_sys_init(|self_ptr| { + let converter = sys::builtin_fn!($gd_method); + converter(self_ptr, variant.var_sys()); + }) + }; + + Ok(result) + } + } + }; } macro_rules! impl_builtin_traits { @@ -116,9 +139,10 @@ macro_rules! impl_builtin_traits { } macro_rules! impl_builtin_stub { + // ($Class:ident, $OpaqueTy:ident $( ; )? $( $Traits:ident ),* ) => { ($Class:ident, $OpaqueTy:ident) => { #[repr(C)] - #[derive(Copy, Clone)] + // #[derive(Copy, Clone)] pub struct $Class { opaque: sys::types::$OpaqueTy, } diff --git a/godot-core/src/builtin/mod.rs b/godot-core/src/builtin/mod.rs index 56e3b8855..40f326bff 100644 --- a/godot-core/src/builtin/mod.rs +++ b/godot-core/src/builtin/mod.rs @@ -64,3 +64,23 @@ pub use vector3::*; pub use vector3i::*; pub use vector4::*; pub use vector4i::*; + +#[doc(hidden)] +pub mod inner { + #[cfg(not(gdext_test))] + pub use crate::gen::builtin_classes::*; +} + +// pub struct PackedArray { +// _phantom: std::marker::PhantomData +// } +// +// pub type PackedByteArray = PackedArray; +// pub type PackedInt32Array = PackedArray; +// pub type PackedInt64Array = PackedArray; +// pub type PackedFloat32Array = PackedArray; +// pub type PackedFloat64Array = PackedArray; +// pub type PackedStringArray = PackedArray; +// pub type PackedVector2Array = PackedArray; +// pub type PackedVector3Array = PackedArray; +// pub type PackedColorArray = PackedArray; diff --git a/godot-core/src/builtin/others.rs b/godot-core/src/builtin/others.rs index 2aff9fa1c..4f39588f7 100644 --- a/godot-core/src/builtin/others.rs +++ b/godot-core/src/builtin/others.rs @@ -6,7 +6,7 @@ // Stub for various other built-in classes, which are currently incomplete, but whose types // are required for codegen -use crate::builtin::{StringName, Vector2}; +use crate::builtin::{inner, StringName, Vector2}; use crate::obj::{Gd, GodotClass}; use godot_ffi as sys; use sys::{ffi_methods, GodotFfi}; @@ -60,4 +60,16 @@ impl Callable { }) } } + + #[cfg(not(any(gdext_test, doctest)))] + #[doc(hidden)] + pub fn as_inner(&mut self) -> inner::InnerCallable { + inner::InnerCallable { outer: self } + } +} + +impl_builtin_traits! { + for Callable { + FromVariant => callable_from_variant; + } } diff --git a/godot-core/src/builtin/vector2.rs b/godot-core/src/builtin/vector2.rs index 9506086be..082c655d0 100644 --- a/godot-core/src/builtin/vector2.rs +++ b/godot-core/src/builtin/vector2.rs @@ -9,7 +9,7 @@ use std::fmt; use godot_ffi as sys; use sys::{ffi_methods, GodotFfi}; -use crate::builtin::Vector2i; +use crate::builtin::{inner, Vector2i}; /// Vector used for 2D math using floating point coordinates. /// @@ -84,6 +84,12 @@ impl Vector2 { fn to_glam(self) -> glam::Vec2 { glam::Vec2::new(self.x, self.y) } + + #[cfg(not(any(gdext_test, doctest)))] + #[doc(hidden)] + pub fn as_inner(&mut self) -> inner::InnerVector2 { + inner::InnerVector2 { outer: self } + } } /// Formats the vector like Godot: `(x, y)`. diff --git a/godot-ffi/src/lib.rs b/godot-ffi/src/lib.rs index a182b5696..bf0e3b4ff 100644 --- a/godot-ffi/src/lib.rs +++ b/godot-ffi/src/lib.rs @@ -79,7 +79,6 @@ mod real_impl { "Initialize GDExtension interface: {}", ver.to_str().unwrap() ); - //dbg!(*interface); BINDING = Some(GodotBinding { interface: *interface, diff --git a/itest/rust/src/builtin_test.rs b/itest/rust/src/builtin_test.rs new file mode 100644 index 000000000..6d6442a54 --- /dev/null +++ b/itest/rust/src/builtin_test.rs @@ -0,0 +1,69 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +use crate::itest; +use godot::builtin::inner::*; +use godot::prelude::*; + +pub(crate) fn run() -> bool { + let mut ok = true; + ok &= test_builtins_vector2(); + ok &= test_builtins_array(); + ok &= test_builtins_callable(); + ok +} + +#[itest] +fn test_builtins_vector2() { + let mut vec = Vector2::new(3.0, -4.0); + let inner: InnerVector2 = vec.as_inner(); + + let len_sq = inner.length_squared(); + assert_eq!(len_sq, 25.0); + + let abs = inner.abs(); + assert_eq!(abs, Vector2::new(3.0, 4.0)); + + let normalized = inner.is_normalized(); + assert_eq!(normalized, false); +} + +#[itest] +fn test_builtins_array() { + let mut array = Array::default(); + let mut inner: InnerArray = array.as_inner(); + + let a = 7.to_variant(); + let b = GodotString::from("Seven").to_variant(); + + inner.append(a.clone()); + inner.append(b.clone()); + + assert_eq!(inner.size(), 2); + assert_eq!(inner.pop_front(), a); + assert_eq!(inner.pop_front(), b); + assert_eq!(inner.pop_front(), Variant::nil()); +} + +#[itest] +fn test_builtins_callable() { + let obj = Node2D::new_alloc(); + let mut cb = Callable::from_object_method(obj.share(), "set_position"); + let inner: InnerCallable = cb.as_inner(); + + assert_eq!(inner.is_null(), false); + assert_eq!(inner.get_object_id(), obj.instance_id().to_i64()); + assert_eq!(inner.get_method(), StringName::from("set_position")); + + // TODO once varargs is available + // let pos = Vector2::new(5.0, 7.0); + // inner.call(&[pos.to_variant()]); + // assert_eq!(obj.get_position(), pos); + // + // inner.bindv(array); + + obj.free(); +} diff --git a/itest/rust/src/lib.rs b/itest/rust/src/lib.rs index 00b62d55c..4983b8863 100644 --- a/itest/rust/src/lib.rs +++ b/itest/rust/src/lib.rs @@ -13,6 +13,7 @@ use godot::test::itest; use std::panic::UnwindSafe; mod base_test; +mod builtin_test; mod enum_test; mod export_test; mod gdscript_ffi_test; @@ -27,11 +28,12 @@ mod virtual_methods_test; fn run_tests() -> bool { let mut ok = true; ok &= base_test::run(); + ok &= builtin_test::run(); + ok &= enum_test::run(); + ok &= export_test::run(); ok &= gdscript_ffi_test::run(); ok &= node_test::run(); - ok &= enum_test::run(); ok &= object_test::run(); - ok &= export_test::run(); ok &= singleton_test::run(); ok &= string_test::run(); ok &= utilities_test::run(); From 785dd8167f16d075e107124d99e165d6b122c56e Mon Sep 17 00:00:00 2001 From: Jan Haller Date: Tue, 24 Jan 2023 21:35:15 +0100 Subject: [PATCH 24/54] More elegant solution regarding mutability of inner builtin types --- godot-codegen/src/class_generator.rs | 25 +++++++++++++------------ godot-core/src/builtin/arrays.rs | 4 ++-- godot-core/src/builtin/others.rs | 4 ++-- godot-core/src/builtin/vector2.rs | 4 ++-- itest/rust/src/builtin_test.rs | 6 +++--- 5 files changed, 22 insertions(+), 21 deletions(-) diff --git a/godot-codegen/src/class_generator.rs b/godot-codegen/src/class_generator.rs index 3289a6994..990793678 100644 --- a/godot-codegen/src/class_generator.rs +++ b/godot-codegen/src/class_generator.rs @@ -4,7 +4,7 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -//! Generates a file for each Godot class +//! Generates a file for each Godot engine + builtin class use proc_macro2::{Ident, Literal, TokenStream}; use quote::{format_ident, quote}; @@ -267,10 +267,7 @@ fn make_builtin_class( } else { panic!("Rust type `{}` categorized wrong", class.name) }; - // let opaque_name = format_ident!("Opaque{}", class.name); - // let constructor = make_constructor(class, ctx, &name_str); - let constructor = quote! {}; let class_enums = class.enums.as_ref().map(|class_enums| { class_enums .iter() @@ -289,19 +286,21 @@ fn make_builtin_class( use crate::sys::GodotFfi as _; use crate::engine::Object; - // #[derive(Debug)] #[repr(transparent)] pub struct #inner_class<'a> { - // opaque: sys::types::#opaque_name, - pub outer: &'a mut #outer_class + _outer_lifetime: std::marker::PhantomData<&'a ()>, + sys_ptr: sys::GDExtensionTypePtr, } impl<'a> #inner_class<'a> { - #constructor + pub fn from_outer(outer: &#outer_class) -> Self { + Self { + _outer_lifetime: std::marker::PhantomData, + sys_ptr: outer.sys(), + } + } + #methods } - // impl sys::GodotFfi for #class_name { - // sys::ffi_methods! { type sys::GDExtensionTypePtr = *mut Self; .. } - // } #enums }; @@ -573,11 +572,13 @@ fn make_builtin_method_definition( } let method_name_str = &method.name; + let receiver = if method.is_const { quote! { &self, } } else { quote! { &mut self, } }; + let return_value = method.return_type.as_deref().map(MethodReturn::from_type); let hash = method.hash; let is_varcall = method.is_vararg; @@ -595,7 +596,7 @@ fn make_builtin_method_definition( let __call_fn = __call_fn.unwrap_unchecked(); }; let ptrcall_invocation = quote! { - __call_fn(self.outer.sys(), __args_ptr, return_ptr, __args.len() as i32); + __call_fn(self.sys_ptr, __args_ptr, return_ptr, __args.len() as i32); }; make_function_definition( diff --git a/godot-core/src/builtin/arrays.rs b/godot-core/src/builtin/arrays.rs index af08513b8..0fdbfba7e 100644 --- a/godot-core/src/builtin/arrays.rs +++ b/godot-core/src/builtin/arrays.rs @@ -57,8 +57,8 @@ impl Array { #[cfg(not(any(gdext_test, doctest)))] #[doc(hidden)] - pub fn as_inner(&mut self) -> inner::InnerArray { - inner::InnerArray { outer: self } + pub fn as_inner(&self) -> inner::InnerArray { + inner::InnerArray::from_outer(self) } } diff --git a/godot-core/src/builtin/others.rs b/godot-core/src/builtin/others.rs index 4f39588f7..d5ef33a8a 100644 --- a/godot-core/src/builtin/others.rs +++ b/godot-core/src/builtin/others.rs @@ -63,8 +63,8 @@ impl Callable { #[cfg(not(any(gdext_test, doctest)))] #[doc(hidden)] - pub fn as_inner(&mut self) -> inner::InnerCallable { - inner::InnerCallable { outer: self } + pub fn as_inner(&self) -> inner::InnerCallable { + inner::InnerCallable::from_outer(self) } } diff --git a/godot-core/src/builtin/vector2.rs b/godot-core/src/builtin/vector2.rs index 082c655d0..334ff2853 100644 --- a/godot-core/src/builtin/vector2.rs +++ b/godot-core/src/builtin/vector2.rs @@ -87,8 +87,8 @@ impl Vector2 { #[cfg(not(any(gdext_test, doctest)))] #[doc(hidden)] - pub fn as_inner(&mut self) -> inner::InnerVector2 { - inner::InnerVector2 { outer: self } + pub fn as_inner(&self) -> inner::InnerVector2 { + inner::InnerVector2::from_outer(self) } } diff --git a/itest/rust/src/builtin_test.rs b/itest/rust/src/builtin_test.rs index 6d6442a54..1b9943f5b 100644 --- a/itest/rust/src/builtin_test.rs +++ b/itest/rust/src/builtin_test.rs @@ -18,7 +18,7 @@ pub(crate) fn run() -> bool { #[itest] fn test_builtins_vector2() { - let mut vec = Vector2::new(3.0, -4.0); + let vec = Vector2::new(3.0, -4.0); let inner: InnerVector2 = vec.as_inner(); let len_sq = inner.length_squared(); @@ -33,7 +33,7 @@ fn test_builtins_vector2() { #[itest] fn test_builtins_array() { - let mut array = Array::default(); + let array = Array::default(); let mut inner: InnerArray = array.as_inner(); let a = 7.to_variant(); @@ -51,7 +51,7 @@ fn test_builtins_array() { #[itest] fn test_builtins_callable() { let obj = Node2D::new_alloc(); - let mut cb = Callable::from_object_method(obj.share(), "set_position"); + let cb = Callable::from_object_method(obj.share(), "set_position"); let inner: InnerCallable = cb.as_inner(); assert_eq!(inner.is_null(), false); From bf50b0d77c08fc834224e8b9ab571e757179823e Mon Sep 17 00:00:00 2001 From: Thomas ten Cate Date: Wed, 25 Jan 2023 11:44:16 +0100 Subject: [PATCH 25/54] Implement basic Array functionality --- godot-core/src/builtin/arrays.rs | 466 +++++++++++++++++- godot-core/src/builtin/variant/impls.rs | 1 + godot-core/src/builtin/variant/mod.rs | 2 +- .../src/builtin/variant/variant_traits.rs | 2 +- itest/rust/src/array_test.rs | 254 ++++++++++ itest/rust/src/lib.rs | 2 + 6 files changed, 716 insertions(+), 11 deletions(-) create mode 100644 itest/rust/src/array_test.rs diff --git a/godot-core/src/builtin/arrays.rs b/godot-core/src/builtin/arrays.rs index 0fdbfba7e..d931c9504 100644 --- a/godot-core/src/builtin/arrays.rs +++ b/godot-core/src/builtin/arrays.rs @@ -6,12 +6,30 @@ use godot_ffi as sys; -use crate::builtin::{inner, FromVariant, Variant}; +use crate::builtin::{inner, FromVariant, ToVariant, Variant, VariantConversionError}; +use std::fmt; use std::marker::PhantomData; use sys::types::*; use sys::{ffi_methods, interface_fn, GodotFfi}; -impl_builtin_stub!(Array, OpaqueArray); +/// Godot's `Array` type. +/// +/// This is a variant array, meaning it contains `Variant`s which may be of different types even +/// within the same array. +/// +/// Unlike GDScript, all indices and sizes are unsigned, so negative indices are not supported. +/// +/// # Safety +/// +/// Usage is safe if the `Array` is used on a single thread only. Concurrent reads on different +/// threads are also safe, but any writes must be externally synchronized. The Rust compiler will +/// enforce this as long as you use only Rust threads, but it cannot protect against concurrent +/// modification on other threads (e.g. created through GDScript). +#[repr(C)] +pub struct Array { + opaque: sys::types::OpaqueArray, +} + impl_builtin_stub!(PackedByteArray, OpaquePackedByteArray); impl_builtin_stub!(PackedColorArray, OpaquePackedColorArray); impl_builtin_stub!(PackedFloat32Array, OpaquePackedFloat32Array); @@ -45,23 +63,437 @@ impl_builtin_froms!(PackedVector2Array; Array => packed_vector2_array_from_array impl_builtin_froms!(PackedVector3Array; Array => packed_vector3_array_from_array); impl Array { - pub fn get(&self, index: i64) -> Option { + fn from_opaque(opaque: sys::types::OpaqueArray) -> Self { + Self { opaque } + } +} + +// This impl relies on `InnerArray` which is not (yet) available in unit tests +#[cfg(not(any(gdext_test, doctest)))] +impl Array { + /// Constructs an empty `Array`. + pub fn new() -> Self { + Self::default() + } + + /// Returns the number of elements in the array. Equivalent of `size()` in Godot. + pub fn len(&self) -> usize { + to_usize(self.as_inner().size()) + } + + /// Returns `true` if the array is empty. + pub fn is_empty(&self) -> bool { + self.as_inner().is_empty() + } + + /// Returns a 32-bit integer hash value representing the array and its contents. + /// + /// Note: Arrays with equal content will always produce identical hash values. However, the + /// reverse is not true. Returning identical hash values does not imply the arrays are equal, + /// because different arrays can have identical hash values due to hash collisions. + pub fn hash(&self) -> u32 { + // The GDExtension interface only deals in `i64`, but the engine's own `hash()` function + // actually returns `uint32_t`. + self.as_inner().hash().try_into().unwrap() + } + + /// Converts this array to a strongly typed Rust vector. If the conversion from `Variant` fails + /// for any element, an error is returned. + pub fn try_to_vec(&self) -> Result, VariantConversionError> { + let len = self.len(); + let mut vec = Vec::with_capacity(len); + let ptr = self.ptr(0); + for offset in 0..to_isize(len) { + // SAFETY: Arrays are stored contiguously in memory, so we can use pointer arithmetic + // instead of going through `array_operator_index_const` for every index. + let element = unsafe { T::try_from_variant(&*ptr.offset(offset))? }; + vec.push(element); + } + Ok(vec) + } + + /// Implements iteration over an `Array` by reference, but returns (cheap) copies of the + /// `Variant`s in the array. + pub fn iter(&self) -> ArrayIterator<'_> { + ArrayIterator { + start: self.ptr(0), + len: self.len(), + next_idx: 0, + _phantom: PhantomData, + } + } + + /// Clears the array, removing all elements. + pub fn clear(&mut self) { + self.as_inner().clear(); + } + + /// Resizes the array to contain a different number of elements. If the new size is smaller, + /// elements are removed from the end. If the new size is larger, new elements are set to + /// [`Variant::nil()`]. + pub fn resize(&mut self, size: usize) { + self.as_inner().resize(to_i64(size)); + } + + // TODO: find out why this segfaults (even on an empty array, regardless of deep = true/false) + // /// Returns a deep copy of the array. All nested arrays and dictionaries are duplicated and + // /// will not be shared with the original array. Note that any `Object`-derived elements will + // /// still be shallow copied. + // /// + // /// To create a shallow copy, use `clone()` instead. + // pub fn duplicate_deep(&self) -> Self { + // self.as_inner().duplicate(true) + // } + + /// Returns the value at the specified index as a `Variant`. To convert to a specific type, use + /// the available conversion methods on `Variant`, such as [`Variant::try_to`] or + /// [`Variant::to`]. + /// + /// # Panics + /// + /// If `index` is out of bounds. + pub fn get(&self, index: usize) -> Variant { + let ptr = self.ptr(index); + // SAFETY: `ptr` just verified that the index is not out of bounds. + unsafe { (*ptr).clone() } + } + + /// Returns the first element in the array, or `None` if the array is empty. Equivalent of + /// `front()` in GDScript. + pub fn first(&self) -> Option { + (!self.is_empty()).then(|| self.as_inner().front()) + } + + /// Returns the last element in the array, or `None` if the array is empty. Equivalent of + /// `back()` in GDScript. + pub fn last(&self) -> Option { + (!self.is_empty()).then(|| self.as_inner().back()) + } + + /// Finds the index of an existing value in a sorted array using binary search. Equivalent of + /// `bsearch` in GDScript. + /// + /// If the value is not present in the array, returns the insertion index that would maintain + /// sorting order. + /// + /// Calling `binary_search` on an unsorted array results in unspecified behavior. + pub fn binary_search(&self, value: Variant) -> usize { + to_usize(self.as_inner().bsearch(value, true)) + } + + /// Returns the number of times a value is in the array. + pub fn count(&self, value: Variant) -> usize { + to_usize(self.as_inner().count(value)) + } + + /// Returns `true` if the array contains the given value. Equivalent of `has` in GDScript. + pub fn contains(&self, value: Variant) -> bool { + self.as_inner().has(value) + } + + /// Searches the array for the first occurrence of a value and returns its index, or `None` if + /// not found. Starts searching at index `from`; pass `None` to search the entire array. + pub fn find(&self, value: Variant, from: Option) -> Option { + let from = to_i64(from.unwrap_or(0)); + let index = self.as_inner().find(value, from); + if index >= 0 { + Some(index.try_into().unwrap()) + } else { + None + } + } + + /// Searches the array backwards for the last occurrence of a value and returns its index, or + /// `None` if not found. Starts searching at index `from`; pass `None` to search the entire + /// array. + pub fn rfind(&self, value: Variant, from: Option) -> Option { + let from = from.map(to_i64).unwrap_or(-1); + let index = self.as_inner().rfind(value, from); + // It's not documented, but `rfind` returns -1 if not found. + if index >= 0 { + Some(to_usize(index)) + } else { + None + } + } + + /// Returns the minimum value contained in the array if all elements are of comparable types. + /// If the elements can't be compared or the array is empty, `None` is returned. + pub fn min(&self) -> Option { + let min = self.as_inner().min(); + (!min.is_nil()).then_some(min) + } + + /// Returns the maximum value contained in the array if all elements are of comparable types. + /// If the elements can't be compared or the array is empty, `None` is returned. + pub fn max(&self) -> Option { + let max = self.as_inner().max(); + (!max.is_nil()).then_some(max) + } + + /// Returns a random element from the array, or `None` if it is empty. + pub fn pick_random(&self) -> Option { + (!self.is_empty()).then(|| self.as_inner().pick_random()) + } + + /// Sets the value at the specified index as a `Variant`. To convert a specific type (which + /// implements `ToVariant`) to a variant, call [`ToVariant::to_variant`] on it. + /// + /// # Panics + /// + /// If `index` is out of bounds. + pub fn set(&mut self, index: usize, value: Variant) { + let ptr_mut = self.ptr_mut(index); + // SAFETY: `ptr_mut` just checked that the index is not out of bounds. unsafe { - let ptr = (interface_fn!(array_operator_index))(self.sys(), index) as *mut Variant; - if ptr.is_null() { - return None; - } - Some((*ptr).clone()) + *ptr_mut = value; + } + } + + /// Appends an element to the end of the array. Equivalent of `append` and `push_back` in + /// GDScript. + pub fn push(&mut self, value: Variant) { + self.as_inner().push_back(value); + } + + /// Adds an element at the beginning of the array. See also `push`. + /// + /// Note: On large arrays, this method is much slower than `push` as it will move all the + /// array's elements. The larger the array, the slower `push_front` will be. + pub fn push_front(&mut self, value: Variant) { + self.as_inner().push_front(value); + } + + /// Removes and returns the last element of the array. Returns `None` if the array is empty. + /// Equivalent of `pop_back` in GDScript. + pub fn pop(&mut self) -> Option { + (!self.is_empty()).then(|| self.as_inner().pop_back()) + } + + /// Removes and returns the first element of the array. Returns `None` if the array is empty. + /// + /// Note: On large arrays, this method is much slower than `pop` as it will move all the + /// array's elements. The larger the array, the slower `pop_front` will be. + pub fn pop_front(&mut self) -> Option { + (!self.is_empty()).then(|| self.as_inner().pop_front()) + } + + /// Inserts a new element at a given index in the array. The index must be valid, or at the end + /// of the array (`index == len()`). + /// + /// Note: On large arrays, this method is much slower than `push` as it will move all the + /// array's elements after the inserted element. The larger the array, the slower `insert` will + /// be. + pub fn insert(&mut self, index: usize, value: Variant) { + let len = self.len(); + assert!( + index <= len, + "Array insertion index {} is out of bounds: length is {}", + index, + len + ); + self.as_inner().insert(to_i64(index), value); + } + + /// Removes and returns the element at the specified index. Equivalent of `pop_at` in GDScript. + /// + /// On large arrays, this method is much slower than `pop_back` as it will move all the array's + /// elements after the removed element. The larger the array, the slower `remove` will be. + /// + /// # Panics + /// + /// If `index` is out of bounds. + pub fn remove(&mut self, index: usize) -> Variant { + self.check_bounds(index); + self.as_inner().pop_at(to_i64(index)) + } + + /// Removes the first occurrence of a value from the array. If the value does not exist in the + /// array, nothing happens. To remove an element by index, use `remove` instead. + /// + /// On large arrays, this method is much slower than `pop_back` as it will move all the array's + /// elements after the removed element. The larger the array, the slower `remove` will be. + pub fn erase(&mut self, value: Variant) { + self.as_inner().erase(value); + } + + /// Assigns the given value to all elements in the array. This can be used together with + /// `resize` to create an array with a given size and initialized elements. + pub fn fill(&mut self, value: Variant) { + self.as_inner().fill(value); + } + + /// Appends another array at the end of this array. Equivalent of `append_array` in GDScript. + pub fn extend_array(&mut self, other: Array) { + self.as_inner().append_array(other); + } + + /// Reverses the order of the elements in the array. + pub fn reverse(&mut self) { + self.as_inner().reverse(); + } + + /// Sorts the array. + /// + /// Note: The sorting algorithm used is not + /// [stable](https://en.wikipedia.org/wiki/Sorting_algorithm#Stability). This means that values + /// considered equal may have their order changed when using `sort_unstable`. + pub fn sort_unstable(&mut self) { + self.as_inner().sort(); + } + + /// Shuffles the array such that the items will have a random order. This method uses the + /// global random number generator common to methods such as `randi`. Call `randomize` to + /// ensure that a new seed will be used each time if you want non-reproducible shuffling. + pub fn shuffle(&mut self) { + self.as_inner().shuffle(); + } + + /// Asserts that the given index refers to an existing element. + /// + /// # Panics + /// + /// If `index` is out of bounds. + fn check_bounds(&self, index: usize) { + let len = self.len(); + assert!( + index < len, + "Array index {} is out of bounds: length is {}", + index, + len + ); + } + + fn ptr(&self, index: usize) -> *const Variant { + if self.is_empty() { + std::ptr::null() + } else { + self.check_bounds(index); + // SAFETY: We just checked that the index is not out of bounds. + let ptr = unsafe { + let item_ptr: sys::GDExtensionVariantPtr = + (interface_fn!(array_operator_index_const))(self.sys(), to_i64(index)); + item_ptr as *const Variant + }; + assert!(!ptr.is_null()); + ptr + } + } + + fn ptr_mut(&self, index: usize) -> *mut Variant { + if self.is_empty() { + std::ptr::null_mut() + } else { + self.check_bounds(index); + // SAFETY: We just checked that the index is not out of bounds. + let ptr = unsafe { + let item_ptr: sys::GDExtensionVariantPtr = + (interface_fn!(array_operator_index))(self.sys(), to_i64(index)); + item_ptr as *mut Variant + }; + assert!(!ptr.is_null()); + ptr } } - #[cfg(not(any(gdext_test, doctest)))] #[doc(hidden)] pub fn as_inner(&self) -> inner::InnerArray { inner::InnerArray::from_outer(self) } } +/// Creates an `Array` from the given Rust array. Each element is converted to a `Variant`. +#[cfg(not(any(gdext_test, doctest)))] +impl From<&[T; N]> for Array { + fn from(arr: &[T; N]) -> Self { + Self::from(&arr[..]) + } +} + +/// Creates an `Array` from the given slice. Each element is converted to a `Variant`. +#[cfg(not(any(gdext_test, doctest)))] +impl From<&[T]> for Array { + fn from(slice: &[T]) -> Self { + let mut array = Self::new(); + let len = slice.len(); + if len == 0 { + return array; + } + array.resize(len); + let ptr = array.ptr_mut(0); + for (i, element) in slice.iter().enumerate() { + // SAFETY: The array contains exactly `len` elements, stored contiguously in memory. + unsafe { + *ptr.offset(to_isize(i)) = element.to_variant(); + } + } + array + } +} + +/// Creates an `Array` from an iterator. Each element is converted to a `Variant`. +#[cfg(not(any(gdext_test, doctest)))] +impl FromIterator for Array { + fn from_iter>(iter: I) -> Self { + let mut array = Array::new(); + array.extend(iter); + array + } +} + +/// Extends an `Array` with the contents of an iterator. Each element is converted to a `Variant`. +#[cfg(not(any(gdext_test, doctest)))] +impl Extend for Array { + fn extend>(&mut self, iter: I) { + // Unfortunately the GDExtension API does not offer the equivalent of `Vec::reserve`. + // Otherwise we could use it to pre-allocate based on `iter.size_hint()`. + // + // A faster implementation using `resize()` and direct pointer writes might still be + // possible. + for item in iter.into_iter() { + self.push(item.to_variant()); + } + } +} + +/// See [`Array::iter()`]. +#[cfg(not(any(gdext_test, doctest)))] +impl<'a> IntoIterator for &'a Array { + type Item = Variant; + type IntoIter = ArrayIterator<'a>; + fn into_iter(self) -> Self::IntoIter { + self.iter() + } +} + +pub struct ArrayIterator<'a> { + start: *const Variant, + len: usize, + next_idx: usize, + _phantom: PhantomData<&'a Array>, +} + +impl<'a> Iterator for ArrayIterator<'a> { + type Item = Variant; + + fn next(&mut self) -> Option { + if self.next_idx < self.len { + let offset = to_isize(self.next_idx); + self.next_idx += 1; + unsafe { Some((*self.start.offset(offset)).clone()) } + } else { + None + } + } +} + +impl fmt::Debug for Array { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + // Going through `Variant` because there doesn't seem to be a direct way. + write!(f, "{:?}", self.to_variant().stringify()) + } +} + impl_builtin_traits! { for Array { Default => array_construct_default; @@ -70,6 +502,22 @@ impl_builtin_traits! { } } +impl GodotFfi for Array { + ffi_methods! { type sys::GDExtensionTypePtr = *mut Opaque; .. } +} + +fn to_i64(i: usize) -> i64 { + i.try_into().unwrap() +} + +fn to_usize(i: i64) -> usize { + i.try_into().unwrap() +} + +fn to_isize(i: usize) -> isize { + i.try_into().unwrap() +} + #[repr(C)] pub struct TypedArray { opaque: OpaqueArray, diff --git a/godot-core/src/builtin/variant/impls.rs b/godot-core/src/builtin/variant/impls.rs index e90195e03..a89aa48e1 100644 --- a/godot-core/src/builtin/variant/impls.rs +++ b/godot-core/src/builtin/variant/impls.rs @@ -141,6 +141,7 @@ mod impls { impl_variant_traits!(Color, color_to_variant, color_from_variant, Color); impl_variant_traits!(GodotString, string_to_variant, string_from_variant, String); impl_variant_traits!(StringName, string_name_to_variant, string_name_from_variant, StringName); + impl_variant_traits!(Array, array_to_variant, array_from_variant, Array); impl_variant_traits!(i64, int_to_variant, int_from_variant, Int, GDEXTENSION_METHOD_ARGUMENT_METADATA_INT_IS_INT64); diff --git a/godot-core/src/builtin/variant/mod.rs b/godot-core/src/builtin/variant/mod.rs index ed3b6e047..72ccb2d9c 100644 --- a/godot-core/src/builtin/variant/mod.rs +++ b/godot-core/src/builtin/variant/mod.rs @@ -101,7 +101,7 @@ impl Variant { } #[allow(unused_mut)] - fn stringify(&self) -> GodotString { + pub(crate) fn stringify(&self) -> GodotString { let mut result = GodotString::new(); unsafe { interface_fn!(variant_stringify)(self.var_sys(), result.string_sys()); diff --git a/godot-core/src/builtin/variant/variant_traits.rs b/godot-core/src/builtin/variant/variant_traits.rs index 8ead0287f..bb2f0d53e 100644 --- a/godot-core/src/builtin/variant/variant_traits.rs +++ b/godot-core/src/builtin/variant/variant_traits.rs @@ -40,7 +40,7 @@ pub trait ToVariant { // ---------------------------------------------------------------------------------------------------------------------------------------------- -#[derive(Debug)] +#[derive(Debug, Eq, PartialEq)] pub struct VariantConversionError; /*pub enum VariantConversionError { /// Variant type does not match expected type diff --git a/itest/rust/src/array_test.rs b/itest/rust/src/array_test.rs new file mode 100644 index 000000000..08e1f1b27 --- /dev/null +++ b/itest/rust/src/array_test.rs @@ -0,0 +1,254 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +use crate::{expect_panic, itest}; +use godot::builtin::{Array, FromVariant, GodotString, ToVariant}; + +pub fn run() -> bool { + let mut ok = true; + ok &= array_default(); + ok &= array_new(); + ok &= array_from_iterator(); + ok &= array_from(); + ok &= array_try_to_vec(); + ok &= array_into_iterator(); + ok &= array_clone(); + // ok &= array_duplicate_deep(); + ok &= array_hash(); + ok &= array_get(); + ok &= array_first_last(); + ok &= array_binary_search(); + ok &= array_find(); + ok &= array_rfind(); + ok &= array_min_max(); + ok &= array_set(); + ok &= array_push_pop(); + ok &= array_insert(); + ok &= array_extend(); + ok &= array_reverse(); + ok &= array_sort(); + ok &= array_shuffle(); + ok +} + +#[itest] +fn array_default() { + assert_eq!(Array::default().len(), 0); +} + +#[itest] +fn array_new() { + assert_eq!(Array::new().len(), 0); +} + +#[itest] +fn array_from_iterator() { + let array = Array::from_iter([1, 2]); + + assert_eq!(array.len(), 2); + assert_eq!(array.get(0), 1.to_variant()); + assert_eq!(array.get(1), 2.to_variant()); +} + +#[itest] +fn array_from() { + let array = Array::from(&[1, 2]); + + assert_eq!(array.len(), 2); + assert_eq!(array.get(0), 1.to_variant()); + assert_eq!(array.get(1), 2.to_variant()); +} + +#[itest] +fn array_try_to_vec() { + let array = Array::from(&[1, 2]); + assert_eq!(array.try_to_vec::(), Ok(vec![1, 2])); +} + +#[itest] +fn array_into_iterator() { + let array = Array::from(&[1, 2]); + let mut iter = array.into_iter(); + assert_eq!(iter.next(), Some(1.to_variant())); + assert_eq!(iter.next(), Some(2.to_variant())); + assert_eq!(iter.next(), None); +} + +#[itest] +fn array_clone() { + let subarray = Array::from(&[2, 3]); + let array = Array::from(&[1.to_variant(), subarray.to_variant()]); + #[allow(clippy::redundant_clone)] + let clone = array.clone(); + Array::try_from_variant(&clone.get(1)) + .unwrap() + .set(0, 4.to_variant()); + assert_eq!(subarray.get(0), 4.to_variant()); +} + +#[itest] +fn array_hash() { + let array = Array::from(&[1, 2]); + // Just testing that it converts successfully from i64 to u32. + array.hash(); +} + +// TODO: enable once the implementation no longer segfaults +// #[itest] +// fn array_duplicate_deep() { +// let subarray = Array::from(&[2, 3]); +// let array = Array::from(&[1.to_variant(), subarray.to_variant()]); +// let mut clone = array.duplicate_deep(); +// Array::try_from_variant(clone.get(1)).unwrap().set(0, 4.to_variant()); +// assert_eq!(subarray.get(0), 3.to_variant()); +// } + +#[itest] +fn array_get() { + let array = Array::from(&[1, 2]); + + assert_eq!(array.get(0), 1.to_variant()); + assert_eq!(array.get(1), 2.to_variant()); + expect_panic("Array index 2 out of bounds: length is 2", || { + array.get(2); + }); +} + +#[itest] +fn array_first_last() { + let array = Array::from(&[1, 2]); + + assert_eq!(array.first(), Some(1.to_variant())); + assert_eq!(array.last(), Some(2.to_variant())); + + let empty_array = Array::new(); + + assert_eq!(empty_array.first(), None); + assert_eq!(empty_array.last(), None); +} + +#[itest] +fn array_binary_search() { + let array = Array::from(&[1, 2]); + + assert_eq!(array.binary_search(0.to_variant()), 0); + assert_eq!(array.binary_search(1.to_variant()), 0); + assert_eq!(array.binary_search(1.5f64.to_variant()), 1); + assert_eq!(array.binary_search(2.to_variant()), 1); + assert_eq!(array.binary_search(3.to_variant()), 2); +} + +#[itest] +fn array_find() { + let array = Array::from(&[1, 2, 1]); + + assert_eq!(array.find(0.to_variant(), None), None); + assert_eq!(array.find(1.to_variant(), None), Some(0)); + assert_eq!(array.find(1.to_variant(), Some(1)), Some(2)); +} + +#[itest] +fn array_rfind() { + let array = Array::from(&[1, 2, 1]); + + assert_eq!(array.rfind(0.to_variant(), None), None); + assert_eq!(array.rfind(1.to_variant(), None), Some(2)); + assert_eq!(array.rfind(1.to_variant(), Some(1)), Some(0)); +} + +#[itest] +fn array_min_max() { + let int_array = Array::from(&[1, 2]); + + assert_eq!(int_array.min(), Some(1.to_variant())); + assert_eq!(int_array.max(), Some(2.to_variant())); + + let uncomparable_array = Array::from(&[1.to_variant(), GodotString::from("two").to_variant()]); + + assert_eq!(uncomparable_array.min(), None); + assert_eq!(uncomparable_array.max(), None); + + let empty_array = Array::new(); + + assert_eq!(empty_array.min(), None); + assert_eq!(empty_array.max(), None); +} + +#[itest] +fn array_pick_random() { + assert_eq!(Array::new().pick_random(), None); + assert_eq!(Array::from(&[1]).pick_random(), Some(1.to_variant())); +} + +#[itest] +fn array_set() { + let mut array = Array::from(&[1, 2]); + + array.set(0, 3.to_variant()); + assert_eq!(array.get(0), 3.to_variant()); + + expect_panic("Array index 2 out of bounds: length is 2", move || { + array.set(2, 4.to_variant()); + }); +} + +#[itest] +fn array_push_pop() { + let mut array = Array::from(&[1, 2]); + + array.push(3.to_variant()); + assert_eq!(array.pop(), Some(3.to_variant())); + + array.push_front(4.to_variant()); + assert_eq!(array.pop_front(), Some(4.to_variant())); + + assert_eq!(array.pop(), Some(2.to_variant())); + assert_eq!(array.pop_front(), Some(1.to_variant())); + + assert_eq!(array.pop(), None); + assert_eq!(array.pop_front(), None); +} + +#[itest] +fn array_insert() { + let mut array = Array::from(&[1, 2]); + + array.insert(0, 3.to_variant()); + assert_eq!(array.try_to_vec::().unwrap(), vec![3, 1, 2]); + + array.insert(3, 4.to_variant()); + assert_eq!(array.try_to_vec::().unwrap(), vec![3, 1, 2, 4]); +} + +#[itest] +fn array_extend() { + let mut array = Array::from(&[1, 2]); + let other = Array::from(&[3, 4]); + array.extend_array(other); + assert_eq!(array.try_to_vec::().unwrap(), vec![1, 2, 3, 4]); +} + +#[itest] +fn array_sort() { + let mut array = Array::from(&[2, 1]); + array.sort_unstable(); + assert_eq!(array.try_to_vec::().unwrap(), vec![1, 2]); +} + +#[itest] +fn array_reverse() { + let mut array = Array::from(&[1, 2]); + array.reverse(); + assert_eq!(array.try_to_vec::().unwrap(), vec![2, 1]); +} + +#[itest] +fn array_shuffle() { + // Since the output is random, we just test that it doesn't crash. + let mut array = Array::from(&[1i64]); + array.shuffle(); + assert_eq!(array.try_to_vec::().unwrap(), vec![1]); +} diff --git a/itest/rust/src/lib.rs b/itest/rust/src/lib.rs index 4983b8863..669df563a 100644 --- a/itest/rust/src/lib.rs +++ b/itest/rust/src/lib.rs @@ -12,6 +12,7 @@ use godot::init::{gdextension, ExtensionLibrary}; use godot::test::itest; use std::panic::UnwindSafe; +mod array_test; mod base_test; mod builtin_test; mod enum_test; @@ -36,6 +37,7 @@ fn run_tests() -> bool { ok &= object_test::run(); ok &= singleton_test::run(); ok &= string_test::run(); + ok &= array_test::run(); ok &= utilities_test::run(); ok &= variant_test::run(); ok &= virtual_methods_test::run(); From d596e020bf17480aec3dc344020a554497042117 Mon Sep 17 00:00:00 2001 From: Jan Haller Date: Wed, 25 Jan 2023 11:36:03 +0100 Subject: [PATCH 26/54] Map Godot class identifiers to Rust ones (e.g. PCKPacker -> PckPacker) --- godot-codegen/Cargo.toml | 2 +- godot-codegen/src/central_generator.rs | 30 +++--- godot-codegen/src/class_generator.rs | 93 ++++++++-------- godot-codegen/src/context.rs | 20 ++-- godot-codegen/src/special_cases.rs | 29 +++-- godot-codegen/src/tests.rs | 143 ++++++++++++++----------- godot-codegen/src/util.rs | 116 ++++++-------------- godot-core/src/log.rs | 3 + itest/rust/src/singleton_test.rs | 8 +- 9 files changed, 219 insertions(+), 225 deletions(-) diff --git a/godot-codegen/Cargo.toml b/godot-codegen/Cargo.toml index 9ef07e5c7..7f1bdeb3f 100644 --- a/godot-codegen/Cargo.toml +++ b/godot-codegen/Cargo.toml @@ -15,7 +15,7 @@ codegen-full = [] quote = "1" proc-macro2 = "1" which = "4" -#heck = "0.4" +heck = "0.4" # Version >= 1.5.5 for security: https://blog.rust-lang.org/2022/03/08/cve-2022-24713.html # 'unicode-gencat' needed for \d, see: https://docs.rs/regex/1.5.5/regex/#unicode-features diff --git a/godot-codegen/src/central_generator.rs b/godot-codegen/src/central_generator.rs index 0fa08af5e..8791cdc93 100644 --- a/godot-codegen/src/central_generator.rs +++ b/godot-codegen/src/central_generator.rs @@ -10,7 +10,7 @@ use std::collections::HashMap; use std::path::{Path, PathBuf}; use crate::api_parser::*; -use crate::util::to_rust_type; +use crate::util::{to_rust_type, to_snake_case}; use crate::{ident, util, Context}; struct CentralItems { @@ -26,8 +26,8 @@ struct CentralItems { } pub(crate) struct TypeNames { - /// "int" or "PackedVector2Array" - pub pascal_case: String, + /// Name in JSON: "int" or "PackedVector2Array" + pub json_builtin_name: String, /// "packed_vector2_array" pub snake_case: String, @@ -379,6 +379,8 @@ fn make_central_items(api: &ExtensionApi, build_config: &str, ctx: &mut Context) result } +/// Creates a map from "normalized" class names (lowercase without underscore, makes it easy to map from different conventions) +/// to meta type information, including all the type name variants fn collect_builtin_classes(api: &ExtensionApi) -> HashMap { let mut class_map = HashMap::new(); for class in &api.builtin_classes { @@ -417,26 +419,26 @@ pub(crate) fn collect_builtin_types(api: &ExtensionApi) -> HashMap>; let operators: Option<&Vec>; if let Some(class) = class_map.get(&normalized) { - pascal_case = class.name.clone(); + class_name = class.name.clone(); has_destructor = class.has_destructor; constructors = Some(&class.constructors); operators = Some(&class.operators); } else { assert_eq!(normalized, "object"); - pascal_case = "Object".to_string(); + class_name = "Object".to_string(); has_destructor = false; constructors = None; operators = None; } let type_names = TypeNames { - pascal_case, - snake_case: shout_case.to_ascii_lowercase(), + json_builtin_name: class_name.clone(), + snake_case: to_snake_case(&class_name), //shout_case: shout_case.to_string(), sys_variant_type: format_ident!("GDEXTENSION_VARIANT_TYPE_{}", shout_case), }; @@ -444,7 +446,7 @@ pub(crate) fn collect_builtin_types(api: &ExtensionApi) -> HashMap (Ident, TokenStream, Literal) { //let shout_name = format_ident!("{}", type_names.shout_case); - let (first, rest) = type_names.pascal_case.split_at(1); + let (first, rest) = type_names.json_builtin_name.split_at(1); let pascal_name = format_ident!("{}{}", first.to_ascii_uppercase(), rest); - let rust_ty = to_rust_type(&type_names.pascal_case, ctx); + let rust_ty = to_rust_type(&type_names.json_builtin_name, ctx); let ord = Literal::i32_unsuffixed(value); (pascal_name, rust_ty.to_token_stream(), ord) @@ -574,11 +576,11 @@ fn make_construct_fns( if let Some(args) = &constructors[1].arguments { assert_eq!(args.len(), 1); assert_eq!(args[0].name, "from"); - assert_eq!(args[0].type_, type_names.pascal_case); + assert_eq!(args[0].type_, type_names.json_builtin_name); } else { panic!( "type {}: no constructor args found for copy constructor", - type_names.pascal_case + type_names.json_builtin_name ); } @@ -734,7 +736,7 @@ fn format_load_error(ident: &impl std::fmt::Display) -> String { fn is_trivial(type_names: &TypeNames) -> bool { let list = ["bool", "int", "float"]; - list.contains(&type_names.pascal_case.as_str()) + list.contains(&type_names.json_builtin_name.as_str()) } fn shout_to_pascal(shout_case: &str) -> String { diff --git a/godot-codegen/src/class_generator.rs b/godot-codegen/src/class_generator.rs index 990793678..80894a11b 100644 --- a/godot-codegen/src/class_generator.rs +++ b/godot-codegen/src/class_generator.rs @@ -6,13 +6,13 @@ //! Generates a file for each Godot engine + builtin class -use proc_macro2::{Ident, Literal, TokenStream}; +use proc_macro2::{Ident, TokenStream}; use quote::{format_ident, quote}; use std::path::{Path, PathBuf}; use crate::api_parser::*; use crate::central_generator::{collect_builtin_types, BuiltinTypeInfo}; -use crate::util::{ident, safe_ident, strlit, to_module_name, to_rust_type}; +use crate::util::{ident, make_class_name, make_module_name, safe_ident, to_pascal_case, to_rust_type}; use crate::{ special_cases, util, Context, GeneratedBuiltin, GeneratedBuiltinModule, GeneratedClass, GeneratedClassModule, RustTy, @@ -39,19 +39,18 @@ pub(crate) fn generate_class_files( continue; } - let generated_class = make_class(class, ctx); + let rust_class = make_class_name(&class.name); + let rust_module = make_module_name(&class.name); + let generated_class = make_class(class, &rust_class, ctx); let file_contents = generated_class.tokens.to_string(); - let module_name = to_module_name(&class.name); - let out_path = gen_path.join(format!("{module_name}.rs")); + let out_path = gen_path.join(format!("{rust_module}.rs")); std::fs::write(&out_path, file_contents).expect("failed to write class file"); out_files.push(out_path); - let class_ident = ident(&class.name); - let module_ident = ident(&module_name); modules.push(GeneratedClassModule { - class_ident, - module_ident, + class_ident: rust_class, + module_ident: rust_module, inherits_macro_ident: generated_class.inherits_macro_ident, is_pub: generated_class.has_pub_module, }); @@ -87,13 +86,12 @@ pub(crate) fn generate_builtin_class_files( let inner_class = format_ident!("Inner{}", class.name); let generated_class = make_builtin_class(class, &inner_class, type_info, ctx); let file_contents = generated_class.tokens.to_string(); + let module_ident = make_module_name(&class.name); - let module_name = to_module_name(&class.name); - let out_path = gen_path.join(format!("{module_name}.rs")); + let out_path = gen_path.join(format!("{module_ident}.rs")); std::fs::write(&out_path, file_contents).expect("failed to write class file"); out_files.push(out_path); - let module_ident = ident(&module_name); modules.push(GeneratedBuiltinModule { class_ident: inner_class, module_ident, @@ -106,8 +104,9 @@ pub(crate) fn generate_builtin_class_files( out_files.push(out_path); } -fn make_constructor(class: &Class, ctx: &Context, class_name_str: &Literal) -> TokenStream { - if ctx.is_singleton(&class.name) { +fn make_constructor(class: &Class, ctx: &Context) -> TokenStream { + let godot_class_name = &class.name; + if ctx.is_singleton(godot_class_name) { // Note: we cannot return &'static mut Self, as this would be very easy to mutably alias. // &'static Self would be possible, but we would lose the whole mutability information (even if that // is best-effort and not strict Rust mutability, it makes the API much more usable). @@ -116,7 +115,7 @@ fn make_constructor(class: &Class, ctx: &Context, class_name_str: &Literal) -> T quote! { pub fn singleton() -> Gd { unsafe { - let __class_name = StringName::from(#class_name_str); + let __class_name = StringName::from(#godot_class_name); let __object_ptr = sys::interface_fn!(global_get_singleton)(__class_name.string_sys()); Gd::from_obj_sys(__object_ptr) } @@ -130,7 +129,7 @@ fn make_constructor(class: &Class, ctx: &Context, class_name_str: &Literal) -> T quote! { pub fn new() -> Gd { unsafe { - let __class_name = StringName::from(#class_name_str); + let __class_name = StringName::from(#godot_class_name); let __object_ptr = sys::interface_fn!(classdb_construct_object)(__class_name.string_sys()); //let instance = Self { object_ptr }; Gd::from_obj_sys(__object_ptr) @@ -143,7 +142,7 @@ fn make_constructor(class: &Class, ctx: &Context, class_name_str: &Literal) -> T #[must_use] pub fn new_alloc() -> Gd { unsafe { - let __class_name = StringName::from(#class_name_str); + let __class_name = StringName::from(#godot_class_name); let __object_ptr = sys::interface_fn!(classdb_construct_object)(__class_name.string_sys()); Gd::from_obj_sys(__object_ptr) } @@ -152,27 +151,28 @@ fn make_constructor(class: &Class, ctx: &Context, class_name_str: &Literal) -> T } } -fn make_class(class: &Class, ctx: &mut Context) -> GeneratedClass { - //let sys = TokenStream::from_str("::godot_ffi"); +fn make_class(class: &Class, rust_class:&Ident, ctx: &mut Context) -> GeneratedClass { + // Strings + let godot_class_name = &class.name; + + // Idents and tokens let base = match class.inherits.as_ref() { Some(base) => { - let base = ident(base); + let base = ident(&to_pascal_case(base)); quote! { crate::engine::#base } } None => quote! { () }, }; - let name = ident(&class.name); - let name_str = strlit(&class.name); - - let constructor = make_constructor(class, ctx, &name_str); + let constructor = make_constructor(class, ctx); + let methods = make_methods(&class.methods, &godot_class_name, ctx); + let enums = make_enums(&class.enums, &godot_class_name, ctx); + let inherits_macro = format_ident!("inherits_transitive_{}", rust_class); + let all_bases = ctx + .inheritance_tree() + .collect_all_bases(rust_class); - let methods = make_methods(&class.methods, &class.name, ctx); - let enums = make_enums(&class.enums, &class.name, ctx); - let inherits_macro = format_ident!("inherits_transitive_{}", &class.name); - let all_bases = ctx.inheritance_tree().map_all_bases(&class.name, ident); - - let memory = if &class.name == "Object" { + let memory = if rust_class == "Object" { ident("DynamicRefCount") } else if class.is_refcounted { ident("StaticRefCount") @@ -193,21 +193,21 @@ fn make_class(class: &Class, ctx: &mut Context) -> GeneratedClass { #[derive(Debug)] #[repr(transparent)] - pub struct #name { + pub struct #rust_class { object_ptr: sys::GDExtensionObjectPtr, } - impl #name { + impl #rust_class { #constructor #methods } - impl crate::obj::GodotClass for #name { + impl crate::obj::GodotClass for #rust_class { type Base = #base; type Declarer = crate::obj::dom::EngineDomain; type Mem = crate::obj::mem::#memory; - const CLASS_NAME: &'static str = #name_str; + const CLASS_NAME: &'static str = #godot_class_name; } - impl crate::obj::EngineClass for #name { + impl crate::obj::EngineClass for #rust_class { fn as_object_ptr(&self) -> sys::GDExtensionObjectPtr { self.object_ptr } @@ -216,9 +216,9 @@ fn make_class(class: &Class, ctx: &mut Context) -> GeneratedClass { } } #( - impl crate::obj::Inherits for #name {} + impl crate::obj::Inherits for #rust_class {} )* - impl std::ops::Deref for #name { + impl std::ops::Deref for #rust_class { type Target = #base; fn deref(&self) -> &Self::Target { @@ -226,7 +226,7 @@ fn make_class(class: &Class, ctx: &mut Context) -> GeneratedClass { unsafe { std::mem::transmute::<&Self, &Self::Target>(self) } } } - impl std::ops::DerefMut for #name { + impl std::ops::DerefMut for #rust_class { fn deref_mut(&mut self) -> &mut Self::Target { // SAFETY: see above unsafe { std::mem::transmute::<&mut Self, &mut Self::Target>(self) } @@ -237,7 +237,7 @@ fn make_class(class: &Class, ctx: &mut Context) -> GeneratedClass { #[allow(non_snake_case)] macro_rules! #inherits_macro { ($Class:ident) => { - impl ::godot::obj::Inherits<::godot::engine::#name> for $Class {} + impl ::godot::obj::Inherits<::godot::engine::#rust_class> for $Class {} #( impl ::godot::obj::Inherits<::godot::engine::#all_bases> for $Class {} )* @@ -371,7 +371,7 @@ fn make_builtin_module_file(classes_and_modules: Vec) -> fn make_methods( methods: &Option>, - class_name: &str, + godot_class_name: &str, ctx: &mut Context, ) -> TokenStream { let methods = match methods { @@ -381,7 +381,7 @@ fn make_methods( let definitions = methods .iter() - .map(|method| make_method_definition(method, class_name, ctx)); + .map(|method| make_method_definition(method, godot_class_name, ctx)); quote! { #( #definitions )* @@ -496,10 +496,11 @@ fn is_function_excluded(function: &UtilityFunction, ctx: &mut Context) -> bool { fn make_method_definition( method: &ClassMethod, - class_name: &str, + godot_class_name: &str, ctx: &mut Context, ) -> TokenStream { - if is_method_excluded(method, ctx) || special_cases::is_deleted(class_name, &method.name) { + if is_method_excluded(method, ctx) || special_cases::is_deleted(godot_class_name, &method.name) + { return TokenStream::new(); } /*if method.map_args(|args| args.is_empty()) { @@ -513,7 +514,7 @@ fn make_method_definition( } }*/ - let method_name_str = special_cases::maybe_renamed(class_name, &method.name); + let method_name_str = special_cases::maybe_renamed(godot_class_name, &method.name); let receiver = if method.is_const { quote! { &self, } } else { @@ -530,7 +531,7 @@ fn make_method_definition( }; let init_code = quote! { - let __class_name = StringName::from(#class_name); + let __class_name = StringName::from(#godot_class_name); let __method_name = StringName::from(#method_name_str); let __method_bind = sys::interface_fn!(classdb_get_method_bind)( __class_name.string_sys(), @@ -548,7 +549,7 @@ fn make_method_definition( make_function_definition( method_name_str, - special_cases::is_private(class_name, &method.name), + special_cases::is_private(godot_class_name, &method.name), receiver, &method.arguments, method.return_value.as_ref(), diff --git a/godot-codegen/src/context.rs b/godot-codegen/src/context.rs index 395629b61..1f56f88bf 100644 --- a/godot-codegen/src/context.rs +++ b/godot-codegen/src/context.rs @@ -6,6 +6,8 @@ use crate::{ExtensionApi, RustTy}; use std::collections::{HashMap, HashSet}; +use proc_macro2::Ident; +use crate::util::make_class_name; #[derive(Default)] pub(crate) struct Context<'a> { @@ -18,6 +20,7 @@ pub(crate) struct Context<'a> { impl<'a> Context<'a> { pub fn build_from_api(api: &'a ExtensionApi) -> Self { + // TODO possibly add a data structure containing both Godot JSON ident and Rust mapped one let mut ctx = Context::default(); for class in api.singletons.iter() { @@ -44,7 +47,7 @@ impl<'a> Context<'a> { if let Some(base) = class.inherits.as_ref() { println!(" -- inherits {base}"); ctx.inheritance_tree - .insert(class_name.to_string(), base.clone()); + .insert(make_class_name(class_name), make_class_name(base)); } } ctx @@ -76,23 +79,24 @@ impl<'a> Context<'a> { } } +/// Maintains class hierarchy. Uses Rust class names, not Godot ones. #[derive(Default)] pub(crate) struct InheritanceTree { - derived_to_base: HashMap, + derived_to_base: HashMap, } impl InheritanceTree { - pub fn insert(&mut self, derived: String, base: String) { - let existing = self.derived_to_base.insert(derived, base); + pub fn insert(&mut self, rust_derived: Ident, rust_base: Ident) { + let existing = self.derived_to_base.insert(rust_derived, rust_base); assert!(existing.is_none(), "Duplicate inheritance insert"); } - pub fn map_all_bases(&self, derived: &str, apply: impl Fn(&str) -> T) -> Vec { - let mut maybe_base = derived; + pub fn collect_all_bases(&self, rust_derived: &Ident) -> Vec { + let mut maybe_base = rust_derived; let mut result = vec![]; - while let Some(base) = self.derived_to_base.get(maybe_base).map(String::as_str) { - result.push(apply(base)); + while let Some(base) = self.derived_to_base.get(maybe_base) { + result.push(base.clone()); maybe_base = base; } result diff --git a/godot-codegen/src/special_cases.rs b/godot-codegen/src/special_cases.rs index 4ed1a4262..944ff3cfa 100644 --- a/godot-codegen/src/special_cases.rs +++ b/godot-codegen/src/special_cases.rs @@ -13,9 +13,11 @@ // * The deleted/private methods and classes deemed "dangerous" may be provided later as unsafe functions -- our safety model // needs to first mature a bit. +// NOTE: the identifiers used here operate on the GODOT types (e.g. AABB, not Aabb) + #[rustfmt::skip] -pub fn is_deleted(class_name: &str, method_name: &str) -> bool { - match (class_name, method_name) { +pub fn is_deleted(godot_class_name: &str, godot_method_name: &str) -> bool { + match (godot_class_name, godot_method_name) { // Already covered by manual APIs //| ("Object", "to_string") | ("Object", "get_instance_id") @@ -31,8 +33,8 @@ pub fn is_deleted(class_name: &str, method_name: &str) -> bool { } #[rustfmt::skip] -pub fn is_class_deleted(class_name: &str) -> bool { - match class_name { +pub fn is_class_deleted(godot_class_name: &str) -> bool { + match godot_class_name { // Thread APIs | "Thread" | "Mutex" @@ -43,8 +45,8 @@ pub fn is_class_deleted(class_name: &str) -> bool { } #[rustfmt::skip] -pub fn is_private(class_name: &str, method_name: &str) -> bool { - match (class_name, method_name) { +pub fn is_private(godot_class_name: &str, godot_method_name: &str) -> bool { + match (godot_class_name, godot_method_name) { // Already covered by manual APIs | ("Object", "to_string") | ("RefCounted", "init_ref") @@ -55,14 +57,19 @@ pub fn is_private(class_name: &str, method_name: &str) -> bool { } } -pub fn is_builtin_type_deleted(class_name: &str) -> bool { - class_name == "Nil" || class_name.chars().next().unwrap().is_ascii_lowercase() +pub fn is_builtin_type_deleted(godot_class_name: &str) -> bool { + godot_class_name == "Nil" + || godot_class_name + .chars() + .next() + .unwrap() + .is_ascii_lowercase() } -pub fn maybe_renamed<'m>(class_name: &str, method_name: &'m str) -> &'m str { - match (class_name, method_name) { +pub fn maybe_renamed<'m>(godot_class_name: &str, godot_method_name: &'m str) -> &'m str { + match (godot_class_name, godot_method_name) { // GDScript, GDScriptNativeClass, possibly more in the future (_, "new") => "instantiate", - _ => method_name, + _ => godot_method_name, } } diff --git a/godot-codegen/src/tests.rs b/godot-codegen/src/tests.rs index d17fc85ac..cc3e8d23b 100644 --- a/godot-codegen/src/tests.rs +++ b/godot-codegen/src/tests.rs @@ -4,75 +4,98 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -use crate::util::to_module_name; +use crate::util::{to_pascal_case, to_snake_case}; #[test] -fn module_name_generator() { - let tests = vec![ - // A number of test cases to cover some possibilities: - // * Underscores are removed - // * First character is always lowercased - // * lowercase to an uppercase inserts an underscore - // - FooBar => foo_bar - // * two capital letter words does not separate the capital letters: - // - FooBBaz => foo_bbaz (lower, cap, cap, lower) - // * many-capital letters to lowercase inserts an underscore before the last uppercase letter: - // - FOOBar => boo_bar - // underscores - ("Ab_Cdefg", "ab_cdefg"), - ("_Abcd", "abcd"), - ("Abcd_", "abcd"), - // first and last - ("Abcdefg", "abcdefg"), - ("abcdefG", "abcdef_g"), - // more than 2 caps - ("ABCDefg", "abc_defg"), - ("AbcDEFg", "abc_de_fg"), - ("AbcdEF10", "abcd_ef10"), - ("AbcDEFG", "abc_defg"), - ("ABCDEFG", "abcdefg"), - ("ABC", "abc"), - // Lowercase to an uppercase - ("AbcDefg", "abc_defg"), - // Only 2 caps - ("ABcdefg", "abcdefg"), - ("ABcde2G", "abcde_2g"), - ("AbcDEfg", "abc_defg"), - ("ABcDe2G", "abc_de_2g"), - ("abcdeFG", "abcde_fg"), - ("AB", "ab"), - // Lowercase to an uppercase - ("AbcdefG", "abcdef_g"), // PosX => pos_x - // text changes - ("FooVec3Uni", "foo_vec3_uni"), - ("GDExtension", "gdextension_"), - ("GDScript", "gdscript"), +fn test_pascal_conversion() { + // More in line with Rust identifiers, and eases recognition of other automation (like enumerator mapping). + #[rustfmt::skip] + let mappings = [ + ("AABB", "Aabb"), + ("AESContext", "AesContext"), + ("AStar3D", "AStar3D"), + ("AudioEffectEQ21", "AudioEffectEq21"), + ("AudioStreamWAV", "AudioStreamWav"), + ("CharFXTransform", "CharFxTransform"), + ("CPUParticles3D", "CpuParticles3D"), + ("EditorSceneImporterGLTF", "EditorSceneImporterGltf"), + ("GIProbe", "GiProbe"), + ("HMACContext", "HmacContext"), + ("HSeparator", "HSeparator"), + ("IP", "Ip"), + ("JNISingleton", "JniSingleton"), + ("JSON", "Json"), + ("JSONParseResult", "JsonParseResult"), + ("JSONRPC", "JsonRpc"), + ("NetworkedMultiplayerENet", "NetworkedMultiplayerENet"), + ("ObjectID", "ObjectId"), + ("PackedFloat32Array", "PackedFloat32Array"), + ("PCKPacker", "PckPacker"), + ("PHashTranslation", "PHashTranslation"), + ("PhysicsServer2DExtensionRayResult", "PhysicsServer2DExtensionRayResult"), + ("Rect2", "Rect2"), + ("Rect2i", "Rect2i"), + ("RID", "Rid"), + ("StreamPeerSSL", "StreamPeerSsl"), + ("Transform3D", "Transform3D"), + ("ViewportScreenSpaceAA", "ViewportScreenSpaceAa"), + ("ViewportSDFScale", "ViewportSdfScale"), + ("WebRTCPeerConnectionGDNative", "WebRtcPeerConnectionGDNative"), + ("X509Certificate", "X509Certificate"), + ("XRServer", "XrServer"), + ("YSort", "YSort"), ]; - tests.iter().for_each(|(class_name, expected)| { - let actual = to_module_name(class_name); - assert_eq!(*expected, actual, "Input: {class_name}"); - }); + + for (class_name, expected) in mappings { + let actual = to_pascal_case(class_name); + assert_eq!(actual, expected, "PascalCase: ident `{class_name}`"); + } } #[test] -fn test_name_smoother() { +fn test_snake_conversion() { // More in line with Rust identifiers, and eases recognition of other automation (like enumerator mapping). #[rustfmt::skip] - let _mappings = [ - ("RID", "Rid"), - ("AESContext", "AesContext"), - ("AudioEffectEQ21", "AudioEffectEq21"), - ("AudioStreamWAV", "AudioStreamWav"), - ("CPUParticles3D", "CpuParticles3D"), - ("ClassDB", "ClassDb"), // should multi-uppercase at the end be retained? - ("CharFXTransform", "CharFxTransform"), - ("ViewportSDFScale", "ViewportSdfScale"), - ("ViewportMSAA", "ViewportMsaa"), - ("ViewportScreenSpaceAA", "ViewportScreenSpaceAa"), - - // unchanged - ("AStar3D", "AStar3D"), + let mappings = [ + ("AABB", "aabb"), + ("AESContext", "aes_context"), + ("AStar3D", "a_star_3d"), + ("AudioEffectEQ21", "audio_effect_eq21"), + ("AudioStreamWAV", "audio_stream_wav"), + ("CharFXTransform", "char_fx_transform"), + ("CPUParticles3D", "cpu_particles_3d"), + ("EditorSceneImporterGLTF", "editor_scene_importer_gltf"), + ("GIProbe", "gi_probe"), + ("HMACContext", "hmac_context"), + ("HSeparator", "h_separator"), + ("IP", "ip"), + ("JNISingleton", "jni_singleton"), + ("JSON", "json"), + ("JSONParseResult", "json_parse_result"), + ("JSONRPC", "json_rpc"), + ("NetworkedMultiplayerENet", "networked_multiplayer_e_net"), + ("ObjectID", "object_id"), + ("PackedFloat32Array", "packed_float32_array"), + ("PCKPacker", "pck_packer"), + ("PHashTranslation", "p_hash_translation"), + ("PhysicsServer2DExtensionRayResult", "physics_server_2d_extension_ray_result"), + ("Rect2", "rect2"), + ("Rect2i", "rect2i"), + ("RID", "rid"), + ("StreamPeerSSL", "stream_peer_ssl"), + ("Transform3D", "transform_3d"), + ("ViewportScreenSpaceAA", "viewport_screen_space_aa"), + ("ViewportSDFScale", "viewport_sdf_scale"), + ("WebRTCPeerConnectionGDNative", "web_rtc_peer_connection_gdnative"), + ("X509Certificate", "x509_certificate"), + ("XRServer", "xr_server"), + ("YSort", "y_sort"), ]; + + for (class_name, expected) in mappings { + let actual = to_snake_case(class_name); + assert_eq!(actual, expected, "snake_case: ident `{class_name}`"); + } } #[test] diff --git a/godot-codegen/src/util.rs b/godot-codegen/src/util.rs index 61c009ba0..5e339a994 100644 --- a/godot-codegen/src/util.rs +++ b/godot-codegen/src/util.rs @@ -7,7 +7,7 @@ use crate::api_parser::Enum; use crate::{Context, RustTy}; use proc_macro2::{Ident, Literal, TokenStream}; -use quote::{format_ident, quote}; +use quote::{format_ident, quote, ToTokens}; pub fn make_enum_definition(enum_: &Enum) -> TokenStream { // TODO enums which have unique ords could be represented as Rust enums @@ -97,6 +97,14 @@ pub fn make_enum_definition(enum_: &Enum) -> TokenStream { } } +pub fn make_module_name(godot_class_name: &str) -> Ident { + ident(&to_snake_case(godot_class_name)) +} + +pub fn make_class_name(godot_class_name: &str) -> Ident { + ident(&to_pascal_case(godot_class_name)) +} + fn make_enum_name(enum_name: &str) -> Ident { // TODO clean up enum name @@ -110,85 +118,36 @@ fn make_enumerator_name(enumerator_name: &str, _enum_name: &str) -> Ident { ident(enumerator_name) } -pub fn to_module_name(class_name: &str) -> String { - // Remove underscores and make peekable - let mut class_chars = class_name.bytes().filter(|&ch| ch != b'_').peekable(); - - // 2-lookbehind - let mut previous: [Option; 2] = [None, None]; // previous-previous, previous - - // None is not upper or number - #[inline(always)] - fn is_upper_or_num(ch: T) -> bool - where - T: Into>, - { - let ch = ch.into(); - match ch { - Some(ch) => ch.is_ascii_digit() || ch.is_ascii_uppercase(), - None => false, - } - } +pub fn to_snake_case(class_name: &str) -> String { + use heck::ToSnakeCase; - // None is lowercase - #[inline(always)] - fn is_lower_or<'a, T>(ch: T, default: bool) -> bool - where - T: Into>, - { - let ch = ch.into(); - match ch { - Some(ch) => ch.is_ascii_lowercase(), - None => default, - } + // Special cases + match class_name { + "JSONRPC" => return "json_rpc".to_string(), + _ => {} } - let mut result = Vec::with_capacity(class_name.len()); - while let Some(current) = class_chars.next() { - let next = class_chars.peek(); - - let [two_prev, one_prev] = previous; - - // See tests for cases covered - let caps_to_lowercase = is_upper_or_num(one_prev) - && is_upper_or_num(current) - && is_lower_or(next, false) - && !is_lower_or(&two_prev, true); - - // Add an underscore for Lowercase followed by Uppercase|Num - // Node2D => node_2d (numbers are considered uppercase) - let lower_to_uppercase = is_lower_or(&one_prev, false) && is_upper_or_num(current); - - if caps_to_lowercase || lower_to_uppercase { - result.push(b'_'); - } - result.push(current.to_ascii_lowercase()); - - // Update the look-behind - previous = [previous[1], Some(current)]; - } - - let mut result = String::from_utf8(result).unwrap(); + class_name + .replace("2D", "_2d") + .replace("3D", "_3d") + .replace("GDNative", "Gdnative") + .replace("GDExtension", "Gdextension") + .to_snake_case() +} - // There are a few cases where the conversions do not work: - // * VisualShaderNodeVec3Uniform => visual_shader_node_vec_3_uniform - // * VisualShaderNodeVec3Constant => visual_shader_node_vec_3_constant - if let Some(range) = result.find("_vec_3").map(|i| i..i + 6) { - result.replace_range(range, "_vec3_") - } - if let Some(range) = result.find("gd_extension").map(|i| i..i + 12) { - result.replace_range(range, "gdextension") - } - if let Some(range) = result.find("gd_script").map(|i| i..i + 9) { - result.replace_range(range, "gdscript") - } +pub fn to_pascal_case(class_name: &str) -> String { + use heck::ToPascalCase; - // Exclude from glob imports "gdextension" - if result == "gdextension" { - return "gdextension_".to_string(); + // Special cases + match class_name { + "JSONRPC" => return "JsonRpc".to_string(), + _ => {} } - result + class_name + .to_pascal_case() + .replace("GdExtension", "GDExtension") + .replace("GdNative", "GDNative") } pub fn ident(s: &str) -> Ident { @@ -218,10 +177,6 @@ pub fn safe_ident(s: &str) -> Ident { } } -pub fn strlit(s: &str) -> Literal { - Literal::string(s) -} - fn to_hardcoded_rust_type(ty: &str) -> Option<&str> { let result = match ty { "int" => "i64", @@ -263,7 +218,7 @@ fn to_rust_type_uncached(ty: &str, ctx: &mut Context) -> RustTy { if let Some(qualified_enum) = qualified_enum { return if let Some((class, enum_)) = qualified_enum.split_once('.') { // Class-local enum - let module = ident(&to_module_name(class)); + let module = make_module_name(class); let enum_ty = make_enum_name(enum_); RustTy::EngineEnum { @@ -282,12 +237,11 @@ fn to_rust_type_uncached(ty: &str, ctx: &mut Context) -> RustTy { } else if let Some(packed_arr_ty) = ty.strip_prefix("Packed") { // Don't trigger on PackedScene ;P if packed_arr_ty.ends_with("Array") { - return RustTy::BuiltinIdent(ident(ty)); - //return RustTy::BuiltinIdent(ident(packed_arr_ty)); + return RustTy::BuiltinIdent(make_class_name(&ty)); } } else if let Some(elem_ty) = ty.strip_prefix("typedarray::") { if let Some(_packed_arr_ty) = elem_ty.strip_prefix("Packed") { - return RustTy::BuiltinIdent(ident(elem_ty)); + return RustTy::BuiltinIdent(make_class_name(&elem_ty)); } let rust_elem_ty = to_rust_type(elem_ty, ctx); diff --git a/godot-core/src/log.rs b/godot-core/src/log.rs index d6056d1e7..619d68aad 100644 --- a/godot-core/src/log.rs +++ b/godot-core/src/log.rs @@ -89,4 +89,7 @@ pub fn print(varargs: &[Variant]) { call_fn(return_ptr, args_ptr, args.len() as i32); }); } + + // TODO use generated method, but figure out how print() with zero args can be called + // crate::engine::utilities::print(head, rest); } diff --git a/itest/rust/src/singleton_test.rs b/itest/rust/src/singleton_test.rs index 90ef8c4f1..1a6910686 100644 --- a/itest/rust/src/singleton_test.rs +++ b/itest/rust/src/singleton_test.rs @@ -6,7 +6,7 @@ use crate::itest; use godot::builtin::GodotString; -use godot::engine::{Input, OS}; +use godot::engine::{Input, Os}; use godot::obj::Gd; pub fn run() -> bool { @@ -30,17 +30,17 @@ fn singleton_is_unique() { #[itest] fn singleton_from_instance_id() { - let a: Gd = OS::singleton(); + let a: Gd = Os::singleton(); let id = a.instance_id(); - let b: Gd = Gd::from_instance_id(id); + let b: Gd = Gd::from_instance_id(id); assert_eq!(a.get_executable_path(), b.get_executable_path()); } #[itest] fn singleton_is_operational() { - let os: Gd = OS::singleton(); + let os: Gd = Os::singleton(); let key = GodotString::from("MY_TEST_ENV"); let value = GodotString::from("SOME_VALUE"); From 9621ba3f3117733d8b84899fa7aa743bf2fb5b00 Mon Sep 17 00:00:00 2001 From: Jan Haller Date: Thu, 26 Jan 2023 20:45:53 +0100 Subject: [PATCH 27/54] Clearly separate class names from Godot and Rust via type system (TyName, ModName) --- godot-codegen/src/class_generator.rs | 123 ++++++++++++++------------- godot-codegen/src/context.rs | 30 +++---- godot-codegen/src/lib.rs | 64 +++++++++++++- godot-codegen/src/special_cases.rs | 30 +++---- godot-codegen/src/util.rs | 19 ++--- 5 files changed, 159 insertions(+), 107 deletions(-) diff --git a/godot-codegen/src/class_generator.rs b/godot-codegen/src/class_generator.rs index 80894a11b..4b9fb89cd 100644 --- a/godot-codegen/src/class_generator.rs +++ b/godot-codegen/src/class_generator.rs @@ -12,10 +12,10 @@ use std::path::{Path, PathBuf}; use crate::api_parser::*; use crate::central_generator::{collect_builtin_types, BuiltinTypeInfo}; -use crate::util::{ident, make_class_name, make_module_name, safe_ident, to_pascal_case, to_rust_type}; +use crate::util::{ident, safe_ident, to_pascal_case, to_rust_type}; use crate::{ special_cases, util, Context, GeneratedBuiltin, GeneratedBuiltinModule, GeneratedClass, - GeneratedClassModule, RustTy, + GeneratedClassModule, ModName, RustTy, TyName, }; pub(crate) fn generate_class_files( @@ -30,27 +30,28 @@ pub(crate) fn generate_class_files( let mut modules = vec![]; for class in api.classes.iter() { + let class_name = TyName::from_godot(&class.name); + let module_name = ModName::from_godot(&class.name); + #[cfg(not(feature = "codegen-full"))] - if !crate::SELECTED_CLASSES.contains(&class.name.as_str()) { + if !crate::SELECTED_CLASSES.contains(&class_name.godot_ty.as_str()) { continue; } - if special_cases::is_class_deleted(class.name.as_str()) { + if special_cases::is_class_deleted(&class_name) { continue; } - let rust_class = make_class_name(&class.name); - let rust_module = make_module_name(&class.name); - let generated_class = make_class(class, &rust_class, ctx); + let generated_class = make_class(class, &class_name, ctx); let file_contents = generated_class.tokens.to_string(); - let out_path = gen_path.join(format!("{rust_module}.rs")); + let out_path = gen_path.join(format!("{}.rs", module_name.rust_mod)); std::fs::write(&out_path, file_contents).expect("failed to write class file"); out_files.push(out_path); modules.push(GeneratedClassModule { - class_ident: rust_class, - module_ident: rust_module, + class_name, + module_name, inherits_macro_ident: generated_class.inherits_macro_ident, is_pub: generated_class.has_pub_module, }); @@ -76,25 +77,29 @@ pub(crate) fn generate_builtin_class_files( let mut modules = vec![]; for class in api.builtin_classes.iter() { - if special_cases::is_builtin_type_deleted(&class.name) { + let module_name = ModName::from_godot(&class.name); + let class_name = TyName::from_godot(&class.name); + let inner_class_name = TyName::from_godot(&format!("Inner{}", class.name)); + + if special_cases::is_builtin_type_deleted(&class_name) { continue; } let type_info = builtin_types_map .get(&class.name) .unwrap_or_else(|| panic!("builtin type not found: {}", class.name)); - let inner_class = format_ident!("Inner{}", class.name); - let generated_class = make_builtin_class(class, &inner_class, type_info, ctx); + + let generated_class = + make_builtin_class(class, &class_name, &inner_class_name, type_info, ctx); let file_contents = generated_class.tokens.to_string(); - let module_ident = make_module_name(&class.name); - let out_path = gen_path.join(format!("{module_ident}.rs")); + let out_path = gen_path.join(format!("{}.rs", module_name.rust_mod)); std::fs::write(&out_path, file_contents).expect("failed to write class file"); out_files.push(out_path); modules.push(GeneratedBuiltinModule { - class_ident: inner_class, - module_ident, + class_name: inner_class_name, + module_name, }); } @@ -151,9 +156,9 @@ fn make_constructor(class: &Class, ctx: &Context) -> TokenStream { } } -fn make_class(class: &Class, rust_class:&Ident, ctx: &mut Context) -> GeneratedClass { +fn make_class(class: &Class, class_name: &TyName, ctx: &mut Context) -> GeneratedClass { // Strings - let godot_class_name = &class.name; + let godot_class_str = &class_name.godot_ty; // Idents and tokens let base = match class.inherits.as_ref() { @@ -165,14 +170,12 @@ fn make_class(class: &Class, rust_class:&Ident, ctx: &mut Context) -> GeneratedC }; let constructor = make_constructor(class, ctx); - let methods = make_methods(&class.methods, &godot_class_name, ctx); - let enums = make_enums(&class.enums, &godot_class_name, ctx); - let inherits_macro = format_ident!("inherits_transitive_{}", rust_class); - let all_bases = ctx - .inheritance_tree() - .collect_all_bases(rust_class); - - let memory = if rust_class == "Object" { + let methods = make_methods(&class.methods, class_name, ctx); + let enums = make_enums(&class.enums, class_name, ctx); + let inherits_macro = format_ident!("inherits_transitive_{}", class_name.rust_ty); + let all_bases = ctx.inheritance_tree().collect_all_bases(class_name); + + let memory = if class_name.rust_ty == "Object" { ident("DynamicRefCount") } else if class.is_refcounted { ident("StaticRefCount") @@ -193,21 +196,21 @@ fn make_class(class: &Class, rust_class:&Ident, ctx: &mut Context) -> GeneratedC #[derive(Debug)] #[repr(transparent)] - pub struct #rust_class { + pub struct #class_name { object_ptr: sys::GDExtensionObjectPtr, } - impl #rust_class { + impl #class_name { #constructor #methods } - impl crate::obj::GodotClass for #rust_class { + impl crate::obj::GodotClass for #class_name { type Base = #base; type Declarer = crate::obj::dom::EngineDomain; type Mem = crate::obj::mem::#memory; - const CLASS_NAME: &'static str = #godot_class_name; + const CLASS_NAME: &'static str = #godot_class_str; } - impl crate::obj::EngineClass for #rust_class { + impl crate::obj::EngineClass for #class_name { fn as_object_ptr(&self) -> sys::GDExtensionObjectPtr { self.object_ptr } @@ -216,9 +219,9 @@ fn make_class(class: &Class, rust_class:&Ident, ctx: &mut Context) -> GeneratedC } } #( - impl crate::obj::Inherits for #rust_class {} + impl crate::obj::Inherits for #class_name {} )* - impl std::ops::Deref for #rust_class { + impl std::ops::Deref for #class_name { type Target = #base; fn deref(&self) -> &Self::Target { @@ -226,7 +229,7 @@ fn make_class(class: &Class, rust_class:&Ident, ctx: &mut Context) -> GeneratedC unsafe { std::mem::transmute::<&Self, &Self::Target>(self) } } } - impl std::ops::DerefMut for #rust_class { + impl std::ops::DerefMut for #class_name { fn deref_mut(&mut self) -> &mut Self::Target { // SAFETY: see above unsafe { std::mem::transmute::<&mut Self, &mut Self::Target>(self) } @@ -237,7 +240,7 @@ fn make_class(class: &Class, rust_class:&Ident, ctx: &mut Context) -> GeneratedC #[allow(non_snake_case)] macro_rules! #inherits_macro { ($Class:ident) => { - impl ::godot::obj::Inherits<::godot::engine::#rust_class> for $Class {} + impl ::godot::obj::Inherits<::godot::engine::#class_name> for $Class {} #( impl ::godot::obj::Inherits<::godot::engine::#all_bases> for $Class {} )* @@ -258,7 +261,8 @@ fn make_class(class: &Class, rust_class:&Ident, ctx: &mut Context) -> GeneratedC fn make_builtin_class( class: &BuiltinClass, - inner_class: &Ident, + class_name: &TyName, + inner_class_name: &TyName, type_info: &BuiltinTypeInfo, ctx: &mut Context, ) -> GeneratedBuiltin { @@ -267,6 +271,7 @@ fn make_builtin_class( } else { panic!("Rust type `{}` categorized wrong", class.name) }; + let inner_class = &inner_class_name.rust_ty; let class_enums = class.enums.as_ref().map(|class_enums| { class_enums @@ -275,8 +280,8 @@ fn make_builtin_class( .collect::>() }); - let methods = make_builtin_methods(&class.methods, &class.name, type_info, ctx); - let enums = make_enums(&class_enums, &class.name, ctx); + let methods = make_builtin_methods(&class.methods, class_name, type_info, ctx); + let enums = make_enums(&class_enums, class_name, ctx); // mod re_export needed, because class should not appear inside the file module, and we can't re-export private struct as pub let tokens = quote! { @@ -312,8 +317,8 @@ fn make_builtin_class( fn make_module_file(classes_and_modules: Vec) -> TokenStream { let decls = classes_and_modules.iter().map(|m| { let GeneratedClassModule { - module_ident, - class_ident, + module_name, + class_name, is_pub, .. } = m; @@ -321,8 +326,8 @@ fn make_module_file(classes_and_modules: Vec) -> TokenStre let vis = is_pub.then_some(quote! { pub }); quote! { - #vis mod #module_ident; - pub use #module_ident::re_export::#class_ident; + #vis mod #module_name; + pub use #module_name::re_export::#class_name; } }); @@ -353,14 +358,14 @@ fn make_module_file(classes_and_modules: Vec) -> TokenStre fn make_builtin_module_file(classes_and_modules: Vec) -> TokenStream { let decls = classes_and_modules.iter().map(|m| { let GeneratedBuiltinModule { - module_ident, - class_ident, + module_name, + class_name, .. } = m; quote! { - mod #module_ident; - pub use #module_ident::#class_ident; + mod #module_name; + pub use #module_name::#class_name; } }); @@ -371,7 +376,7 @@ fn make_builtin_module_file(classes_and_modules: Vec) -> fn make_methods( methods: &Option>, - godot_class_name: &str, + class_name: &TyName, ctx: &mut Context, ) -> TokenStream { let methods = match methods { @@ -381,7 +386,7 @@ fn make_methods( let definitions = methods .iter() - .map(|method| make_method_definition(method, godot_class_name, ctx)); + .map(|method| make_method_definition(method, class_name, ctx)); quote! { #( #definitions )* @@ -390,7 +395,7 @@ fn make_methods( fn make_builtin_methods( methods: &Option>, - class_name: &str, + class_name: &TyName, type_info: &BuiltinTypeInfo, ctx: &mut Context, ) -> TokenStream { @@ -408,7 +413,7 @@ fn make_builtin_methods( } } -fn make_enums(enums: &Option>, _class_name: &str, _ctx: &Context) -> TokenStream { +fn make_enums(enums: &Option>, _class_name: &TyName, _ctx: &Context) -> TokenStream { let enums = match enums { Some(e) => e, None => return TokenStream::new(), @@ -496,11 +501,10 @@ fn is_function_excluded(function: &UtilityFunction, ctx: &mut Context) -> bool { fn make_method_definition( method: &ClassMethod, - godot_class_name: &str, + class_name: &TyName, ctx: &mut Context, ) -> TokenStream { - if is_method_excluded(method, ctx) || special_cases::is_deleted(godot_class_name, &method.name) - { + if is_method_excluded(method, ctx) || special_cases::is_deleted(class_name, &method.name) { return TokenStream::new(); } /*if method.map_args(|args| args.is_empty()) { @@ -514,7 +518,7 @@ fn make_method_definition( } }*/ - let method_name_str = special_cases::maybe_renamed(godot_class_name, &method.name); + let method_name_str = special_cases::maybe_renamed(class_name, &method.name); let receiver = if method.is_const { quote! { &self, } } else { @@ -530,8 +534,9 @@ fn make_method_definition( ident("object_method_bind_ptrcall") }; + let class_name_str = &class_name.godot_ty; let init_code = quote! { - let __class_name = StringName::from(#godot_class_name); + let __class_name = StringName::from(#class_name_str); let __method_name = StringName::from(#method_name_str); let __method_bind = sys::interface_fn!(classdb_get_method_bind)( __class_name.string_sys(), @@ -549,7 +554,7 @@ fn make_method_definition( make_function_definition( method_name_str, - special_cases::is_private(godot_class_name, &method.name), + special_cases::is_private(class_name, &method.name), receiver, &method.arguments, method.return_value.as_ref(), @@ -563,7 +568,7 @@ fn make_method_definition( fn make_builtin_method_definition( method: &BuiltinClassMethod, - class_name_str: &str, + class_name: &TyName, type_info: &BuiltinTypeInfo, ctx: &mut Context, ) -> TokenStream { @@ -602,7 +607,7 @@ fn make_builtin_method_definition( make_function_definition( method_name_str, - special_cases::is_private(class_name_str, &method.name), + special_cases::is_private(class_name, &method.name), receiver, &method.arguments, return_value.as_ref(), diff --git a/godot-codegen/src/context.rs b/godot-codegen/src/context.rs index 1f56f88bf..990fa53cf 100644 --- a/godot-codegen/src/context.rs +++ b/godot-codegen/src/context.rs @@ -4,14 +4,12 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -use crate::{ExtensionApi, RustTy}; +use crate::{ExtensionApi, RustTy, TyName}; use std::collections::{HashMap, HashSet}; -use proc_macro2::Ident; -use crate::util::make_class_name; #[derive(Default)] pub(crate) struct Context<'a> { - engine_classes: HashSet<&'a str>, + engine_classes: HashSet, builtin_types: HashSet<&'a str>, singletons: HashSet<&'a str>, inheritance_tree: InheritanceTree, @@ -34,20 +32,20 @@ impl<'a> Context<'a> { } for class in api.classes.iter() { - let class_name = class.name.as_str(); + let class_name = TyName::from_godot(&class.name); #[cfg(not(feature = "codegen-full"))] - if !crate::SELECTED_CLASSES.contains(&class_name) { + if !crate::SELECTED_CLASSES.contains(&class_name.godot_ty.as_str()) { continue; } - println!("-- add engine class {class_name}"); - ctx.engine_classes.insert(class_name); + println!("-- add engine class {}", class_name.description()); + ctx.engine_classes.insert(class_name.clone()); if let Some(base) = class.inherits.as_ref() { - println!(" -- inherits {base}"); - ctx.inheritance_tree - .insert(make_class_name(class_name), make_class_name(base)); + let base_name = TyName::from_godot(base); + println!(" -- inherits {}", base_name.description()); + ctx.inheritance_tree.insert(class_name, base_name); } } ctx @@ -82,17 +80,17 @@ impl<'a> Context<'a> { /// Maintains class hierarchy. Uses Rust class names, not Godot ones. #[derive(Default)] pub(crate) struct InheritanceTree { - derived_to_base: HashMap, + derived_to_base: HashMap, } impl InheritanceTree { - pub fn insert(&mut self, rust_derived: Ident, rust_base: Ident) { - let existing = self.derived_to_base.insert(rust_derived, rust_base); + pub fn insert(&mut self, derived_name: TyName, base_name: TyName) { + let existing = self.derived_to_base.insert(derived_name, base_name); assert!(existing.is_none(), "Duplicate inheritance insert"); } - pub fn collect_all_bases(&self, rust_derived: &Ident) -> Vec { - let mut maybe_base = rust_derived; + pub fn collect_all_bases(&self, derived_name: &TyName) -> Vec { + let mut maybe_base = derived_name; let mut result = vec![]; while let Some(base) = self.derived_to_base.get(maybe_base) { diff --git a/godot-codegen/src/lib.rs b/godot-codegen/src/lib.rs index 61356ce2f..51ff535b3 100644 --- a/godot-codegen/src/lib.rs +++ b/godot-codegen/src/lib.rs @@ -29,6 +29,7 @@ use util::ident; use utilities_generator::generate_utilities_file; use watch::StopWatch; +use crate::util::{to_pascal_case, to_snake_case}; use proc_macro2::{Ident, TokenStream}; use quote::{quote, ToTokens}; use std::path::{Path, PathBuf}; @@ -176,6 +177,61 @@ impl ToTokens for RustTy { } } +// ---------------------------------------------------------------------------------------------------------------------------------------------- + +/// Contains multiple naming conventions for types (classes, builtin classes, enums). +#[derive(Clone, Eq, PartialEq, Hash)] +pub(crate) struct TyName { + godot_ty: String, + rust_ty: Ident, +} + +impl TyName { + fn from_godot(godot_ty: &str) -> Self { + Self { + godot_ty: godot_ty.to_owned(), + rust_ty: ident(&to_pascal_case(godot_ty)), + } + } + + fn description(&self) -> String { + if self.rust_ty == self.godot_ty { + self.godot_ty.clone() + } else { + format!("{} [renamed {}]", self.godot_ty, self.rust_ty) + } + } +} + +impl ToTokens for TyName { + fn to_tokens(&self, tokens: &mut TokenStream) { + self.rust_ty.to_tokens(tokens) + } +} + +// ---------------------------------------------------------------------------------------------------------------------------------------------- + +/// Contains naming conventions for modules. +pub(crate) struct ModName { + // godot_mod: String, + rust_mod: Ident, +} + +impl ModName { + fn from_godot(godot_ty: &str) -> Self { + Self { + // godot_mod: godot_ty.to_owned(), + rust_mod: ident(&to_snake_case(godot_ty)), + } + } +} + +impl ToTokens for ModName { + fn to_tokens(&self, tokens: &mut TokenStream) { + self.rust_mod.to_tokens(tokens) + } +} + struct GeneratedClass { tokens: TokenStream, inherits_macro_ident: Ident, @@ -187,15 +243,15 @@ struct GeneratedBuiltin { } struct GeneratedClassModule { - class_ident: Ident, - module_ident: Ident, + class_name: TyName, + module_name: ModName, inherits_macro_ident: Ident, is_pub: bool, } struct GeneratedBuiltinModule { - class_ident: Ident, - module_ident: Ident, + class_name: TyName, + module_name: ModName, } // ---------------------------------------------------------------------------------------------------------------------------------------------- diff --git a/godot-codegen/src/special_cases.rs b/godot-codegen/src/special_cases.rs index 944ff3cfa..ac15906f0 100644 --- a/godot-codegen/src/special_cases.rs +++ b/godot-codegen/src/special_cases.rs @@ -13,11 +13,13 @@ // * The deleted/private methods and classes deemed "dangerous" may be provided later as unsafe functions -- our safety model // needs to first mature a bit. -// NOTE: the identifiers used here operate on the GODOT types (e.g. AABB, not Aabb) +// NOTE: the methods are generally implemented on Godot types (e.g. AABB, not Aabb) + +use crate::TyName; #[rustfmt::skip] -pub fn is_deleted(godot_class_name: &str, godot_method_name: &str) -> bool { - match (godot_class_name, godot_method_name) { +pub(crate) fn is_deleted(class_name: &TyName, godot_method_name: &str) -> bool { + match (class_name.godot_ty.as_str(), godot_method_name) { // Already covered by manual APIs //| ("Object", "to_string") | ("Object", "get_instance_id") @@ -33,8 +35,8 @@ pub fn is_deleted(godot_class_name: &str, godot_method_name: &str) -> bool { } #[rustfmt::skip] -pub fn is_class_deleted(godot_class_name: &str) -> bool { - match godot_class_name { +pub(crate) fn is_class_deleted(class_name: &TyName) -> bool { + match class_name.godot_ty.as_str() { // Thread APIs | "Thread" | "Mutex" @@ -45,8 +47,8 @@ pub fn is_class_deleted(godot_class_name: &str) -> bool { } #[rustfmt::skip] -pub fn is_private(godot_class_name: &str, godot_method_name: &str) -> bool { - match (godot_class_name, godot_method_name) { +pub(crate) fn is_private(class_name: &TyName, godot_method_name: &str) -> bool { + match (class_name.godot_ty.as_str(), godot_method_name) { // Already covered by manual APIs | ("Object", "to_string") | ("RefCounted", "init_ref") @@ -57,17 +59,13 @@ pub fn is_private(godot_class_name: &str, godot_method_name: &str) -> bool { } } -pub fn is_builtin_type_deleted(godot_class_name: &str) -> bool { - godot_class_name == "Nil" - || godot_class_name - .chars() - .next() - .unwrap() - .is_ascii_lowercase() +pub(crate) fn is_builtin_type_deleted(class_name: &TyName) -> bool { + let name = class_name.godot_ty.as_str(); + name == "Nil" || name.chars().next().unwrap().is_ascii_lowercase() } -pub fn maybe_renamed<'m>(godot_class_name: &str, godot_method_name: &'m str) -> &'m str { - match (godot_class_name, godot_method_name) { +pub(crate) fn maybe_renamed<'m>(class_name: &TyName, godot_method_name: &'m str) -> &'m str { + match (class_name.godot_ty.as_str(), godot_method_name) { // GDScript, GDScriptNativeClass, possibly more in the future (_, "new") => "instantiate", _ => godot_method_name, diff --git a/godot-codegen/src/util.rs b/godot-codegen/src/util.rs index 5e339a994..b68b9eae6 100644 --- a/godot-codegen/src/util.rs +++ b/godot-codegen/src/util.rs @@ -5,7 +5,7 @@ */ use crate::api_parser::Enum; -use crate::{Context, RustTy}; +use crate::{Context, ModName, RustTy, TyName}; use proc_macro2::{Ident, Literal, TokenStream}; use quote::{format_ident, quote, ToTokens}; @@ -97,14 +97,6 @@ pub fn make_enum_definition(enum_: &Enum) -> TokenStream { } } -pub fn make_module_name(godot_class_name: &str) -> Ident { - ident(&to_snake_case(godot_class_name)) -} - -pub fn make_class_name(godot_class_name: &str) -> Ident { - ident(&to_pascal_case(godot_class_name)) -} - fn make_enum_name(enum_name: &str) -> Ident { // TODO clean up enum name @@ -122,6 +114,7 @@ pub fn to_snake_case(class_name: &str) -> String { use heck::ToSnakeCase; // Special cases + #[allow(clippy::single_match)] match class_name { "JSONRPC" => return "json_rpc".to_string(), _ => {} @@ -139,6 +132,7 @@ pub fn to_pascal_case(class_name: &str) -> String { use heck::ToPascalCase; // Special cases + #[allow(clippy::single_match)] match class_name { "JSONRPC" => return "JsonRpc".to_string(), _ => {} @@ -194,6 +188,7 @@ fn to_hardcoded_rust_type(ty: &str) -> Option<&str> { /// Maps an _input_ type from the Godot JSON to the corresponding Rust type (wrapping some sort of a token stream). /// /// Uses an internal cache (via `ctx`), as several types are ubiquitous. +// TODO take TyName as input pub(crate) fn to_rust_type(ty: &str, ctx: &mut Context<'_>) -> RustTy { // Separate find + insert slightly slower, but much easier with lifetimes // The insert path will be hit less often and thus doesn't matter @@ -218,7 +213,7 @@ fn to_rust_type_uncached(ty: &str, ctx: &mut Context) -> RustTy { if let Some(qualified_enum) = qualified_enum { return if let Some((class, enum_)) = qualified_enum.split_once('.') { // Class-local enum - let module = make_module_name(class); + let module = ModName::from_godot(class); let enum_ty = make_enum_name(enum_); RustTy::EngineEnum { @@ -237,11 +232,11 @@ fn to_rust_type_uncached(ty: &str, ctx: &mut Context) -> RustTy { } else if let Some(packed_arr_ty) = ty.strip_prefix("Packed") { // Don't trigger on PackedScene ;P if packed_arr_ty.ends_with("Array") { - return RustTy::BuiltinIdent(make_class_name(&ty)); + return RustTy::BuiltinIdent(TyName::from_godot(ty).rust_ty); } } else if let Some(elem_ty) = ty.strip_prefix("typedarray::") { if let Some(_packed_arr_ty) = elem_ty.strip_prefix("Packed") { - return RustTy::BuiltinIdent(make_class_name(&elem_ty)); + return RustTy::BuiltinIdent(TyName::from_godot(elem_ty).rust_ty); } let rust_elem_ty = to_rust_type(elem_ty, ctx); From aeac34372ccee19c09073eb0be902d5cae5312f5 Mon Sep 17 00:00:00 2001 From: Jan Haller Date: Thu, 26 Jan 2023 21:15:06 +0100 Subject: [PATCH 28/54] Map also Godot builtins to Rust identifiers (AABB -> Aabb, RID -> Rid) --- godot-codegen/src/central_generator.rs | 8 ++++---- godot-codegen/src/class_generator.rs | 7 ++++--- godot-codegen/src/context.rs | 4 +++- godot-codegen/src/lib.rs | 10 +++++++--- godot-codegen/src/special_cases.rs | 8 +++++++- godot-codegen/src/util.rs | 25 +++++++++++++++++++------ godot-core/src/builtin/others.rs | 4 ++-- godot-ffi/src/gen_central_stub.rs | 4 ++-- 8 files changed, 48 insertions(+), 22 deletions(-) diff --git a/godot-codegen/src/central_generator.rs b/godot-codegen/src/central_generator.rs index 8791cdc93..6fd7b7649 100644 --- a/godot-codegen/src/central_generator.rs +++ b/godot-codegen/src/central_generator.rs @@ -10,7 +10,7 @@ use std::collections::HashMap; use std::path::{Path, PathBuf}; use crate::api_parser::*; -use crate::util::{to_rust_type, to_snake_case}; +use crate::util::{to_pascal_case, to_rust_type, to_snake_case}; use crate::{ident, util, Context}; struct CentralItems { @@ -485,13 +485,13 @@ fn make_enumerator( } fn make_opaque_type(name: &str, size: usize) -> TokenStream { - // Capitalize: "int" -> "Int" + let name = to_pascal_case(name); let (first, rest) = name.split_at(1); + + // Capitalize: "int" -> "Int" let ident = format_ident!("Opaque{}{}", first.to_ascii_uppercase(), rest); - //let upper = format_ident!("SIZE_{}", name.to_uppercase()); quote! { pub type #ident = crate::opaque::Opaque<#size>; - //pub const #upper: usize = #size; } } diff --git a/godot-codegen/src/class_generator.rs b/godot-codegen/src/class_generator.rs index 4b9fb89cd..3b414f7a8 100644 --- a/godot-codegen/src/class_generator.rs +++ b/godot-codegen/src/class_generator.rs @@ -440,7 +440,7 @@ fn is_type_excluded(ty: &str, ctx: &mut Context) -> bool { None => false, Some(class) => is_class_excluded(class.as_str()), }, - RustTy::EngineClass(_) => is_class_excluded(ty), + RustTy::EngineClass { .. } => is_class_excluded(ty), } } @@ -765,7 +765,7 @@ fn make_params( arg_exprs.push(quote! { <#param_ty as ToVariant>::to_variant(&#param_name) }); - } else if let RustTy::EngineClass(path) = param_ty { + } else if let RustTy::EngineClass { tokens: path, .. } = param_ty { arg_exprs.push(quote! { <#path as AsArg>::as_arg_ptr(&#param_name) }); @@ -827,7 +827,8 @@ fn make_return( assert_eq!(__err.error, sys::GDEXTENSION_CALL_OK); } } - (None, Some(RustTy::EngineClass(return_ty))) => { + (None, Some(RustTy::EngineClass { tokens, .. })) => { + let return_ty = tokens; quote! { <#return_ty>::from_sys_init_opt(|return_ptr| { #ptrcall_invocation diff --git a/godot-codegen/src/context.rs b/godot-codegen/src/context.rs index 990fa53cf..d3525870d 100644 --- a/godot-codegen/src/context.rs +++ b/godot-codegen/src/context.rs @@ -18,7 +18,6 @@ pub(crate) struct Context<'a> { impl<'a> Context<'a> { pub fn build_from_api(api: &'a ExtensionApi) -> Self { - // TODO possibly add a data structure containing both Godot JSON ident and Rust mapped one let mut ctx = Context::default(); for class in api.singletons.iter() { @@ -55,6 +54,9 @@ impl<'a> Context<'a> { // self.engine_classes.contains(class_name) // } + /// Checks if this is a builtin type (not `Object`). + /// + /// Note that builtins != variant types. pub fn is_builtin(&self, ty_name: &str) -> bool { self.builtin_types.contains(ty_name) } diff --git a/godot-codegen/src/lib.rs b/godot-codegen/src/lib.rs index 51ff535b3..38b643ef2 100644 --- a/godot-codegen/src/lib.rs +++ b/godot-codegen/src/lib.rs @@ -152,13 +152,17 @@ enum RustTy { }, /// `Gd` - EngineClass(TokenStream), + EngineClass { + tokens: TokenStream, + #[allow(dead_code)] // currently not read + class: String, + }, } impl RustTy { pub fn return_decl(&self) -> TokenStream { match self { - Self::EngineClass(tokens) => quote! { -> Option<#tokens> }, + Self::EngineClass { tokens, .. } => quote! { -> Option<#tokens> }, other => quote! { -> #other }, } } @@ -171,7 +175,7 @@ impl ToTokens for RustTy { RustTy::BuiltinArray(path) => path.to_tokens(tokens), RustTy::EngineArray { tokens: path, .. } => path.to_tokens(tokens), RustTy::EngineEnum { tokens: path, .. } => path.to_tokens(tokens), - RustTy::EngineClass(path) => path.to_tokens(tokens), + RustTy::EngineClass { tokens: path, .. } => path.to_tokens(tokens), //RustTy::Other(path) => path.to_tokens(tokens), } } diff --git a/godot-codegen/src/special_cases.rs b/godot-codegen/src/special_cases.rs index ac15906f0..c275ccc72 100644 --- a/godot-codegen/src/special_cases.rs +++ b/godot-codegen/src/special_cases.rs @@ -59,9 +59,15 @@ pub(crate) fn is_private(class_name: &TyName, godot_method_name: &str) -> bool { } } +/// True if builtin type is excluded (`NIL` or scalars) pub(crate) fn is_builtin_type_deleted(class_name: &TyName) -> bool { let name = class_name.godot_ty.as_str(); - name == "Nil" || name.chars().next().unwrap().is_ascii_lowercase() + name == "Nil" || is_builtin_scalar(name) +} + +/// True if `int`, `float`, `bool`, ... +pub(crate) fn is_builtin_scalar(name: &str) -> bool { + name.chars().next().unwrap().is_ascii_lowercase() } pub(crate) fn maybe_renamed<'m>(class_name: &TyName, godot_method_name: &'m str) -> &'m str { diff --git a/godot-codegen/src/util.rs b/godot-codegen/src/util.rs index b68b9eae6..9cb15e224 100644 --- a/godot-codegen/src/util.rs +++ b/godot-codegen/src/util.rs @@ -5,9 +5,10 @@ */ use crate::api_parser::Enum; +use crate::special_cases::is_builtin_scalar; use crate::{Context, ModName, RustTy, TyName}; use proc_macro2::{Ident, Literal, TokenStream}; -use quote::{format_ident, quote, ToTokens}; +use quote::{format_ident, quote}; pub fn make_enum_definition(enum_: &Enum) -> TokenStream { // TODO enums which have unique ords could be represented as Rust enums @@ -202,6 +203,15 @@ pub(crate) fn to_rust_type(ty: &str, ctx: &mut Context<'_>) -> RustTy { } fn to_rust_type_uncached(ty: &str, ctx: &mut Context) -> RustTy { + /// Transforms a Godot class/builtin/enum IDENT (without `::` or other syntax) to a Rust one + fn rustify_ty(ty: &str) -> Ident { + if is_builtin_scalar(ty) { + ident(ty) + } else { + TyName::from_godot(ty).rust_ty + } + } + if let Some(hardcoded) = to_hardcoded_rust_type(ty) { return RustTy::BuiltinIdent(ident(hardcoded)); } @@ -232,11 +242,11 @@ fn to_rust_type_uncached(ty: &str, ctx: &mut Context) -> RustTy { } else if let Some(packed_arr_ty) = ty.strip_prefix("Packed") { // Don't trigger on PackedScene ;P if packed_arr_ty.ends_with("Array") { - return RustTy::BuiltinIdent(TyName::from_godot(ty).rust_ty); + return RustTy::BuiltinIdent(rustify_ty(ty)); } } else if let Some(elem_ty) = ty.strip_prefix("typedarray::") { if let Some(_packed_arr_ty) = elem_ty.strip_prefix("Packed") { - return RustTy::BuiltinIdent(TyName::from_godot(elem_ty).rust_ty); + return RustTy::BuiltinIdent(rustify_ty(elem_ty)); } let rust_elem_ty = to_rust_type(elem_ty, ctx); @@ -253,9 +263,12 @@ fn to_rust_type_uncached(ty: &str, ctx: &mut Context) -> RustTy { // Note: do not check if it's a known engine class, because that will not work in minimal mode (since not all classes are stored) if ctx.is_builtin(ty) { // Unchanged - RustTy::BuiltinIdent(ident(ty)) + RustTy::BuiltinIdent(rustify_ty(ty)) } else { - let ty = ident(ty); - RustTy::EngineClass(quote! { Gd<#ty> }) + let ty = rustify_ty(ty); + RustTy::EngineClass { + tokens: quote! { Gd<#ty> }, + class: ty.to_string(), + } } } diff --git a/godot-core/src/builtin/others.rs b/godot-core/src/builtin/others.rs index d5ef33a8a..acfd77387 100644 --- a/godot-core/src/builtin/others.rs +++ b/godot-core/src/builtin/others.rs @@ -17,12 +17,12 @@ impl_builtin_stub!(Rect2, OpaqueRect2); impl_builtin_stub!(Rect2i, OpaqueRect2i); impl_builtin_stub!(Plane, OpaquePlane); impl_builtin_stub!(Quaternion, OpaqueQuaternion); -impl_builtin_stub!(AABB, OpaqueAABB); +impl_builtin_stub!(Aabb, OpaqueAabb); impl_builtin_stub!(Basis, OpaqueBasis); impl_builtin_stub!(Transform2D, OpaqueTransform2D); impl_builtin_stub!(Transform3D, OpaqueTransform3D); impl_builtin_stub!(Projection, OpaqueProjection); -impl_builtin_stub!(RID, OpaqueRID); +impl_builtin_stub!(Rid, OpaqueRid); impl_builtin_stub!(Callable, OpaqueCallable); impl_builtin_stub!(Signal, OpaqueSignal); impl_builtin_stub!(Dictionary, OpaqueDictionary); diff --git a/godot-ffi/src/gen_central_stub.rs b/godot-ffi/src/gen_central_stub.rs index a083cb2e8..5d0db08c9 100644 --- a/godot-ffi/src/gen_central_stub.rs +++ b/godot-ffi/src/gen_central_stub.rs @@ -26,14 +26,14 @@ pub mod types { pub type OpaqueVector4i = crate::opaque::Opaque<16usize>; pub type OpaquePlane = crate::opaque::Opaque<16usize>; pub type OpaqueQuaternion = crate::opaque::Opaque<16usize>; - pub type OpaqueAABB = crate::opaque::Opaque<24usize>; + pub type OpaqueAabb = crate::opaque::Opaque<24usize>; pub type OpaqueBasis = crate::opaque::Opaque<36usize>; pub type OpaqueTransform3D = crate::opaque::Opaque<48usize>; pub type OpaqueProjection = crate::opaque::Opaque<64usize>; pub type OpaqueColor = crate::opaque::Opaque<16usize>; pub type OpaqueStringName = crate::opaque::Opaque<8usize>; pub type OpaqueNodePath = crate::opaque::Opaque<8usize>; - pub type OpaqueRID = crate::opaque::Opaque<8usize>; + pub type OpaqueRid = crate::opaque::Opaque<8usize>; pub type OpaqueObject = crate::opaque::Opaque<8usize>; pub type OpaqueCallable = crate::opaque::Opaque<16usize>; pub type OpaqueSignal = crate::opaque::Opaque<16usize>; From 62cd271c28bfa090c5acaa5589fc5d13c0c7a640 Mon Sep 17 00:00:00 2001 From: Jan Haller Date: Thu, 26 Jan 2023 21:25:39 +0100 Subject: [PATCH 29/54] Clippy fixes related to inline strings in println!, write!, format! etc. --- godot-core/src/builtin/arrays.rs | 8 ++------ godot-core/src/builtin/variant/mod.rs | 4 ++-- godot-core/src/init/mod.rs | 2 +- godot-core/src/lib.rs | 4 ++-- godot-ffi/build.rs | 4 ++-- godot-macros/src/derive_godot_class.rs | 7 ++----- godot-macros/src/godot_api.rs | 2 +- godot-macros/src/itest.rs | 4 ++-- 8 files changed, 14 insertions(+), 21 deletions(-) diff --git a/godot-core/src/builtin/arrays.rs b/godot-core/src/builtin/arrays.rs index d931c9504..8a003d797 100644 --- a/godot-core/src/builtin/arrays.rs +++ b/godot-core/src/builtin/arrays.rs @@ -288,9 +288,7 @@ impl Array { let len = self.len(); assert!( index <= len, - "Array insertion index {} is out of bounds: length is {}", - index, - len + "Array insertion index {index} is out of bounds: length is {len}", ); self.as_inner().insert(to_i64(index), value); } @@ -358,9 +356,7 @@ impl Array { let len = self.len(); assert!( index < len, - "Array index {} is out of bounds: length is {}", - index, - len + "Array index {index} is out of bounds: length is {len}", ); } diff --git a/godot-core/src/builtin/variant/mod.rs b/godot-core/src/builtin/variant/mod.rs index 72ccb2d9c..9dac6f28d 100644 --- a/godot-core/src/builtin/variant/mod.rs +++ b/godot-core/src/builtin/variant/mod.rs @@ -200,7 +200,7 @@ impl PartialEq for Variant { impl fmt::Display for Variant { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let s = self.stringify(); - write!(f, "{}", s) + write!(f, "{s}") } } @@ -208,6 +208,6 @@ impl fmt::Debug for Variant { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { // TODO include variant type name let s = self.stringify(); - write!(f, "Variant({})", s) + write!(f, "Variant({s})") } } diff --git a/godot-core/src/init/mod.rs b/godot-core/src/init/mod.rs index 718600816..de8afeabf 100644 --- a/godot-core/src/init/mod.rs +++ b/godot-core/src/init/mod.rs @@ -201,7 +201,7 @@ impl InitLevel { sys::GDEXTENSION_INITIALIZATION_SCENE => Self::Scene, sys::GDEXTENSION_INITIALIZATION_EDITOR => Self::Editor, _ => { - eprintln!("WARNING: unknown initialization level {}", level); + eprintln!("WARNING: unknown initialization level {level}"); Self::Scene } } diff --git a/godot-core/src/lib.rs b/godot-core/src/lib.rs index 7e25ad54c..dfe5a92be 100644 --- a/godot-core/src/lib.rs +++ b/godot-core/src/lib.rs @@ -78,9 +78,9 @@ pub mod private { pub fn print_panic(err: Box) { if let Some(s) = err.downcast_ref::<&'static str>() { - log::godot_error!("rust-panic: {}", s); + log::godot_error!("rust-panic: {s}"); } else if let Some(s) = err.downcast_ref::() { - log::godot_error!("rust-panic: {}", s); + log::godot_error!("rust-panic: {s}"); } else { log::godot_error!("rust-panic of type ID {:?}", err.type_id()); } diff --git a/godot-ffi/build.rs b/godot-ffi/build.rs index b8a39b5d8..01c36324b 100644 --- a/godot-ffi/build.rs +++ b/godot-ffi/build.rs @@ -23,7 +23,7 @@ fn main() { fn run_bindgen(out_file: &Path) { let header_path = "../godot-codegen/input/gdextension_interface.h"; - println!("cargo:rerun-if-changed={}", header_path); + println!("cargo:rerun-if-changed={header_path}"); let builder = bindgen::Builder::default() .header(header_path) @@ -92,7 +92,7 @@ fn apple_include_path() -> Result { .trim_end(); let suffix = "usr/include"; - let directory = format!("{}/{}", prefix, suffix); + let directory = format!("{prefix}/{suffix}"); Ok(directory) } diff --git a/godot-macros/src/derive_godot_class.rs b/godot-macros/src/derive_godot_class.rs index a169527c8..5bbc89f63 100644 --- a/godot-macros/src/derive_godot_class.rs +++ b/godot-macros/src/derive_godot_class.rs @@ -255,15 +255,12 @@ impl ExportedField { Ok(value) } else { bail( - format!( - "#[export] attribute {} with a non-literal variant_type", - key - ), + format!("#[export] attribute {key} with a non-literal variant_type",), attr, )? } } else { - bail(format!("#[export] attribute without a {}", key), attr) + bail(format!("#[export] attribute without a {key}"), attr) } } } diff --git a/godot-macros/src/godot_api.rs b/godot-macros/src/godot_api.rs index 436e0bf13..bff0acbf7 100644 --- a/godot-macros/src/godot_api.rs +++ b/godot-macros/src/godot_api.rs @@ -270,7 +270,7 @@ fn transform_trait_impl(original_impl: Impl) -> Result { // Unknown methods which are declared inside trait impl are not supported (possibly compiler catches those first anyway) other_name => { return bail( - format!("Unsupported GodotExt method: {}", other_name), + format!("Unsupported GodotExt method: {other_name}"), &method.name, ) } diff --git a/godot-macros/src/itest.rs b/godot-macros/src/itest.rs index 570395177..fae8c009f 100644 --- a/godot-macros/src/itest.rs +++ b/godot-macros/src/itest.rs @@ -30,8 +30,8 @@ pub fn transform(input: TokenStream) -> Result { } let test_name = &func.name; - let init_msg = format!(" -- {}", test_name); - let error_msg = format!(" !! Test {} failed", test_name); + let init_msg = format!(" -- {test_name}"); + let error_msg = format!(" !! Test {test_name} failed"); let body = &func.body; Ok(quote! { From 5719d5902a8f90a8b6bbdca25e35c821e3a10e42 Mon Sep 17 00:00:00 2001 From: Thomas ten Cate Date: Thu, 26 Jan 2023 11:41:36 +0100 Subject: [PATCH 30/54] Several safety fixes and doc improvements for Array - Iterator is now sound and won't crash on modification while iterating - Renamed `iter()` to `iter_shared()` to make this risk more explicit - Removed `IntoIterator` impl which is very much not explicit - `Array::duplicate()` no longer segfaults - `Clone` impl replaced by `Share` Also implement `Array::slice()` --- godot-core/src/builtin/arrays.rs | 220 +++++++++++++++++++++++-------- itest/rust/src/array_test.rs | 86 ++++++++---- 2 files changed, 226 insertions(+), 80 deletions(-) diff --git a/godot-core/src/builtin/arrays.rs b/godot-core/src/builtin/arrays.rs index d931c9504..44b0045b8 100644 --- a/godot-core/src/builtin/arrays.rs +++ b/godot-core/src/builtin/arrays.rs @@ -7,6 +7,7 @@ use godot_ffi as sys; use crate::builtin::{inner, FromVariant, ToVariant, Variant, VariantConversionError}; +use crate::obj::Share; use std::fmt; use std::marker::PhantomData; use sys::types::*; @@ -19,7 +20,15 @@ use sys::{ffi_methods, interface_fn, GodotFfi}; /// /// Unlike GDScript, all indices and sizes are unsigned, so negative indices are not supported. /// -/// # Safety +/// # Reference semantics +/// +/// Like in GDScript, `Array` acts as a reference type: multiple `Array` instances may refer to the +/// same underlying array, and changes to one are visible in the other. +/// +/// To create a copy that shares data with the original array, use [`Share::share()`]. If you want +/// to create a copy of the data, use [`duplicate_shallow()`] or [`duplicate_deep()`]. +/// +/// # Thread safety /// /// Usage is safe if the `Array` is used on a single thread only. Concurrent reads on different /// threads are also safe, but any writes must be externally synchronized. The Rust compiler will @@ -112,12 +121,15 @@ impl Array { Ok(vec) } - /// Implements iteration over an `Array` by reference, but returns (cheap) copies of the - /// `Variant`s in the array. - pub fn iter(&self) -> ArrayIterator<'_> { + /// Returns an iterator over the `Array` by reference. Instead of references to elements as you + /// might expect, the iterator returns a (cheap, shallow) copy of each element. + /// + /// Notice that it's possible to modify the `Array` through another reference while iterating + /// over it. This will not result in unsoundness or crashes, but will cause the iterator to + /// behave in an unspecified way. + pub fn iter_shared(&self) -> ArrayIterator<'_> { ArrayIterator { - start: self.ptr(0), - len: self.len(), + array: self, next_idx: 0, _phantom: PhantomData, } @@ -135,15 +147,68 @@ impl Array { self.as_inner().resize(to_i64(size)); } - // TODO: find out why this segfaults (even on an empty array, regardless of deep = true/false) - // /// Returns a deep copy of the array. All nested arrays and dictionaries are duplicated and - // /// will not be shared with the original array. Note that any `Object`-derived elements will - // /// still be shallow copied. - // /// - // /// To create a shallow copy, use `clone()` instead. - // pub fn duplicate_deep(&self) -> Self { - // self.as_inner().duplicate(true) - // } + /// Returns a shallow copy of the array. All array elements are copied, but any reference types + /// (such as `Array`, `Dictionary` and `Object`) will still refer to the same value. + /// + /// To create a deep copy, use [`duplicate_deep()`] instead. To create a new reference to the + /// same array data, use [`share()`]. + pub fn duplicate_shallow(&self) -> Self { + self.as_inner().duplicate(false) + } + + /// Returns a deep copy of the array. All nested arrays and dictionaries are duplicated and + /// will not be shared with the original array. Note that any `Object`-derived elements will + /// still be shallow copied. + /// + /// To create a shallow copy, use [`duplicate_shallow()`] instead. To create a new reference to + /// the same array data, use [`share()`]. + pub fn duplicate_deep(&self) -> Self { + self.as_inner().duplicate(true) + } + + /// Returns the slice of the `Array`, from `begin` (inclusive) to `end` (exclusive), as a new + /// `Array`. + /// + /// The values of `begin` and `end` will be clamped to the array size. + /// + /// If specified, `step` is the relative index between source elements. It can be negative, + /// in which case `begin` must be higher than `end`. For example, + /// `Array::from(&[0, 1, 2, 3, 4, 5]).slice(5, 1, -2)` returns `[5, 3]`. + /// + /// Array elements are copied to the slice, but any reference types (such as `Array`, + /// `Dictionary` and `Object`) will still refer to the same value. To create a deep copy, use + /// [`slice_deep()`] instead. + pub fn slice_shallow(&self, begin: usize, end: usize, step: Option) -> Self { + assert_ne!(step, Some(0)); + let len = self.len(); + let begin = begin.min(len); + let end = end.min(len); + let step = step.unwrap_or(1); + self.as_inner() + .slice(to_i64(begin), to_i64(end), step.try_into().unwrap(), false) + } + + /// Returns the slice of the `Array`, from `begin` (inclusive) to `end` (exclusive), as a new + /// `Array`. + /// + /// The values of `begin` and `end` will be clamped to the array size. + /// + /// If specified, `step` is the relative index between source elements. It can be negative, + /// in which case `begin` must be higher than `end`. For example, + /// `Array::from(&[0, 1, 2, 3, 4, 5]).slice(5, 1, -2)` returns `[5, 3]`. + /// + /// All nested arrays and dictionaries are duplicated and will not be shared with the original + /// array. Note that any `Object`-derived elements will still be shallow copied. To create a + /// shallow copy, use [`slice_shallow()`] instead. + pub fn slice_deep(&self, begin: usize, end: usize, step: Option) -> Self { + let len = self.len(); + let begin = begin.min(len); + let end = end.min(len); + let step = step.unwrap_or(1); + assert!(step != 0); + self.as_inner() + .slice(to_i64(begin), to_i64(end), step.try_into().unwrap(), true) + } /// Returns the value at the specified index as a `Variant`. To convert to a specific type, use /// the available conversion methods on `Variant`, such as [`Variant::try_to`] or @@ -364,36 +429,52 @@ impl Array { ); } + /// Returns a pointer to the element at the given index. + /// + /// # Panics + /// + /// If `index` is out of bounds. fn ptr(&self, index: usize) -> *const Variant { - if self.is_empty() { - std::ptr::null() - } else { - self.check_bounds(index); - // SAFETY: We just checked that the index is not out of bounds. - let ptr = unsafe { - let item_ptr: sys::GDExtensionVariantPtr = - (interface_fn!(array_operator_index_const))(self.sys(), to_i64(index)); - item_ptr as *const Variant - }; - assert!(!ptr.is_null()); - ptr - } + self.check_bounds(index); + // SAFETY: We just checked that the index is not out of bounds. + let ptr = unsafe { self.ptr_unchecked(index) }; + assert!(!ptr.is_null()); + ptr } + /// Returns a mutable pointer to the element at the given index. + /// + /// # Panics + /// + /// If `index` is out of bounds. fn ptr_mut(&self, index: usize) -> *mut Variant { - if self.is_empty() { - std::ptr::null_mut() - } else { - self.check_bounds(index); - // SAFETY: We just checked that the index is not out of bounds. - let ptr = unsafe { - let item_ptr: sys::GDExtensionVariantPtr = - (interface_fn!(array_operator_index))(self.sys(), to_i64(index)); - item_ptr as *mut Variant - }; - assert!(!ptr.is_null()); - ptr - } + self.check_bounds(index); + // SAFETY: We just checked that the index is not out of bounds. + let ptr = unsafe { self.ptr_mut_unchecked(index) }; + assert!(!ptr.is_null()); + ptr + } + + /// Returns a pointer to the element at the given index. + /// + /// # Safety + /// + /// Calling this with an out-of-bounds index is undefined behavior. + unsafe fn ptr_unchecked(&self, index: usize) -> *const Variant { + let item_ptr: sys::GDExtensionVariantPtr = + (interface_fn!(array_operator_index_const))(self.sys(), to_i64(index)); + item_ptr as *const Variant + } + + /// Returns a mutable pointer to the element at the given index. + /// + /// # Safety + /// + /// Calling this with an out-of-bounds index is undefined behavior. + unsafe fn ptr_mut_unchecked(&self, index: usize) -> *mut Variant { + let item_ptr: sys::GDExtensionVariantPtr = + (interface_fn!(array_operator_index))(self.sys(), to_i64(index)); + item_ptr as *mut Variant } #[doc(hidden)] @@ -456,31 +537,24 @@ impl Extend for Array { } } -/// See [`Array::iter()`]. -#[cfg(not(any(gdext_test, doctest)))] -impl<'a> IntoIterator for &'a Array { - type Item = Variant; - type IntoIter = ArrayIterator<'a>; - fn into_iter(self) -> Self::IntoIter { - self.iter() - } -} - pub struct ArrayIterator<'a> { - start: *const Variant, - len: usize, + array: &'a Array, next_idx: usize, _phantom: PhantomData<&'a Array>, } +#[cfg(not(any(gdext_test, doctest)))] impl<'a> Iterator for ArrayIterator<'a> { type Item = Variant; fn next(&mut self) -> Option { - if self.next_idx < self.len { - let offset = to_isize(self.next_idx); + if self.next_idx < self.array.len() { + let idx = self.next_idx; self.next_idx += 1; - unsafe { Some((*self.start.offset(offset)).clone()) } + // Using `ptr_unchecked` rather than going through `get()` so we can avoid a second + // bounds check. + // SAFETY: We just checked that the index is not out of bounds. + Some(unsafe { (*self.array.ptr_unchecked(idx)).clone() }) } else { None } @@ -494,16 +568,46 @@ impl fmt::Debug for Array { } } +/// Creates a new reference to the data in this array. Changes to the original array will be +/// reflected in the copy and vice versa. +/// +/// To create a (mostly) independent copy instead, see [`Array::duplicate_shallow()`] and +/// [`Array::duplicate_deep()`]. +impl Share for Array { + fn share(&self) -> Self { + unsafe { + Self::from_sys_init(|self_ptr| { + let ctor = ::godot_ffi::builtin_fn!(array_construct_copy); + let args = [self.sys_const()]; + ctor(self_ptr, args.as_ptr()); + }) + } + } +} + impl_builtin_traits! { for Array { Default => array_construct_default; - Clone => array_construct_copy; Drop => array_destroy; } } impl GodotFfi for Array { - ffi_methods! { type sys::GDExtensionTypePtr = *mut Opaque; .. } + ffi_methods! { + type sys::GDExtensionTypePtr = *mut Opaque; + fn from_sys; + fn sys; + fn write_sys; + } + + unsafe fn from_sys_init(init_fn: impl FnOnce(sys::GDExtensionTypePtr)) -> Self { + // Can't use uninitialized pointer -- Array CoW implementation in C++ expects that on + // assignment, the target CoW pointer is either initialized or nullptr + + let mut result = Self::default(); + init_fn(result.sys_mut()); + result + } } fn to_i64(i: usize) -> i64 { diff --git a/itest/rust/src/array_test.rs b/itest/rust/src/array_test.rs index 08e1f1b27..289495f18 100644 --- a/itest/rust/src/array_test.rs +++ b/itest/rust/src/array_test.rs @@ -6,6 +6,7 @@ use crate::{expect_panic, itest}; use godot::builtin::{Array, FromVariant, GodotString, ToVariant}; +use godot::prelude::Share; pub fn run() -> bool { let mut ok = true; @@ -14,10 +15,13 @@ pub fn run() -> bool { ok &= array_from_iterator(); ok &= array_from(); ok &= array_try_to_vec(); - ok &= array_into_iterator(); - ok &= array_clone(); - // ok &= array_duplicate_deep(); + ok &= array_iter_shared(); ok &= array_hash(); + ok &= array_share(); + ok &= array_duplicate_shallow(); + ok &= array_duplicate_deep(); + ok &= array_slice_shallow(); + ok &= array_slice_deep(); ok &= array_get(); ok &= array_first_last(); ok &= array_binary_search(); @@ -69,42 +73,80 @@ fn array_try_to_vec() { } #[itest] -fn array_into_iterator() { +fn array_iter_shared() { let array = Array::from(&[1, 2]); - let mut iter = array.into_iter(); + let mut iter = array.iter_shared(); assert_eq!(iter.next(), Some(1.to_variant())); assert_eq!(iter.next(), Some(2.to_variant())); assert_eq!(iter.next(), None); } #[itest] -fn array_clone() { +fn array_hash() { + let array = Array::from(&[1, 2]); + // Just testing that it converts successfully from i64 to u32. + array.hash(); +} + +#[itest] +fn array_share() { + let mut array = Array::from(&[1, 2]); + let shared = array.share(); + array.set(0, 3.to_variant()); + assert_eq!(shared.get(0), 3.to_variant()); +} + +#[itest] +fn array_duplicate_shallow() { let subarray = Array::from(&[2, 3]); let array = Array::from(&[1.to_variant(), subarray.to_variant()]); - #[allow(clippy::redundant_clone)] - let clone = array.clone(); - Array::try_from_variant(&clone.get(1)) + let duplicate = array.duplicate_shallow(); + Array::try_from_variant(&duplicate.get(1)) .unwrap() .set(0, 4.to_variant()); assert_eq!(subarray.get(0), 4.to_variant()); } #[itest] -fn array_hash() { - let array = Array::from(&[1, 2]); - // Just testing that it converts successfully from i64 to u32. - array.hash(); +fn array_duplicate_deep() { + let subarray = Array::from(&[2, 3]); + let array = Array::from(&[1.to_variant(), subarray.to_variant()]); + let duplicate = array.duplicate_deep(); + Array::try_from_variant(&duplicate.get(1)) + .unwrap() + .set(0, 4.to_variant()); + assert_eq!(subarray.get(0), 2.to_variant()); +} + +#[itest] +fn array_slice_shallow() { + let array = Array::from(&[0, 1, 2, 3, 4, 5]); + let slice = array.slice_shallow(5, 1, Some(-2)); + assert_eq!(slice.try_to_vec::().unwrap(), vec![5, 3]); + + let subarray = Array::from(&[2, 3]); + let array = Array::from(&[1.to_variant(), subarray.to_variant()]); + let slice = array.slice_shallow(1, 2, None); + Array::try_from_variant(&slice.get(0)) + .unwrap() + .set(0, 4.to_variant()); + assert_eq!(subarray.get(0), 4.to_variant()); } -// TODO: enable once the implementation no longer segfaults -// #[itest] -// fn array_duplicate_deep() { -// let subarray = Array::from(&[2, 3]); -// let array = Array::from(&[1.to_variant(), subarray.to_variant()]); -// let mut clone = array.duplicate_deep(); -// Array::try_from_variant(clone.get(1)).unwrap().set(0, 4.to_variant()); -// assert_eq!(subarray.get(0), 3.to_variant()); -// } +#[itest] +fn array_slice_deep() { + let array = Array::from(&[0, 1, 2, 3, 4, 5]); + let slice = array.slice_deep(5, 1, Some(-2)); + assert_eq!(slice.try_to_vec::().unwrap(), vec![5, 3]); + + let subarray = Array::from(&[2, 3]); + let array = Array::from(&[1.to_variant(), subarray.to_variant()]); + let slice = array.slice_deep(1, 2, None); + Array::try_from_variant(&slice.get(0)) + .unwrap() + .set(0, 4.to_variant()); + assert_eq!(subarray.get(0), 2.to_variant()); +} #[itest] fn array_get() { From 82a8a1629837fdc780c7c1545acf9f5259509dcd Mon Sep 17 00:00:00 2001 From: Thomas ten Cate Date: Thu, 26 Jan 2023 17:24:16 +0100 Subject: [PATCH 31/54] Implement packed arrays non-generically The ideal is to have a `struct PackedArray`, where `T` implements some `PackedArrayElement` trait. But since the autogenerated `InnerPacked*Array` types are entirely independent at the moment and don't implement a common trait, that would require indirecting every method implementation through `PackedArrayElement`, which results in mentioning each method call three times: once in `impl PackedArray`, once in `PackedArrayElement` and once in `impl PackedArrayElement for T`. So, for now, we use a big (but quite straightforward) macro to define each `Packed*Array` as a separate type. --- godot-core/src/builtin/arrays.rs | 34 +- godot-core/src/builtin/mod.rs | 26 +- godot-core/src/builtin/packed_array.rs | 553 ++++++++++++++++++++++++ godot-core/src/builtin/variant/impls.rs | 10 +- itest/rust/src/array_test.rs | 1 + itest/rust/src/lib.rs | 2 + itest/rust/src/packed_array_test.rs | 185 ++++++++ 7 files changed, 764 insertions(+), 47 deletions(-) create mode 100644 godot-core/src/builtin/packed_array.rs create mode 100644 itest/rust/src/packed_array_test.rs diff --git a/godot-core/src/builtin/arrays.rs b/godot-core/src/builtin/arrays.rs index 270bbfeb0..4671f4018 100644 --- a/godot-core/src/builtin/arrays.rs +++ b/godot-core/src/builtin/arrays.rs @@ -6,7 +6,7 @@ use godot_ffi as sys; -use crate::builtin::{inner, FromVariant, ToVariant, Variant, VariantConversionError}; +use crate::builtin::*; use crate::obj::Share; use std::fmt; use std::marker::PhantomData; @@ -39,16 +39,6 @@ pub struct Array { opaque: sys::types::OpaqueArray, } -impl_builtin_stub!(PackedByteArray, OpaquePackedByteArray); -impl_builtin_stub!(PackedColorArray, OpaquePackedColorArray); -impl_builtin_stub!(PackedFloat32Array, OpaquePackedFloat32Array); -impl_builtin_stub!(PackedFloat64Array, OpaquePackedFloat64Array); -impl_builtin_stub!(PackedInt32Array, OpaquePackedInt32Array); -impl_builtin_stub!(PackedInt64Array, OpaquePackedInt64Array); -impl_builtin_stub!(PackedStringArray, OpaquePackedStringArray); -impl_builtin_stub!(PackedVector2Array, OpaquePackedVector2Array); -impl_builtin_stub!(PackedVector3Array, OpaquePackedVector3Array); - impl_builtin_froms!(Array; PackedByteArray => array_from_packed_byte_array, PackedColorArray => array_from_packed_color_array, @@ -61,16 +51,6 @@ impl_builtin_froms!(Array; PackedVector3Array => array_from_packed_vector3_array, ); -impl_builtin_froms!(PackedByteArray; Array => packed_byte_array_from_array); -impl_builtin_froms!(PackedColorArray; Array => packed_color_array_from_array); -impl_builtin_froms!(PackedFloat32Array; Array => packed_float32_array_from_array); -impl_builtin_froms!(PackedFloat64Array; Array => packed_float64_array_from_array); -impl_builtin_froms!(PackedInt32Array; Array => packed_int32_array_from_array); -impl_builtin_froms!(PackedInt64Array; Array => packed_int64_array_from_array); -impl_builtin_froms!(PackedStringArray; Array => packed_string_array_from_array); -impl_builtin_froms!(PackedVector2Array; Array => packed_vector2_array_from_array); -impl_builtin_froms!(PackedVector3Array; Array => packed_vector3_array_from_array); - impl Array { fn from_opaque(opaque: sys::types::OpaqueArray) -> Self { Self { opaque } @@ -606,18 +586,6 @@ impl GodotFfi for Array { } } -fn to_i64(i: usize) -> i64 { - i.try_into().unwrap() -} - -fn to_usize(i: i64) -> usize { - i.try_into().unwrap() -} - -fn to_isize(i: usize) -> isize { - i.try_into().unwrap() -} - #[repr(C)] pub struct TypedArray { opaque: OpaqueArray, diff --git a/godot-core/src/builtin/mod.rs b/godot-core/src/builtin/mod.rs index 40f326bff..7961df97c 100644 --- a/godot-core/src/builtin/mod.rs +++ b/godot-core/src/builtin/mod.rs @@ -39,6 +39,7 @@ mod arrays; mod color; mod node_path; mod others; +mod packed_array; mod string; mod string_name; mod variant; @@ -55,6 +56,7 @@ pub use arrays::*; pub use color::*; pub use node_path::*; pub use others::*; +pub use packed_array::*; pub use string::*; pub use string_name::*; pub use variant::*; @@ -71,16 +73,14 @@ pub mod inner { pub use crate::gen::builtin_classes::*; } -// pub struct PackedArray { -// _phantom: std::marker::PhantomData -// } -// -// pub type PackedByteArray = PackedArray; -// pub type PackedInt32Array = PackedArray; -// pub type PackedInt64Array = PackedArray; -// pub type PackedFloat32Array = PackedArray; -// pub type PackedFloat64Array = PackedArray; -// pub type PackedStringArray = PackedArray; -// pub type PackedVector2Array = PackedArray; -// pub type PackedVector3Array = PackedArray; -// pub type PackedColorArray = PackedArray; +pub(crate) fn to_i64(i: usize) -> i64 { + i.try_into().unwrap() +} + +pub(crate) fn to_usize(i: i64) -> usize { + i.try_into().unwrap() +} + +pub(crate) fn to_isize(i: usize) -> isize { + i.try_into().unwrap() +} diff --git a/godot-core/src/builtin/packed_array.rs b/godot-core/src/builtin/packed_array.rs new file mode 100644 index 000000000..0adef5de9 --- /dev/null +++ b/godot-core/src/builtin/packed_array.rs @@ -0,0 +1,553 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +use godot_ffi as sys; + +use crate::builtin::*; +use std::fmt; +use sys::types::*; +use sys::{ffi_methods, interface_fn, GodotFfi, TagString, TagType}; + +/// Defines and implements a single packed array type. This macro is not hygienic and is meant to +/// be used only in the current module. +macro_rules! impl_packed_array { + ( + // Name of the type to define, e.g. `PackedByteArray`. + $PackedArray:ident, + // Type of elements contained in the array, e.g. `u8`. + $Element:ty, + // Name of wrapped opaque type, e.g. `OpaquePackedByteArray`. + $Opaque:ty, + // Name of inner type, e.g. `InnerPackedByteArray`. + $Inner:ident, + // Name of type that represents elements in function call arguments, e.g. `i64`. See + // `Self::into_arg()`. + $Arg:ty, + // Type that is returned from `$operator_index` and `$operator_index_const`. + $IndexRetType:ty, + // Name of default constructor function from FFI, e.g. + // `packed_byte_array_construct_default`. + $construct_default:ident, + // Name of copy constructor function from FFI, e.g. + // `packed_byte_array_construct_copy`. + $construct_copy:ident, + // Name of constructor function from `Array` from FFI, e.g. `packed_byte_array_from_array`. + $from_array:ident, + // Name of destructor function from FFI, e.g. `packed_byte_array_destroy`. + $destroy:ident, + // Name of index operator from FFI, e.g. `packed_byte_array_operator_index`. + $operator_index:ident, + // Name of const index operator from FFI, e.g. `packed_byte_array_operator_index_const`. + $operator_index_const:ident, + ) => { + // TODO expand type names in doc comments (use e.g. `paste` crate) + /// Implements Godot's `$PackedArray` type, which is an efficient array of `$Element`s. + /// + /// Note that, unlike `Array`, this type has value semantics: each copy will be independent + /// of the original. (Under the hood, Godot uses copy-on-write, so copies are still cheap + /// to make.) + /// + /// # Thread safety + /// + /// Usage is safe if the `$PackedArray` is used on a single thread only. Concurrent reads on + /// different threads are also safe, but any writes must be externally synchronized. The + /// Rust compiler will enforce this as long as you use only Rust threads, but it cannot + /// protect against concurrent modification on other threads (e.g. created through + /// GDScript). + #[repr(C)] + pub struct $PackedArray { + opaque: $Opaque, + } + + impl $PackedArray { + fn from_opaque(opaque: $Opaque) -> Self { + Self { opaque } + } + } + + // This impl relies on `$Inner` which is not (yet) available in unit tests + #[cfg(not(any(gdext_test, doctest)))] + impl $PackedArray { + /// Constructs an empty array. + pub fn new() -> Self { + Self::default() + } + + /// Returns the number of elements in the array. Equivalent of `size()` in Godot. + pub fn len(&self) -> usize { + to_usize(self.as_inner().size()) + } + + /// Returns `true` if the array is empty. + pub fn is_empty(&self) -> bool { + self.as_inner().is_empty() + } + + /// Converts this array to a Rust vector, making a copy of its contents. + pub fn to_vec(&self) -> Vec<$Element> { + let len = self.len(); + let mut vec = Vec::with_capacity(len); + let ptr = self.ptr(0); + for offset in 0..to_isize(len) { + // SAFETY: Packed arrays are stored contiguously in memory, so we can use + // pointer arithmetic instead of going through `$operator_index_const` for + // every index. + // Note that we do need to use `.clone()` because `GodotString` is refcounted; + // we can't just do a memcpy. + let element = unsafe { (*ptr.offset(offset)).clone() }; + vec.push(element); + } + vec + } + + /// Clears the array, removing all elements. + pub fn clear(&mut self) { + self.as_inner().clear(); + } + + /// Resizes the array to contain a different number of elements. If the new size is + /// smaller, elements are removed from the end. If the new size is larger, new elements + /// are set to [`Default::default()`]. + pub fn resize(&mut self, size: usize) { + self.as_inner().resize(to_i64(size)); + } + + /// Returns a slice of the array, from `begin` (inclusive) to `end` (exclusive), as a + /// new array. + /// + /// The values of `begin` and `end` will be clamped to the array size. + pub fn slice(&self, begin: usize, end: usize) -> Self { + let len = self.len(); + let begin = begin.min(len); + let end = end.min(len); + self.as_inner().slice(to_i64(begin), to_i64(end)) + } + + /// Returns a copy of the value at the specified index. + /// + /// # Panics + /// + /// If `index` is out of bounds. + pub fn get(&self, index: usize) -> $Element { + let ptr = self.ptr(index); + // SAFETY: `ptr` just verified that the index is not out of bounds. + unsafe { (*ptr).clone() } + } + + /// Finds the index of an existing value in a sorted array using binary search. + /// Equivalent of `bsearch` in GDScript. + /// + /// If the value is not present in the array, returns the insertion index that would + /// maintain sorting order. + /// + /// Calling `binary_search` on an unsorted array results in unspecified behavior. + pub fn binary_search(&self, value: $Element) -> usize { + to_usize(self.as_inner().bsearch(Self::into_arg(value), true)) + } + + /// Returns the number of times a value is in the array. + pub fn count(&self, value: $Element) -> usize { + to_usize(self.as_inner().count(Self::into_arg(value))) + } + + /// Returns `true` if the array contains the given value. Equivalent of `has` in + /// GDScript. + pub fn contains(&self, value: $Element) -> bool { + self.as_inner().has(Self::into_arg(value)) + } + + /// Searches the array for the first occurrence of a value and returns its index, or + /// `None` if not found. Starts searching at index `from`; pass `None` to search the + /// entire array. + pub fn find(&self, value: $Element, from: Option) -> Option { + let from = to_i64(from.unwrap_or(0)); + let index = self.as_inner().find(Self::into_arg(value), from); + if index >= 0 { + Some(index.try_into().unwrap()) + } else { + None + } + } + + /// Searches the array backwards for the last occurrence of a value and returns its + /// index, or `None` if not found. Starts searching at index `from`; pass `None` to + /// search the entire array. + pub fn rfind(&self, value: $Element, from: Option) -> Option { + let from = from.map(to_i64).unwrap_or(-1); + let index = self.as_inner().rfind(Self::into_arg(value), from); + // It's not documented, but `rfind` returns -1 if not found. + if index >= 0 { + Some(to_usize(index)) + } else { + None + } + } + + /// Sets the value at the specified index. + /// + /// # Panics + /// + /// If `index` is out of bounds. + pub fn set(&mut self, index: usize, value: $Element) { + let ptr_mut = self.ptr_mut(index); + // SAFETY: `ptr_mut` just checked that the index is not out of bounds. + unsafe { + *ptr_mut = value; + } + } + + /// Appends an element to the end of the array. Equivalent of `append` and `push_back` + /// in GDScript. + pub fn push(&mut self, value: $Element) { + self.as_inner().push_back(Self::into_arg(value)); + } + + /// Inserts a new element at a given index in the array. The index must be valid, or at + /// the end of the array (`index == len()`). + /// + /// Note: On large arrays, this method is much slower than `push` as it will move all + /// the array's elements after the inserted element. The larger the array, the slower + /// `insert` will be. + pub fn insert(&mut self, index: usize, value: $Element) { + let len = self.len(); + assert!( + index <= len, + "Array insertion index {index} is out of bounds: length is {len}"); + self.as_inner().insert(to_i64(index), Self::into_arg(value)); + } + + /// Removes and returns the element at the specified index. Similar to `remove_at` in + /// GDScript, but also returns the removed value. + /// + /// On large arrays, this method is much slower than `pop_back` as it will move all the array's + /// elements after the removed element. The larger the array, the slower `remove` will be. + /// + /// # Panics + /// + /// If `index` is out of bounds. + // Design note: This returns the removed value instead of `()` for consistency with + // `Array` and with `Vec::remove`. Compared to shifting all the subsequent array + // elements to their new position, the overhead of retrieving this element is trivial. + pub fn remove(&mut self, index: usize) -> $Element { + self.check_bounds(index); + let element = self.get(index); + self.as_inner().remove_at(to_i64(index)); + element + } + + /// Assigns the given value to all elements in the array. This can be used together + /// with `resize` to create an array with a given size and initialized elements. + pub fn fill(&mut self, value: $Element) { + self.as_inner().fill(Self::into_arg(value)); + } + + /// Appends another array at the end of this array. Equivalent of `append_array` in + /// GDScript. + pub fn extend_array(&mut self, other: &$PackedArray) { + self.as_inner().append_array(other.clone()); + } + + /// Reverses the order of the elements in the array. + pub fn reverse(&mut self) { + self.as_inner().reverse(); + } + + /// Sorts the elements of the array in ascending order. + // Presumably, just like `Array`, this is not a stable sort so we might call it + // `sort_unstable`. But Packed*Array elements that compare equal are always identical, + // so it doesn't matter. + pub fn sort(&mut self) { + self.as_inner().sort(); + } + + /// Asserts that the given index refers to an existing element. + /// + /// # Panics + /// + /// If `index` is out of bounds. + fn check_bounds(&self, index: usize) { + let len = self.len(); + assert!( + index < len, + "Array index {index} is out of bounds: length is {len}"); + } + + /// Returns a pointer to the element at the given index. + /// + /// # Panics + /// + /// If `index` is out of bounds. + fn ptr(&self, index: usize) -> *const $Element { + self.check_bounds(index); + // SAFETY: We just checked that the index is not out of bounds. + let ptr = unsafe { + let item_ptr: *const $IndexRetType = + (interface_fn!($operator_index_const))(self.sys(), to_i64(index)); + item_ptr as *const $Element + }; + assert!(!ptr.is_null()); + ptr + } + + /// Returns a mutable pointer to the element at the given index. + /// + /// # Panics + /// + /// If `index` is out of bounds. + fn ptr_mut(&self, index: usize) -> *mut $Element { + self.check_bounds(index); + // SAFETY: We just checked that the index is not out of bounds. + let ptr = unsafe { + let item_ptr: *mut $IndexRetType = + (interface_fn!($operator_index))(self.sys(), to_i64(index)); + item_ptr as *mut $Element + }; + assert!(!ptr.is_null()); + ptr + } + + /// Converts an `$Element` into a value that can be passed into API functions. For most + /// types, this is a no-op. But `u8` and `i32` are widened to `i64`, and `f32` is + /// widened to `f64`. + #[inline] + fn into_arg(e: $Element) -> $Arg { + e.into() + } + + #[doc(hidden)] + pub fn as_inner(&self) -> inner::$Inner<'_> { + inner::$Inner::from_outer(self) + } + } + + impl_builtin_traits! { + for $PackedArray { + Default => $construct_default; + Clone => $construct_copy; + Drop => $destroy; + } + } + + /// Creates a `$PackedArray` from the given Rust array. + #[cfg(not(any(gdext_test, doctest)))] + impl From<&[$Element; N]> for $PackedArray { + fn from(arr: &[$Element; N]) -> Self { + Self::from(&arr[..]) + } + } + + /// Creates a `$PackedArray` from the given slice. + #[cfg(not(any(gdext_test, doctest)))] + impl From<&[$Element]> for $PackedArray { + fn from(slice: &[$Element]) -> Self { + let mut array = Self::new(); + let len = slice.len(); + if len == 0 { + return array; + } + array.resize(len); + let ptr = array.ptr_mut(0); + for (i, element) in slice.iter().enumerate() { + // SAFETY: The array contains exactly `len` elements, stored contiguously in memory. + unsafe { + // `GodotString` does not implement `Copy` so we have to call `.clone()` + // here. + *ptr.offset(to_isize(i)) = element.clone(); + } + } + array + } + } + + /// Creates a `$PackedArray` from an iterator. + #[cfg(not(any(gdext_test, doctest)))] + impl FromIterator<$Element> for $PackedArray { + fn from_iter>(iter: I) -> Self { + let mut array = $PackedArray::default(); + array.extend(iter); + array + } + } + + /// Extends a `$PackedArray` with the contents of an iterator. + #[cfg(not(any(gdext_test, doctest)))] + impl Extend<$Element> for $PackedArray { + fn extend>(&mut self, iter: I) { + // Unfortunately the GDExtension API does not offer the equivalent of `Vec::reserve`. + // Otherwise we could use it to pre-allocate based on `iter.size_hint()`. + // + // A faster implementation using `resize()` and direct pointer writes might still be + // possible. + for item in iter.into_iter() { + self.push(item); + } + } + } + + impl_builtin_froms!($PackedArray; Array => $from_array); + + impl fmt::Debug for $PackedArray { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + // Going through `Variant` because there doesn't seem to be a direct way. + write!(f, "{:?}", self.to_variant().stringify()) + } + } + + impl GodotFfi for $PackedArray { + ffi_methods! { + type sys::GDExtensionTypePtr = *mut Opaque; + fn from_sys; + fn sys; + fn write_sys; + } + + unsafe fn from_sys_init(init_fn: impl FnOnce(sys::GDExtensionTypePtr)) -> Self { + // Can't use uninitialized pointer -- Vector CoW implementation in C++ expects that + // on assignment, the target CoW pointer is either initialized or nullptr + + let mut result = Self::default(); + init_fn(result.sys_mut()); + result + } + + } + } +} + +impl_packed_array!( + PackedByteArray, + u8, + OpaquePackedByteArray, + InnerPackedByteArray, + i64, + u8, + packed_byte_array_construct_default, + packed_byte_array_construct_copy, + packed_byte_array_from_array, + packed_byte_array_destroy, + packed_byte_array_operator_index, + packed_byte_array_operator_index_const, +); + +impl_packed_array!( + PackedInt32Array, + i32, + OpaquePackedInt32Array, + InnerPackedInt32Array, + i64, + i32, + packed_int32_array_construct_default, + packed_int32_array_construct_copy, + packed_int32_array_from_array, + packed_int32_array_destroy, + packed_int32_array_operator_index, + packed_int32_array_operator_index_const, +); + +impl_packed_array!( + PackedInt64Array, + i64, + OpaquePackedInt64Array, + InnerPackedInt64Array, + i64, + i64, + packed_int64_array_construct_default, + packed_int64_array_construct_copy, + packed_int64_array_from_array, + packed_int64_array_destroy, + packed_int64_array_operator_index, + packed_int64_array_operator_index_const, +); + +impl_packed_array!( + PackedFloat32Array, + f32, + OpaquePackedFloat32Array, + InnerPackedFloat32Array, + f64, + f32, + packed_float32_array_construct_default, + packed_float32_array_construct_copy, + packed_float32_array_from_array, + packed_float32_array_destroy, + packed_float32_array_operator_index, + packed_float32_array_operator_index_const, +); + +impl_packed_array!( + PackedFloat64Array, + f64, + OpaquePackedFloat64Array, + InnerPackedFloat64Array, + f64, + f64, + packed_float64_array_construct_default, + packed_float64_array_construct_copy, + packed_float64_array_from_array, + packed_float64_array_destroy, + packed_float64_array_operator_index, + packed_float64_array_operator_index_const, +); + +impl_packed_array!( + PackedStringArray, + GodotString, + OpaquePackedStringArray, + InnerPackedStringArray, + GodotString, + TagString, + packed_string_array_construct_default, + packed_string_array_construct_copy, + packed_string_array_from_array, + packed_string_array_destroy, + packed_string_array_operator_index, + packed_string_array_operator_index_const, +); + +impl_packed_array!( + PackedVector2Array, + Vector2, + OpaquePackedVector2Array, + InnerPackedVector2Array, + Vector2, + TagType, + packed_vector2_array_construct_default, + packed_vector2_array_construct_copy, + packed_vector2_array_from_array, + packed_vector2_array_destroy, + packed_vector2_array_operator_index, + packed_vector2_array_operator_index_const, +); + +impl_packed_array!( + PackedVector3Array, + Vector3, + OpaquePackedVector3Array, + InnerPackedVector3Array, + Vector3, + TagType, + packed_vector3_array_construct_default, + packed_vector3_array_construct_copy, + packed_vector3_array_from_array, + packed_vector3_array_destroy, + packed_vector3_array_operator_index, + packed_vector3_array_operator_index_const, +); + +impl_packed_array!( + PackedColorArray, + Color, + OpaquePackedColorArray, + InnerPackedColorArray, + Color, + TagType, + packed_color_array_construct_default, + packed_color_array_construct_copy, + packed_color_array_from_array, + packed_color_array_destroy, + packed_color_array_operator_index, + packed_color_array_operator_index_const, +); diff --git a/godot-core/src/builtin/variant/impls.rs b/godot-core/src/builtin/variant/impls.rs index a89aa48e1..1bb29a83f 100644 --- a/godot-core/src/builtin/variant/impls.rs +++ b/godot-core/src/builtin/variant/impls.rs @@ -142,7 +142,15 @@ mod impls { impl_variant_traits!(GodotString, string_to_variant, string_from_variant, String); impl_variant_traits!(StringName, string_name_to_variant, string_name_from_variant, StringName); impl_variant_traits!(Array, array_to_variant, array_from_variant, Array); - + impl_variant_traits!(PackedByteArray, packed_byte_array_to_variant, packed_byte_array_from_variant, PackedByteArray); + impl_variant_traits!(PackedInt32Array, packed_int32_array_to_variant, packed_int32_array_from_variant, PackedInt32Array); + impl_variant_traits!(PackedInt64Array, packed_int64_array_to_variant, packed_int64_array_from_variant, PackedInt64Array); + impl_variant_traits!(PackedFloat32Array, packed_float32_array_to_variant, packed_float32_array_from_variant, PackedFloat32Array); + impl_variant_traits!(PackedFloat64Array, packed_float64_array_to_variant, packed_float64_array_from_variant, PackedFloat64Array); + impl_variant_traits!(PackedStringArray, packed_string_array_to_variant, packed_string_array_from_variant, PackedStringArray); + impl_variant_traits!(PackedVector2Array, packed_vector2_array_to_variant, packed_vector2_array_from_variant, PackedVector2Array); + impl_variant_traits!(PackedVector3Array, packed_vector3_array_to_variant, packed_vector3_array_from_variant, PackedVector3Array); + impl_variant_traits!(PackedColorArray, packed_color_array_to_variant, packed_color_array_from_variant, PackedColorArray); impl_variant_traits!(i64, int_to_variant, int_from_variant, Int, GDEXTENSION_METHOD_ARGUMENT_METADATA_INT_IS_INT64); impl_variant_traits_int!(i8, GDEXTENSION_METHOD_ARGUMENT_METADATA_INT_IS_INT8); diff --git a/itest/rust/src/array_test.rs b/itest/rust/src/array_test.rs index 289495f18..aa3995ee4 100644 --- a/itest/rust/src/array_test.rs +++ b/itest/rust/src/array_test.rs @@ -28,6 +28,7 @@ pub fn run() -> bool { ok &= array_find(); ok &= array_rfind(); ok &= array_min_max(); + ok &= array_pick_random(); ok &= array_set(); ok &= array_push_pop(); ok &= array_insert(); diff --git a/itest/rust/src/lib.rs b/itest/rust/src/lib.rs index 669df563a..71a5a0026 100644 --- a/itest/rust/src/lib.rs +++ b/itest/rust/src/lib.rs @@ -20,6 +20,7 @@ mod export_test; mod gdscript_ffi_test; mod node_test; mod object_test; +mod packed_array_test; mod singleton_test; mod string_test; mod utilities_test; @@ -38,6 +39,7 @@ fn run_tests() -> bool { ok &= singleton_test::run(); ok &= string_test::run(); ok &= array_test::run(); + ok &= packed_array_test::run(); ok &= utilities_test::run(); ok &= variant_test::run(); ok &= virtual_methods_test::run(); diff --git a/itest/rust/src/packed_array_test.rs b/itest/rust/src/packed_array_test.rs new file mode 100644 index 000000000..aca9ad1ad --- /dev/null +++ b/itest/rust/src/packed_array_test.rs @@ -0,0 +1,185 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +use crate::{expect_panic, itest}; +use godot::builtin::PackedByteArray; + +pub fn run() -> bool { + let mut ok = true; + ok &= packed_array_default(); + ok &= packed_array_new(); + ok &= packed_array_from_iterator(); + ok &= packed_array_from(); + ok &= packed_array_to_vec(); + // ok &= packed_array_into_iterator(); + ok &= packed_array_clone(); + ok &= packed_array_slice(); + ok &= packed_array_get(); + ok &= packed_array_binary_search(); + ok &= packed_array_find(); + ok &= packed_array_rfind(); + ok &= packed_array_set(); + ok &= packed_array_push(); + ok &= packed_array_insert(); + ok &= packed_array_extend(); + ok &= packed_array_reverse(); + ok &= packed_array_sort(); + ok +} + +#[itest] +fn packed_array_default() { + assert_eq!(PackedByteArray::default().len(), 0); +} + +#[itest] +fn packed_array_new() { + assert_eq!(PackedByteArray::new().len(), 0); +} + +#[itest] +fn packed_array_from_iterator() { + let array = PackedByteArray::from_iter([1, 2]); + + assert_eq!(array.len(), 2); + assert_eq!(array.get(0), 1); + assert_eq!(array.get(1), 2); +} + +#[itest] +fn packed_array_from() { + let array = PackedByteArray::from(&[1, 2]); + + assert_eq!(array.len(), 2); + assert_eq!(array.get(0), 1); + assert_eq!(array.get(1), 2); +} + +#[itest] +fn packed_array_to_vec() { + let array = PackedByteArray::from(&[1, 2]); + assert_eq!(array.to_vec(), vec![1, 2]); +} + +// #[itest] +// fn packed_array_into_iterator() { +// let array = Array::from(&[1, 2]); +// let mut iter = array.into_iter(); +// assert_eq!(iter.next(), Some(1)); +// assert_eq!(iter.next(), Some(2)); +// assert_eq!(iter.next(), None); +// } + +#[itest] +fn packed_array_clone() { + let mut array = PackedByteArray::from(&[1, 2]); + #[allow(clippy::redundant_clone)] + let clone = array.clone(); + array.set(0, 3); + assert_eq!(clone.get(0), 1); +} + +#[itest] +fn packed_array_slice() { + let array = PackedByteArray::from(&[1, 2, 3]); + let slice = array.slice(1, 2); + assert_eq!(slice.to_vec(), vec![2]); +} + +#[itest] +fn packed_array_get() { + let array = PackedByteArray::from(&[1, 2]); + + assert_eq!(array.get(0), 1); + assert_eq!(array.get(1), 2); + expect_panic("Array index 2 out of bounds: length is 2", || { + array.get(2); + }); +} + +#[itest] +fn packed_array_binary_search() { + let array = PackedByteArray::from(&[1, 3]); + + assert_eq!(array.binary_search(0), 0); + assert_eq!(array.binary_search(1), 0); + assert_eq!(array.binary_search(2), 1); + assert_eq!(array.binary_search(3), 1); + assert_eq!(array.binary_search(4), 2); +} + +#[itest] +fn packed_array_find() { + let array = PackedByteArray::from(&[1, 2, 1]); + + assert_eq!(array.find(0, None), None); + assert_eq!(array.find(1, None), Some(0)); + assert_eq!(array.find(1, Some(1)), Some(2)); +} + +#[itest] +fn packed_array_rfind() { + let array = PackedByteArray::from(&[1, 2, 1]); + + assert_eq!(array.rfind(0, None), None); + assert_eq!(array.rfind(1, None), Some(2)); + assert_eq!(array.rfind(1, Some(1)), Some(0)); +} + +#[itest] +fn packed_array_set() { + let mut array = PackedByteArray::from(&[1, 2]); + + array.set(0, 3); + assert_eq!(array.get(0), 3); + + expect_panic("Array index 2 out of bounds: length is 2", move || { + array.set(2, 4); + }); +} + +#[itest] +fn packed_array_push() { + let mut array = PackedByteArray::from(&[1, 2]); + + array.push(3); + + assert_eq!(array.len(), 3); + assert_eq!(array.get(2), 3); +} + +#[itest] +fn packed_array_insert() { + let mut array = PackedByteArray::from(&[1, 2]); + + array.insert(0, 3); + assert_eq!(array.to_vec(), vec![3, 1, 2]); + + array.insert(3, 4); + assert_eq!(array.to_vec(), vec![3, 1, 2, 4]); +} + +#[itest] +fn packed_array_extend() { + let mut array = PackedByteArray::from(&[1, 2]); + let other = PackedByteArray::from(&[3, 4]); + array.extend_array(&other); + assert_eq!(array.to_vec(), vec![1, 2, 3, 4]); +} + +#[itest] +fn packed_array_sort() { + let mut array = PackedByteArray::from(&[2, 1]); + array.sort(); + assert_eq!(array.to_vec(), vec![1, 2]); +} + +#[itest] +fn packed_array_reverse() { + let mut array = PackedByteArray::from(&[1, 2]); + array.reverse(); + assert_eq!(array.to_vec(), vec![2, 1]); +} From ce490dbb133209628307f63708894127e012a00e Mon Sep 17 00:00:00 2001 From: Jan Haller Date: Thu, 26 Jan 2023 23:24:15 +0100 Subject: [PATCH 32/54] CI: most jobs (test, itest, clippy) now use Godot In particular: * Split installation and integration tests into separate composite workflows * Centralize flag for LLVM usage (on macOS) * Simplify invocations of `cargo test` + `cargo clippy` * Cancel previous run of minimal-ci when pushed again --- .github/composite/godot-install/action.yml | 51 +++++++++++++ .../{godot => godot-itest}/action.yml | 37 +++------- .github/composite/rust/action.yml | 11 +++ .github/workflows/full-ci.yml | 74 ++++++++----------- .github/workflows/minimal-ci.yml | 57 ++++++-------- 5 files changed, 125 insertions(+), 105 deletions(-) create mode 100644 .github/composite/godot-install/action.yml rename .github/composite/{godot => godot-itest}/action.yml (83%) diff --git a/.github/composite/godot-install/action.yml b/.github/composite/godot-install/action.yml new file mode 100644 index 000000000..68998d4bd --- /dev/null +++ b/.github/composite/godot-install/action.yml @@ -0,0 +1,51 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. + +name: godot +description: "Run Godot integration tests" + +inputs: + artifact-name: + required: true + description: "Name of the compiled Godot artifact to download" + + binary-filename: + required: true + description: "Filename of the Godot executable" + + +runs: + using: "composite" + steps: + - uses: actions/checkout@v3 + + # Replaces also backspaces on Windows, since they cause problems in Bash + - name: "Store variable to Godot binary" + run: | + runnerDir=$(echo "${{ runner.temp }}" | sed "s!\\\\!/!") + echo "RUNNER_DIR=$runnerDir" >> $GITHUB_ENV + echo "GODOT4_BIN=$runnerDir/godot_bin/${{ inputs.binary-filename }}" >> $GITHUB_ENV + shell: bash + +# - name: "Check cache for installed Godot version" +# id: "cache-godot" +# uses: actions/cache@v3 +# with: +# path: ${{ runner.temp }}/godot_bin +# key: ${{ inputs.artifact-name }}-v${{ inputs.godot-ver }} +# shell: bash + + - name: "Download Godot artifact" +# if: steps.cache-godot.outputs.cache-hit != 'true' + run: | + curl https://nightly.link/Bromeon/godot4-nightly/workflows/compile-godot/master/${{ inputs.artifact-name }}.zip \ + -Lo artifact.zip \ + --retry 3 + unzip artifact.zip -d $RUNNER_DIR/godot_bin + shell: bash + + - name: "Prepare Godot executable" + run: | + chmod +x $GODOT4_BIN + shell: bash diff --git a/.github/composite/godot/action.yml b/.github/composite/godot-itest/action.yml similarity index 83% rename from .github/composite/godot/action.yml rename to .github/composite/godot-itest/action.yml index b36498a43..1b1466353 100644 --- a/.github/composite/godot/action.yml +++ b/.github/composite/godot-itest/action.yml @@ -24,39 +24,27 @@ inputs: default: '' description: "Extra command line arguments for 'cargo build', e.g. features" + with-llvm: + required: false + description: "Set to 'true' if LLVM should be installed" + default: '' + runs: using: "composite" steps: - uses: actions/checkout@v3 - # Replaces also backspaces on Windows, since they cause problems in Bash - - name: "Store variable to Godot binary" - run: | - runnerDir=$(echo "${{ runner.temp }}" | sed "s!\\\\!/!") - echo "RUNNER_DIR=$runnerDir" >> $GITHUB_ENV - echo "GODOT4_BIN=$runnerDir/godot_bin/${{ inputs.binary-filename }}" >> $GITHUB_ENV - shell: bash - -# - name: "Check cache for installed Godot version" -# id: "cache-godot" -# uses: actions/cache@v3 -# with: -# path: ${{ runner.temp }}/godot_bin -# key: ${{ inputs.artifact-name }}-v${{ inputs.godot-ver }} - - - name: "Download Godot artifact" -# if: steps.cache-godot.outputs.cache-hit != 'true' - run: | - curl https://nightly.link/Bromeon/godot4-nightly/workflows/compile-godot/master/${{ inputs.artifact-name }}.zip -Lo artifact.zip - unzip artifact.zip -d $RUNNER_DIR/godot_bin - shell: bash + - name: "Install Godot" + uses: ./.github/composite/godot-install + with: + artifact-name: ${{ inputs.artifact-name }} + binary-filename: ${{ inputs.binary-filename }} # The chmod seems still necessary, although applied before uploading artifact. Possibly modes are not preserved. # The `| xargs` pattern trims the output, since printed version may contain extra newline, which causes problems in env vars. - name: "Inspect Godot version" run: | - chmod +x $GODOT4_BIN godotVer=$($GODOT4_BIN --version | xargs || true) gitSha=$(echo $godotVer | sed -E "s/.+custom_build\.//") echo "GODOT_BUILT_FROM=_Built from [\`$godotVer\`](https://github.com/godotengine/godot/commit/$gitSha)._" >> $GITHUB_ENV @@ -89,10 +77,7 @@ runs: uses: ./.github/composite/rust with: rust: ${{ inputs.rust-toolchain }} - - - name: "Install LLVM" - uses: ./.github/composite/llvm - # TODO only run it on systems needed + with-llvm: ${{ inputs.with-llvm }} - name: "Build godot-rust" run: | diff --git a/.github/composite/rust/action.yml b/.github/composite/rust/action.yml index df105b25b..1b4e59ea5 100644 --- a/.github/composite/rust/action.yml +++ b/.github/composite/rust/action.yml @@ -10,15 +10,22 @@ inputs: required: false description: "Rust toolchain, e.g. 'stable' or 'nightly'" default: stable + components: required: false description: "Components array" default: '' + cache-key: required: false description: "Extra key to resolve cache" default: '' + with-llvm: + required: false + description: "Set to 'true' if LLVM should be installed" + default: '' + runs: using: "composite" steps: @@ -41,6 +48,10 @@ runs: with: shared-key: ${{ inputs.cache-key }} + - name: "Install LLVM" + uses: ./.github/composite/llvm + if: ${{ inputs.with-llvm == 'true' }} + - name: "Set environment variables used by toolchain" run: | echo CARGO_TERM_COLOR=always >> $GITHUB_ENV diff --git a/.github/workflows/full-ci.yml b/.github/workflows/full-ci.yml index 22ca4e7cc..789a5f3ca 100644 --- a/.github/workflows/full-ci.yml +++ b/.github/workflows/full-ci.yml @@ -15,7 +15,7 @@ on: env: GDEXT_FEATURES: 'godot-core/convenience' - GDEXT_CRATE_ARGS: '-p godot-codegen -p godot-ffi -p godot-core -p godot-macros -p godot' +# GDEXT_CRATE_ARGS: '-p godot-codegen -p godot-ffi -p godot-core -p godot-macros -p godot' defaults: run: @@ -52,35 +52,18 @@ jobs: # TODO get rid of Godot binary, once the JSON is either versioned or fetched from somewhere # Replaces also backspaces on Windows, since they cause problems in Bash - - name: "Store variable to Godot binary" - run: | - runnerDir=$(echo "${{ runner.temp }}" | sed "s!\\\\!/!") - echo "RUNNER_DIR=$runnerDir" >> $GITHUB_ENV - echo "GODOT4_BIN=$runnerDir/godot_bin/godot.linuxbsd.editor.dev.x86_64" >> $GITHUB_ENV - - # - name: "Check cache for installed Godot version" - # id: "cache-godot" - # uses: actions/cache@v3 - # with: - # path: ${{ runner.temp }}/godot_bin - # key: ${{ inputs.artifact-name }}-v${{ inputs.godot-ver }} - - - name: "Download Godot artifact" - # if: steps.cache-godot.outputs.cache-hit != 'true' - run: | - curl https://nightly.link/Bromeon/godot4-nightly/workflows/compile-godot/master/godot-linux.zip -Lo artifact.zip - unzip artifact.zip -d $RUNNER_DIR/godot_bin - - - name: "Prepare Godot executable" - run: | - chmod +x $GODOT4_BIN + - name: "Install Godot" + uses: ./.github/composite/godot-install + with: + artifact-name: godot-linux + binary-filename: godot.linuxbsd.editor.dev.x86_64 - name: "Check clippy" - run: cargo clippy --features $GDEXT_FEATURES $GDEXT_CRATE_ARGS -- --cfg gdext_clippy -D clippy::style -D clippy::complexity -D clippy::perf -D clippy::dbg_macro -D clippy::todo -D clippy::unimplemented + run: cargo clippy -- -D clippy::style -D clippy::complexity -D clippy::perf -D clippy::dbg_macro -D clippy::todo -D clippy::unimplemented unit-test: - name: unit-test (${{ matrix.name }}) + name: unit-test (${{ matrix.name }}${{ matrix.rust-special }}) runs-on: ${{ matrix.os }} continue-on-error: false strategy: @@ -93,21 +76,25 @@ jobs: - name: macos os: macos-11 rust-toolchain: stable + godot-binary: godot.macos.editor.dev.x86_64 - name: windows os: windows-latest rust-toolchain: stable-x86_64-pc-windows-msvc + godot-binary: godot.windows.editor.dev.x86_64.exe # Don't use latest Ubuntu (22.04) as it breaks lots of ecosystem compatibility. # If ever moving to ubuntu-latest, need to manually install libtinfo5 for LLVM. - name: linux os: ubuntu-20.04 rust-toolchain: stable + godot-binary: godot.linuxbsd.editor.dev.x86_64 - - name: linux-minimal-deps + - name: linux os: ubuntu-20.04 rust-toolchain: stable - rust-special: minimal-deps + rust-special: -minimal-deps + godot-binary: godot.linuxbsd.editor.dev.x86_64 steps: - uses: actions/checkout@v3 @@ -117,35 +104,36 @@ jobs: with: rust: stable cache-key: ${{ matrix.rust-special }} # 'minimal-deps' or empty/not defined + with-llvm: ${{ matrix.name == 'macos' }} - name: "Install Rust nightly (minimal deps)" uses: ./.github/composite/rust with: rust: nightly cache-key: minimal-deps-nightly - if: ${{ matrix.rust-special == 'minimal-deps' }} + if: ${{ matrix.rust-special == '-minimal-deps' }} - name: "Install minimal dependency versions from Cargo" run: cargo +nightly update -Z minimal-versions - if: ${{ matrix.rust-special == 'minimal-deps' }} + if: ${{ matrix.rust-special == '-minimal-deps' }} - - name: "Install LLVM" - uses: ./.github/composite/llvm - if: matrix.name == 'macos' + # TODO get rid of Godot binary, once the JSON is either versioned or fetched from somewhere + # Replaces also backspaces on Windows, since they cause problems in Bash + - name: "Install Godot" + uses: ./.github/composite/godot-install + with: + artifact-name: godot-${{ matrix.name }} + binary-filename: ${{ matrix.godot-binary }} - name: "Compile tests" - run: cargo test $GDEXT_CRATE_ARGS --features $GDEXT_FEATURES --no-run - env: - RUSTFLAGS: --cfg=gdext_test + run: cargo test --no-run - name: "Test" - run: cargo test $GDEXT_CRATE_ARGS --features $GDEXT_FEATURES - env: - RUSTFLAGS: --cfg=gdext_test + run: cargo test $GDEXT_FEATURES - itest-godot: - name: itest-godot (${{ matrix.name }}) + godot-itest: + name: godot-itest (${{ matrix.name }}) runs-on: ${{ matrix.os }} continue-on-error: false timeout-minutes: 24 @@ -177,11 +165,11 @@ jobs: - uses: actions/checkout@v3 - name: "Run Godot integration test" - uses: ./.github/composite/godot + uses: ./.github/composite/godot-itest with: artifact-name: godot-${{ matrix.name }} binary-filename: ${{ matrix.godot-binary }} - #godot_ver: ${{ matrix.godot }} + with-llvm: ${{ matrix.name == 'macos' }} license-guard: @@ -206,7 +194,7 @@ jobs: - rustfmt - clippy - unit-test - - itest-godot + - godot-itest - license-guard runs-on: ubuntu-20.04 steps: diff --git a/.github/workflows/minimal-ci.yml b/.github/workflows/minimal-ci.yml index 36234c016..0997e9724 100644 --- a/.github/workflows/minimal-ci.yml +++ b/.github/workflows/minimal-ci.yml @@ -15,16 +15,16 @@ on: env: GDEXT_FEATURES: 'godot-core/convenience' - GDEXT_CRATE_ARGS: '-p godot-codegen -p godot-ffi -p godot-core -p godot-macros -p godot' +# GDEXT_CRATE_ARGS: '-p godot-codegen -p godot-ffi -p godot-core -p godot-macros -p godot' defaults: run: shell: bash # If a new commit is pushed before the old one's CI has completed (on the same branch), abort previous run -#concurrency: -# group: ${{ github.head_ref }} -# cancel-in-progress: true +concurrency: + group: ${{ github.head_ref }} + cancel-in-progress: true jobs: rustfmt: @@ -52,31 +52,14 @@ jobs: # TODO get rid of Godot binary, once the JSON is either versioned or fetched from somewhere # Replaces also backspaces on Windows, since they cause problems in Bash - - name: "Store variable to Godot binary" - run: | - runnerDir=$(echo "${{ runner.temp }}" | sed "s!\\\\!/!") - echo "RUNNER_DIR=$runnerDir" >> $GITHUB_ENV - echo "GODOT4_BIN=$runnerDir/godot_bin/godot.linuxbsd.editor.dev.x86_64" >> $GITHUB_ENV - -# - name: "Check cache for installed Godot version" -# id: "cache-godot" -# uses: actions/cache@v3 -# with: -# path: ${{ runner.temp }}/godot_bin -# key: ${{ inputs.artifact-name }}-v${{ inputs.godot-ver }} - - - name: "Download Godot artifact" -# if: steps.cache-godot.outputs.cache-hit != 'true' - run: | - curl https://nightly.link/Bromeon/godot4-nightly/workflows/compile-godot/master/godot-linux.zip -Lo artifact.zip - unzip artifact.zip -d $RUNNER_DIR/godot_bin - - - name: "Prepare Godot executable" - run: | - chmod +x $GODOT4_BIN + - name: "Install Godot" + uses: ./.github/composite/godot-install + with: + artifact-name: godot-linux + binary-filename: godot.linuxbsd.editor.dev.x86_64 - name: "Check clippy" - run: cargo clippy --features $GDEXT_FEATURES $GDEXT_CRATE_ARGS -- --cfg gdext_clippy -D clippy::style -D clippy::complexity -D clippy::perf -D clippy::dbg_macro -D clippy::todo -D clippy::unimplemented + run: cargo clippy -- -D clippy::style -D clippy::complexity -D clippy::perf -D clippy::dbg_macro -D clippy::todo -D clippy::unimplemented unit-test: @@ -88,26 +71,28 @@ jobs: - name: "Install Rust" uses: ./.github/composite/rust + - name: "Install Godot" + uses: ./.github/composite/godot-install + with: + artifact-name: godot-linux + binary-filename: godot.linuxbsd.editor.dev.x86_64 + - name: "Compile tests" - run: cargo test $GDEXT_CRATE_ARGS --features $GDEXT_FEATURES --no-run - env: - RUSTFLAGS: --cfg=gdext_test + run: cargo test $GDEXT_FEATURES --no-run - name: "Test" - run: cargo test $GDEXT_CRATE_ARGS --features $GDEXT_FEATURES - env: - RUSTFLAGS: --cfg=gdext_test + run: cargo test $GDEXT_FEATURES - itest-godot: - name: itest-godot + godot-itest: + name: godot-itest runs-on: ubuntu-20.04 timeout-minutes: 24 steps: - uses: actions/checkout@v3 - name: "Run Godot integration test" - uses: ./.github/composite/godot + uses: ./.github/composite/godot-itest with: artifact-name: godot-linux binary-filename: godot.linuxbsd.editor.dev.x86_64 From 01b22677bdfafb9c0f5d66edcafbac9621c25eee Mon Sep 17 00:00:00 2001 From: Jan Haller Date: Fri, 27 Jan 2023 20:19:20 +0100 Subject: [PATCH 33/54] Purge the #[cfg] horrors, remove "mock" stubs Get rid of #[cfg(not(any(gdext_test, doctest)))] and similar monstrosities, that previously allowed unit-tests to run without engine/codegen. Remove stubs previously used as an alternative unit-test path: both manually written and generated ones. --- godot-codegen/src/central_generator.rs | 53 +---- godot-codegen/src/lib.rs | 16 +- godot-core/build.rs | 5 +- godot-core/src/builtin/arrays.rs | 6 - godot-core/src/builtin/mod.rs | 1 - godot-core/src/builtin/others.rs | 2 - godot-core/src/builtin/vector2.rs | 1 - godot-core/src/init/mod.rs | 14 +- godot-core/src/lib.rs | 28 --- godot-core/src/test_stubs.rs | 111 ---------- godot-ffi/build.rs | 4 +- godot-ffi/src/gen_central_stub.rs | 222 ------------------- godot-ffi/src/lib.rs | 284 +++++++++---------------- godot/src/lib.rs | 2 - itest/rust/src/lib.rs | 68 ------ 15 files changed, 118 insertions(+), 699 deletions(-) delete mode 100644 godot-core/src/test_stubs.rs delete mode 100644 godot-ffi/src/gen_central_stub.rs diff --git a/godot-codegen/src/central_generator.rs b/godot-codegen/src/central_generator.rs index 6fd7b7649..514a4725d 100644 --- a/godot-codegen/src/central_generator.rs +++ b/godot-codegen/src/central_generator.rs @@ -64,55 +64,22 @@ pub(crate) fn generate_sys_central_file( write_file(sys_gen_path, "central.rs", sys_code, out_files); } -pub(crate) fn generate_sys_mod_file( - core_gen_path: &Path, - out_files: &mut Vec, - stubs_only: bool, -) { - // When invoked by another crate during unit-test (not integration test), don't run generator - let code = if stubs_only { - quote! { - #[path = "../gen_central_stub.rs"] - pub mod central; - pub mod gdextension_interface; - } - } else { - quote! { - pub mod central; - pub mod gdextension_interface; - } +pub(crate) fn generate_sys_mod_file(core_gen_path: &Path, out_files: &mut Vec) { + let code = quote! { + pub mod central; + pub mod gdextension_interface; }; write_file(core_gen_path, "mod.rs", code.to_string(), out_files); } -pub(crate) fn generate_core_mod_file( - core_gen_path: &Path, - out_files: &mut Vec, - stubs_only: bool, -) { +pub(crate) fn generate_core_mod_file(core_gen_path: &Path, out_files: &mut Vec) { // When invoked by another crate during unit-test (not integration test), don't run generator - let code = if stubs_only { - quote! { - pub mod central { - pub mod global {} - } - pub mod classes { - pub struct Node {} - pub struct Resource {} - - pub mod class_macros {} - } - pub mod builtin_classes {} - pub mod utilities {} - } - } else { - quote! { - pub mod central; - pub mod classes; - pub mod builtin_classes; - pub mod utilities; - } + let code = quote! { + pub mod central; + pub mod classes; + pub mod builtin_classes; + pub mod utilities; }; write_file(core_gen_path, "mod.rs", code.to_string(), out_files); diff --git a/godot-codegen/src/lib.rs b/godot-codegen/src/lib.rs index 38b643ef2..e11b035f6 100644 --- a/godot-codegen/src/lib.rs +++ b/godot-codegen/src/lib.rs @@ -34,15 +34,11 @@ use proc_macro2::{Ident, TokenStream}; use quote::{quote, ToTokens}; use std::path::{Path, PathBuf}; -pub fn generate_sys_files(sys_gen_path: &Path, stubs_only: bool) { +pub fn generate_sys_files(sys_gen_path: &Path) { let mut out_files = vec![]; let mut watch = StopWatch::start(); - generate_sys_mod_file(sys_gen_path, &mut out_files, stubs_only); - if stubs_only { - rustfmt_if_needed(out_files); - return; - } + generate_sys_mod_file(sys_gen_path, &mut out_files); let (api, build_config) = load_extension_api(&mut watch); let mut ctx = Context::build_from_api(&api); @@ -56,15 +52,11 @@ pub fn generate_sys_files(sys_gen_path: &Path, stubs_only: bool) { watch.write_stats_to(&sys_gen_path.join("codegen-stats.txt")); } -pub fn generate_core_files(core_gen_path: &Path, stubs_only: bool) { +pub fn generate_core_files(core_gen_path: &Path) { let mut out_files = vec![]; let mut watch = StopWatch::start(); - generate_core_mod_file(core_gen_path, &mut out_files, stubs_only); - if stubs_only { - rustfmt_if_needed(out_files); - return; - } + generate_core_mod_file(core_gen_path, &mut out_files); let (api, build_config) = load_extension_api(&mut watch); let mut ctx = Context::build_from_api(&api); diff --git a/godot-core/build.rs b/godot-core/build.rs index 037c23c39..55854aed8 100644 --- a/godot-core/build.rs +++ b/godot-core/build.rs @@ -13,8 +13,5 @@ fn main() { std::fs::remove_dir_all(gen_path).unwrap_or_else(|e| panic!("failed to delete dir: {e}")); } - // Note: cannot use cfg!(test) because that isn't recognizable from build files. - // See https://github.com/rust-lang/cargo/issues/1581, which was closed without a solution. - let stubs_only = cfg!(gdext_test); - godot_codegen::generate_core_files(gen_path, stubs_only); + godot_codegen::generate_core_files(gen_path); } diff --git a/godot-core/src/builtin/arrays.rs b/godot-core/src/builtin/arrays.rs index 4671f4018..e14c9bf85 100644 --- a/godot-core/src/builtin/arrays.rs +++ b/godot-core/src/builtin/arrays.rs @@ -58,7 +58,6 @@ impl Array { } // This impl relies on `InnerArray` which is not (yet) available in unit tests -#[cfg(not(any(gdext_test, doctest)))] impl Array { /// Constructs an empty `Array`. pub fn new() -> Self { @@ -460,7 +459,6 @@ impl Array { } /// Creates an `Array` from the given Rust array. Each element is converted to a `Variant`. -#[cfg(not(any(gdext_test, doctest)))] impl From<&[T; N]> for Array { fn from(arr: &[T; N]) -> Self { Self::from(&arr[..]) @@ -468,7 +466,6 @@ impl From<&[T; N]> for Array { } /// Creates an `Array` from the given slice. Each element is converted to a `Variant`. -#[cfg(not(any(gdext_test, doctest)))] impl From<&[T]> for Array { fn from(slice: &[T]) -> Self { let mut array = Self::new(); @@ -489,7 +486,6 @@ impl From<&[T]> for Array { } /// Creates an `Array` from an iterator. Each element is converted to a `Variant`. -#[cfg(not(any(gdext_test, doctest)))] impl FromIterator for Array { fn from_iter>(iter: I) -> Self { let mut array = Array::new(); @@ -499,7 +495,6 @@ impl FromIterator for Array { } /// Extends an `Array` with the contents of an iterator. Each element is converted to a `Variant`. -#[cfg(not(any(gdext_test, doctest)))] impl Extend for Array { fn extend>(&mut self, iter: I) { // Unfortunately the GDExtension API does not offer the equivalent of `Vec::reserve`. @@ -519,7 +514,6 @@ pub struct ArrayIterator<'a> { _phantom: PhantomData<&'a Array>, } -#[cfg(not(any(gdext_test, doctest)))] impl<'a> Iterator for ArrayIterator<'a> { type Item = Variant; diff --git a/godot-core/src/builtin/mod.rs b/godot-core/src/builtin/mod.rs index 7961df97c..b1f6aa3d7 100644 --- a/godot-core/src/builtin/mod.rs +++ b/godot-core/src/builtin/mod.rs @@ -69,7 +69,6 @@ pub use vector4i::*; #[doc(hidden)] pub mod inner { - #[cfg(not(gdext_test))] pub use crate::gen::builtin_classes::*; } diff --git a/godot-core/src/builtin/others.rs b/godot-core/src/builtin/others.rs index acfd77387..76e16b215 100644 --- a/godot-core/src/builtin/others.rs +++ b/godot-core/src/builtin/others.rs @@ -33,7 +33,6 @@ struct InnerRect { size: Vector2, } -#[cfg(not(gdext_test))] impl Rect2 { pub fn size(self) -> Vector2 { self.inner().size @@ -61,7 +60,6 @@ impl Callable { } } - #[cfg(not(any(gdext_test, doctest)))] #[doc(hidden)] pub fn as_inner(&self) -> inner::InnerCallable { inner::InnerCallable::from_outer(self) diff --git a/godot-core/src/builtin/vector2.rs b/godot-core/src/builtin/vector2.rs index 334ff2853..b4372be43 100644 --- a/godot-core/src/builtin/vector2.rs +++ b/godot-core/src/builtin/vector2.rs @@ -85,7 +85,6 @@ impl Vector2 { glam::Vec2::new(self.x, self.y) } - #[cfg(not(any(gdext_test, doctest)))] #[doc(hidden)] pub fn as_inner(&self) -> inner::InnerVector2 { inner::InnerVector2::from_outer(self) diff --git a/godot-core/src/init/mod.rs b/godot-core/src/init/mod.rs index de8afeabf..c237df389 100644 --- a/godot-core/src/init/mod.rs +++ b/godot-core/src/init/mod.rs @@ -7,16 +7,6 @@ use godot_ffi as sys; use std::collections::btree_map::BTreeMap; -#[cfg(gdext_test)] -pub fn __gdext_load_library( - interface: *const sys::GDExtensionInterface, - library: sys::GDExtensionClassLibraryPtr, - init: *mut sys::GDExtensionInitialization, -) -> sys::GDExtensionBool { - sys::panic_no_godot!(__gdext_load_library) -} - -#[cfg(not(gdext_test))] #[doc(hidden)] // TODO consider body safe despite unsafe function, and explicitly mark unsafe {} locations pub unsafe fn __gdext_load_library( @@ -24,6 +14,10 @@ pub unsafe fn __gdext_load_library( library: sys::GDExtensionClassLibraryPtr, init: *mut sys::GDExtensionInitialization, ) -> sys::GDExtensionBool { + if cfg!(test) { + panic!("Attempted to call Godot engine from unit-tests; use integration tests for this.") + } + sys::initialize(interface, library); let mut handle = InitHandle::new(); diff --git a/godot-core/src/lib.rs b/godot-core/src/lib.rs index dfe5a92be..dec116478 100644 --- a/godot-core/src/lib.rs +++ b/godot-core/src/lib.rs @@ -4,26 +4,6 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -// If running in tests, a lot of symbols are unused or panic early -#![cfg_attr(gdext_test, allow(unreachable_code, unused))] - -// More test hacks... -// -// Technically, `cargo test -p godot-core` *could* be supported by this abomination: -// #[cfg(not(any(test, doctest, gdext_test))] -// which would be necessary because `cargo test` runs both test/doctest, and downstream crates may need the feature as -// workaround https://github.com/rust-lang/rust/issues/59168#issuecomment-962214945. However, this *also* does not work, -// as #[cfg(doctest)] is currently near-useless for conditional compilation: https://github.com/rust-lang/rust/issues/67295. -// Yet even then, our compile error here is only one of many, as the compiler tries to build doctest without hitting this. -#[cfg(all( - test, // `cargo test` - not(gdext_test), // but forgot `--cfg gdext_test` - not(gdext_clippy) // and is not `cargo clippy --cfg gdext_clippy` (this implicitly enables `test`) -))] -compile_error!("Running `cargo test` requires `--cfg gdext_test`; `cargo clippy` requires `--cfg gdext_clippy`"); - -// ---------------------------------------------------------------------------------------------------------------------------------------------- - mod registry; mod storage; @@ -38,7 +18,6 @@ pub mod obj; pub use godot_ffi as sys; pub use registry::*; -#[cfg(not(any(gdext_test, doctest)))] pub mod engine; // Output of generated code. Mimics the file structure, symbols are re-exported. @@ -48,13 +27,6 @@ pub mod engine; #[allow(clippy::wrong_self_convention)] // TODO remove once to_string is const mod gen; -// For some buggy reason, during doctest, the --cfg flag is not always considered, leading to monstrosities -// such as #[cfg(not(any(gdext_test, doctest)))]. -#[cfg(any(gdext_test, doctest))] -mod test_stubs; -#[cfg(any(gdext_test, doctest))] -pub use test_stubs::*; - #[doc(hidden)] pub mod private { // If someone forgets #[godot_api], this causes a compile error, rather than virtual functions not being called at runtime. diff --git a/godot-core/src/test_stubs.rs b/godot-core/src/test_stubs.rs deleted file mode 100644 index 99830647c..000000000 --- a/godot-core/src/test_stubs.rs +++ /dev/null @@ -1,111 +0,0 @@ -/* - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ - -use super::sys; - -/*pub mod callbacks { - use super::sys; - use crate::obj::{Base, GodotClass}; - - pub unsafe extern "C" fn create( - _class_userdata: *mut std::ffi::c_void, - ) -> sys::GDExtensionObjectPtr { - sys::panic_no_godot!(create) - } - - pub(crate) fn create_custom(_make_user_instance: F) -> sys::GDExtensionObjectPtr - where - T: GodotClass, - F: FnOnce(Base) -> T, - { - sys::panic_no_godot!(create_custom) - } -}*/ - -pub mod engine { - use super::sys; - use crate::obj::{Gd, GodotClass}; - - pub struct Object {} - - pub struct RefCounted {} - - impl RefCounted { - pub fn init_ref(&self) -> bool { - sys::panic_no_godot!(RefCounted::init_ref) - } - pub fn reference(&self) -> bool { - sys::panic_no_godot!(RefCounted::reference) - } - pub fn unreference(&self) -> bool { - sys::panic_no_godot!(RefCounted::unreference) - } - } - - impl GodotClass for Object { - type Base = (); - type Declarer = crate::obj::dom::EngineDomain; - type Mem = crate::obj::mem::DynamicRefCount; - const CLASS_NAME: &'static str = ""; - } - - impl GodotClass for RefCounted { - type Base = Object; - type Declarer = crate::obj::dom::EngineDomain; - type Mem = crate::obj::mem::StaticRefCount; - const CLASS_NAME: &'static str = ""; - } - - pub mod utilities { - use super::sys; - - pub fn is_instance_id_valid(id: i64) -> bool { - sys::panic_no_godot!(is_instance_id_valid) - } - } - - #[allow(non_camel_case_types)] - pub mod global { - use super::sys; - - #[derive(Debug)] - pub enum PropertyHint { - PROPERTY_HINT_NONE, - } - - impl PropertyHint { - pub fn ord(&self) -> i32 { - sys::panic_no_godot!(PropertyHint::ord) - } - } - - #[derive(Debug)] - pub enum PropertyUsageFlags { - PROPERTY_USAGE_DEFAULT, - } - - impl PropertyUsageFlags { - pub fn ord(&self) -> i32 { - sys::panic_no_godot!(PropertyUsageFlags::ord) - } - } - } - - pub(crate) fn debug_string( - ptr: &Gd, - f: &mut std::fmt::Formatter<'_>, - ty: &str, - ) -> std::fmt::Result { - sys::panic_no_godot!(Debug) - } - - pub(crate) fn display_string( - ptr: &Gd, - f: &mut std::fmt::Formatter<'_>, - ) -> std::fmt::Result { - sys::panic_no_godot!(Display) - } -} diff --git a/godot-ffi/build.rs b/godot-ffi/build.rs index 01c36324b..de26a0157 100644 --- a/godot-ffi/build.rs +++ b/godot-ffi/build.rs @@ -16,9 +16,7 @@ fn main() { } run_bindgen(&gen_path.join("gdextension_interface.rs")); - - let stubs_only = cfg!(gdext_test); - godot_codegen::generate_sys_files(gen_path, stubs_only); + godot_codegen::generate_sys_files(gen_path); } fn run_bindgen(out_file: &Path) { diff --git a/godot-ffi/src/gen_central_stub.rs b/godot-ffi/src/gen_central_stub.rs deleted file mode 100644 index 5d0db08c9..000000000 --- a/godot-ffi/src/gen_central_stub.rs +++ /dev/null @@ -1,222 +0,0 @@ -/* - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ - -// Note: this code is only used during unit tests, and may be out of sync with the engine's values. -// The concrete values matter much less than having a structure at all, to avoid thousands of upstream -// conditional compilation differentiations. - -use crate::{ffi_methods, GDExtensionTypePtr, /*GDExtensionVariantPtr,*/ GodotFfi}; -pub mod types { - pub type OpaqueNil = crate::opaque::Opaque<0usize>; - pub type OpaqueBool = crate::opaque::Opaque<1usize>; - pub type OpaqueInt = crate::opaque::Opaque<8usize>; - pub type OpaqueFloat = crate::opaque::Opaque<8usize>; - pub type OpaqueString = crate::opaque::Opaque<8usize>; - pub type OpaqueVector2 = crate::opaque::Opaque<8usize>; - pub type OpaqueVector2i = crate::opaque::Opaque<8usize>; - pub type OpaqueRect2 = crate::opaque::Opaque<16usize>; - pub type OpaqueRect2i = crate::opaque::Opaque<16usize>; - pub type OpaqueVector3 = crate::opaque::Opaque<12usize>; - pub type OpaqueVector3i = crate::opaque::Opaque<12usize>; - pub type OpaqueTransform2D = crate::opaque::Opaque<24usize>; - pub type OpaqueVector4 = crate::opaque::Opaque<16usize>; - pub type OpaqueVector4i = crate::opaque::Opaque<16usize>; - pub type OpaquePlane = crate::opaque::Opaque<16usize>; - pub type OpaqueQuaternion = crate::opaque::Opaque<16usize>; - pub type OpaqueAabb = crate::opaque::Opaque<24usize>; - pub type OpaqueBasis = crate::opaque::Opaque<36usize>; - pub type OpaqueTransform3D = crate::opaque::Opaque<48usize>; - pub type OpaqueProjection = crate::opaque::Opaque<64usize>; - pub type OpaqueColor = crate::opaque::Opaque<16usize>; - pub type OpaqueStringName = crate::opaque::Opaque<8usize>; - pub type OpaqueNodePath = crate::opaque::Opaque<8usize>; - pub type OpaqueRid = crate::opaque::Opaque<8usize>; - pub type OpaqueObject = crate::opaque::Opaque<8usize>; - pub type OpaqueCallable = crate::opaque::Opaque<16usize>; - pub type OpaqueSignal = crate::opaque::Opaque<16usize>; - pub type OpaqueDictionary = crate::opaque::Opaque<8usize>; - pub type OpaqueArray = crate::opaque::Opaque<8usize>; - pub type OpaquePackedByteArray = crate::opaque::Opaque<16usize>; - pub type OpaquePackedInt32Array = crate::opaque::Opaque<16usize>; - pub type OpaquePackedInt64Array = crate::opaque::Opaque<16usize>; - pub type OpaquePackedFloat32Array = crate::opaque::Opaque<16usize>; - pub type OpaquePackedFloat64Array = crate::opaque::Opaque<16usize>; - pub type OpaquePackedStringArray = crate::opaque::Opaque<16usize>; - pub type OpaquePackedVector2Array = crate::opaque::Opaque<16usize>; - pub type OpaquePackedVector3Array = crate::opaque::Opaque<16usize>; - pub type OpaquePackedColorArray = crate::opaque::Opaque<16usize>; - pub type OpaqueVariant = crate::opaque::Opaque<24usize>; -} -// pub struct GlobalMethodTable {} -// impl GlobalMethodTable { -// pub(crate) unsafe fn new(interface: &crate::GDExtensionInterface) -> Self { -// Self {} -// } -// } -#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)] -#[repr(i32)] -pub enum VariantType { - Nil = 0, - Bool = 1, - Int = 2, - Float = 3, - String = 4, - Vector2 = 5, - Vector2i = 6, - Rect2 = 7, - Rect2i = 8, - Vector3 = 9, - Vector3i = 10, - Transform2D = 11, - Vector4 = 12, - Vector4i = 13, - Plane = 14, - Quaternion = 15, - AABB = 16, - Basis = 17, - Transform3D = 18, - Projection = 19, - Color = 20, - StringName = 21, - NodePath = 22, - RID = 23, - Object = 24, - Callable = 25, - Signal = 26, - Dictionary = 27, - Array = 28, - PackedByteArray = 29, - PackedInt32Array = 30, - PackedInt64Array = 31, - PackedFloat32Array = 32, - PackedFloat64Array = 33, - PackedStringArray = 34, - PackedVector2Array = 35, - PackedVector3Array = 36, - PackedColorArray = 37, -} -impl VariantType { - #[doc(hidden)] - pub fn from_sys(enumerator: crate::GDExtensionVariantType) -> Self { - match enumerator { - 0 => Self::Nil, - 1 => Self::Bool, - 2 => Self::Int, - 3 => Self::Float, - 4 => Self::String, - 5 => Self::Vector2, - 6 => Self::Vector2i, - 7 => Self::Rect2, - 8 => Self::Rect2i, - 9 => Self::Vector3, - 10 => Self::Vector3i, - 11 => Self::Transform2D, - 12 => Self::Vector4, - 13 => Self::Vector4i, - 14 => Self::Plane, - 15 => Self::Quaternion, - 16 => Self::AABB, - 17 => Self::Basis, - 18 => Self::Transform3D, - 19 => Self::Projection, - 20 => Self::Color, - 21 => Self::StringName, - 22 => Self::NodePath, - 23 => Self::RID, - 24 => Self::Object, - 25 => Self::Callable, - 26 => Self::Signal, - 27 => Self::Dictionary, - 28 => Self::Array, - 29 => Self::PackedByteArray, - 30 => Self::PackedInt32Array, - 31 => Self::PackedInt64Array, - 32 => Self::PackedFloat32Array, - 33 => Self::PackedFloat64Array, - 34 => Self::PackedStringArray, - 35 => Self::PackedVector2Array, - 36 => Self::PackedVector3Array, - 37 => Self::PackedColorArray, - _ => unreachable!("invalid variant type {}", enumerator), - } - } - #[doc(hidden)] - pub fn sys(self) -> crate::GDExtensionVariantType { - self as _ - } -} -impl GodotFfi for VariantType { - ffi_methods! { type GDExtensionTypePtr = * mut Self ; .. } -} -#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)] -#[repr(i32)] -pub enum VariantOperator { - Equal = 0, - NotEqual = 1, - Less = 2, - LessEqual = 3, - Greater = 4, - GreaterEqual = 5, - Add = 6, - Subtract = 7, - Multiply = 8, - Divide = 9, - Negate = 10, - Positive = 11, - Module = 12, - Power = 13, - ShiftLeft = 14, - ShiftRight = 15, - BitAnd = 16, - BitOr = 17, - BitXor = 18, - BitNegate = 19, - And = 20, - Or = 21, - Xor = 22, - Not = 23, - In = 24, -} -impl VariantOperator { - #[doc(hidden)] - pub fn from_sys(enumerator: crate::GDExtensionVariantOperator) -> Self { - match enumerator { - 0 => Self::Equal, - 1 => Self::NotEqual, - 2 => Self::Less, - 3 => Self::LessEqual, - 4 => Self::Greater, - 5 => Self::GreaterEqual, - 6 => Self::Add, - 7 => Self::Subtract, - 8 => Self::Multiply, - 9 => Self::Divide, - 10 => Self::Negate, - 11 => Self::Positive, - 12 => Self::Module, - 13 => Self::Power, - 14 => Self::ShiftLeft, - 15 => Self::ShiftRight, - 16 => Self::BitAnd, - 17 => Self::BitOr, - 18 => Self::BitXor, - 19 => Self::BitNegate, - 20 => Self::And, - 21 => Self::Or, - 22 => Self::Xor, - 23 => Self::Not, - 24 => Self::In, - _ => unreachable!("invalid variant operator {}", enumerator), - } - } - #[doc(hidden)] - pub fn sys(self) -> crate::GDExtensionVariantOperator { - self as _ - } -} -impl GodotFfi for VariantOperator { - ffi_methods! { type GDExtensionTypePtr = * mut Self ; .. } -} diff --git a/godot-ffi/src/lib.rs b/godot-ffi/src/lib.rs index bf0e3b4ff..28d482570 100644 --- a/godot-ffi/src/lib.rs +++ b/godot-ffi/src/lib.rs @@ -9,8 +9,6 @@ #![cfg_attr(test, allow(unused))] // Output of generated code. Mimics the file structure, symbols are re-exported. -// Note: accessing `gen` *may* still work without explicitly specifying `--cfg gdext_test` flag, -// but stubs are generated for consistency with how godot-core depends on godot-codegen. #[rustfmt::skip] #[allow( non_camel_case_types, @@ -31,214 +29,128 @@ mod plugins; #[doc(hidden)] pub use paste; -pub use crate::godot_ffi::{GodotFfi, GodotFuncMarshal}; - +pub use godot_ffi::{GodotFfi, GodotFuncMarshal}; pub use gen::central::*; -pub use gen::gdextension_interface::*; // needs `crate::` - -#[cfg(not(any(gdext_test, doctest)))] -#[doc(inline)] -pub use real_impl::*; +pub use gen::gdextension_interface::*; -#[cfg(gdext_test)] -#[doc(inline)] -pub use test_impl::*; +use crate::global_registry::GlobalRegistry; // needs `crate::` // ---------------------------------------------------------------------------------------------------------------------------------------------- // Real implementation, when Godot engine is running -#[cfg(not(any(gdext_test, doctest)))] -mod real_impl { - use super::global_registry::GlobalRegistry; - use super::*; - - struct GodotBinding { - interface: GDExtensionInterface, - library: GDExtensionClassLibraryPtr, - method_table: GlobalMethodTable, - registry: GlobalRegistry, - } - - /// Late-init globals - // Note: static mut is _very_ dangerous. Here a bit less so, since modification happens only once (during init) and no - // &mut references are handed out (except for registry, see below). Overall, UnsafeCell/RefCell + Sync might be a safer abstraction. - static mut BINDING: Option = None; - - /// # Safety - /// - /// - The `interface` pointer must be a valid pointer to a [`GDExtensionInterface`] obj. - /// - The `library` pointer must be the pointer given by Godot at initialisation. - /// - This function must not be called from multiple threads. - /// - This function must be called before any use of [`get_library`]. - pub unsafe fn initialize( - interface: *const GDExtensionInterface, - library: GDExtensionClassLibraryPtr, - ) { - let ver = std::ffi::CStr::from_ptr((*interface).version_string); - println!( - "Initialize GDExtension interface: {}", - ver.to_str().unwrap() - ); - - BINDING = Some(GodotBinding { - interface: *interface, - method_table: GlobalMethodTable::new(&*interface), - registry: GlobalRegistry::default(), - library, - }); - } - - /// # Safety - /// - /// The interface must have been initialised with [`initialize`] before calling this function. - #[inline(always)] - pub unsafe fn get_interface() -> &'static GDExtensionInterface { - &unwrap_ref_unchecked(&BINDING).interface - } - - /// # Safety - /// - /// The library must have been initialised with [`initialize`] before calling this function. - #[inline(always)] - pub unsafe fn get_library() -> GDExtensionClassLibraryPtr { - unwrap_ref_unchecked(&BINDING).library - } - - /// # Safety - /// - /// The interface must have been initialised with [`initialize`] before calling this function. - #[inline(always)] - pub unsafe fn method_table() -> &'static GlobalMethodTable { - &unwrap_ref_unchecked(&BINDING).method_table - } - - /// # Safety - /// - /// The interface must have been initialised with [`initialize`] before calling this function. - /// - /// Calling this while another place holds a reference (threads, re-entrancy, iteration, etc) is immediate undefined behavior. - // note: could potentially avoid &mut aliasing, using UnsafeCell/RefCell - #[inline(always)] - pub unsafe fn get_registry() -> &'static mut GlobalRegistry { - &mut unwrap_ref_unchecked_mut(&mut BINDING).registry - } - - /// Combination of `as_ref()` and `unwrap_unchecked()`, but without the case differentiation in - /// the former (thus raw pointer access in release mode) - unsafe fn unwrap_ref_unchecked(opt: &Option) -> &T { - debug_assert!(opt.is_some(), "unchecked access to Option::None"); - match opt { - Some(ref val) => val, - None => std::hint::unreachable_unchecked(), - } - } - - unsafe fn unwrap_ref_unchecked_mut(opt: &mut Option) -> &mut T { - debug_assert!(opt.is_some(), "unchecked access to Option::None"); - match opt { - Some(ref mut val) => val, - None => std::hint::unreachable_unchecked(), - } - } - - #[doc(hidden)] - pub fn default_call_error() -> GDExtensionCallError { - GDExtensionCallError { - error: GDEXTENSION_CALL_OK, - argument: -1, - expected: -1, - } - } +struct GodotBinding { + interface: GDExtensionInterface, + library: GDExtensionClassLibraryPtr, + method_table: GlobalMethodTable, + registry: GlobalRegistry, +} - #[macro_export] - #[doc(hidden)] - macro_rules! builtin_fn { - ($name:ident $(@1)?) => { - $crate::method_table().$name - }; - } +/// Late-init globals +// Note: static mut is _very_ dangerous. Here a bit less so, since modification happens only once (during init) and no +// &mut references are handed out (except for registry, see below). Overall, UnsafeCell/RefCell + Sync might be a safer abstraction. +static mut BINDING: Option = None; - #[macro_export] - #[doc(hidden)] - macro_rules! builtin_call { - ($name:ident ( $($args:expr),* $(,)? )) => { - ($crate::method_table().$name)( $($args),* ) - }; - } +/// # Safety +/// +/// - The `interface` pointer must be a valid pointer to a [`GDExtensionInterface`] obj. +/// - The `library` pointer must be the pointer given by Godot at initialisation. +/// - This function must not be called from multiple threads. +/// - This function must be called before any use of [`get_library`]. +pub unsafe fn initialize( + interface: *const GDExtensionInterface, + library: GDExtensionClassLibraryPtr, +) { + let ver = std::ffi::CStr::from_ptr((*interface).version_string); + println!( + "Initialize GDExtension interface: {}", + ver.to_str().unwrap() + ); + + BINDING = Some(GodotBinding { + interface: *interface, + method_table: GlobalMethodTable::new(&*interface), + registry: GlobalRegistry::default(), + library, + }); } -// ---------------------------------------------------------------------------------------------------------------------------------------------- -// Stubs when in unit-test (without Godot) - -#[cfg(gdext_test)] -mod test_impl { - use super::gen::gdextension_interface::*; - use super::global_registry::GlobalRegistry; +/// # Safety +/// +/// The interface must have been initialised with [`initialize`] before calling this function. +#[inline(always)] +pub unsafe fn get_interface() -> &'static GDExtensionInterface { + &unwrap_ref_unchecked(&BINDING).interface +} - pub struct GlobalMethodTable {} +/// # Safety +/// +/// The library must have been initialised with [`initialize`] before calling this function. +#[inline(always)] +pub unsafe fn get_library() -> GDExtensionClassLibraryPtr { + unwrap_ref_unchecked(&BINDING).library +} - #[inline(always)] - pub unsafe fn get_interface() -> &'static GDExtensionInterface { - crate::panic_no_godot!(get_interface) - } +/// # Safety +/// +/// The interface must have been initialised with [`initialize`] before calling this function. +#[inline(always)] +pub unsafe fn method_table() -> &'static GlobalMethodTable { + &unwrap_ref_unchecked(&BINDING).method_table +} - #[inline(always)] - pub unsafe fn get_library() -> GDExtensionClassLibraryPtr { - crate::panic_no_godot!(get_library) - } +/// # Safety +/// +/// The interface must have been initialised with [`initialize`] before calling this function. +/// +/// Calling this while another place holds a reference (threads, re-entrancy, iteration, etc) is immediate undefined behavior. +// note: could potentially avoid &mut aliasing, using UnsafeCell/RefCell +#[inline(always)] +pub unsafe fn get_registry() -> &'static mut GlobalRegistry { + &mut unwrap_ref_unchecked_mut(&mut BINDING).registry +} - #[inline(always)] - pub unsafe fn method_table() -> &'static GlobalMethodTable { - crate::panic_no_godot!(method_table) +/// Combination of `as_ref()` and `unwrap_unchecked()`, but without the case differentiation in +/// the former (thus raw pointer access in release mode) +unsafe fn unwrap_ref_unchecked(opt: &Option) -> &T { + debug_assert!(opt.is_some(), "unchecked access to Option::None"); + match opt { + Some(ref val) => val, + None => std::hint::unreachable_unchecked(), } +} - #[inline(always)] - pub unsafe fn get_registry() -> &'static mut GlobalRegistry { - crate::panic_no_godot!(get_registry) +unsafe fn unwrap_ref_unchecked_mut(opt: &mut Option) -> &mut T { + debug_assert!(opt.is_some(), "unchecked access to Option::None"); + match opt { + Some(ref mut val) => val, + None => std::hint::unreachable_unchecked(), } +} - #[macro_export] - #[doc(hidden)] - macro_rules! builtin_fn { - // Don't use ! because of warnings - ($name:ident) => {{ - #[allow(unreachable_code)] - fn panic2(t: T, u: U) -> () { - panic!("builtin_fn! unavailable in unit tests; needs Godot engine"); - () - } - panic2 - }}; - ($name:ident @1) => {{ - #[allow(unreachable_code)] - fn panic1(t: T) -> () { - panic!("builtin_fn! unavailable in unit tests; needs Godot engine"); - () - } - panic1 - }}; +#[doc(hidden)] +pub fn default_call_error() -> GDExtensionCallError { + GDExtensionCallError { + error: GDEXTENSION_CALL_OK, + argument: -1, + expected: -1, } +} - // Possibly interesting: https://stackoverflow.com/a/40234666 - #[macro_export] - #[doc(hidden)] - macro_rules! panic_no_godot { - ($symbol:expr) => { - panic!(concat!( - stringify!($symbol), - " unavailable in unit tests; needs Godot engine" - )) - }; - } +#[macro_export] +#[doc(hidden)] +macro_rules! builtin_fn { + ($name:ident $(@1)?) => { + $crate::method_table().$name + }; +} - #[macro_export] - #[doc(hidden)] - macro_rules! builtin_call { +#[macro_export] +#[doc(hidden)] +macro_rules! builtin_call { ($name:ident ( $($args:expr),* $(,)? )) => { - $crate::panic_no_godot!(builtin_call) + ($crate::method_table().$name)( $($args),* ) }; } -} // ---------------------------------------------------------------------------------------------------------------------------------------------- diff --git a/godot/src/lib.rs b/godot/src/lib.rs index 4eae6b9ef..194f9aebd 100644 --- a/godot/src/lib.rs +++ b/godot/src/lib.rs @@ -36,7 +36,6 @@ pub use godot_core::private; pub mod prelude { pub use super::bind::{godot_api, GodotClass, GodotExt}; pub use super::builtin::*; - #[cfg(not(any(gdext_test, doctest)))] pub use super::engine::{ load, try_load, utilities, AudioStreamPlayer, Camera2D, Camera3D, Input, Node, Node2D, Node3D, Object, PackedScene, RefCounted, Resource, SceneTree, @@ -46,7 +45,6 @@ pub mod prelude { pub use super::obj::{Base, Gd, GdMut, GdRef, GodotClass, Inherits, InstanceId, Share}; // Make trait methods available - #[cfg(not(any(gdext_test, doctest)))] pub use super::engine::NodeExt as _; pub use super::obj::EngineEnum as _; } diff --git a/itest/rust/src/lib.rs b/itest/rust/src/lib.rs index 71a5a0026..fc3b50f7a 100644 --- a/itest/rust/src/lib.rs +++ b/itest/rust/src/lib.rs @@ -4,9 +4,6 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -#[cfg(all(test, not(gdext_clippy)))] -compile_error!("`cargo test` not supported for integration test -- use `cargo run`."); - use godot::bind::{godot_api, GodotClass}; use godot::init::{gdextension, ExtensionLibrary}; use godot::test::itest; @@ -71,71 +68,6 @@ impl IntegrationTests { #[gdextension(entry_point=itest_init)] unsafe impl ExtensionLibrary for IntegrationTests {} -#[doc(hidden)] -#[macro_export] -macro_rules! godot_test_impl { - ( $( $test_name:ident $body:block $($attrs:tt)* )* ) => { - $( - $($attrs)* - #[doc(hidden)] - #[inline] - #[must_use] - pub fn $test_name() -> bool { - let str_name = stringify!($test_name); - println!(" -- {}", str_name); - - let ok = ::std::panic::catch_unwind( - || $body - ).is_ok(); - - if !ok { - godot::log::godot_error!(" !! Test {} failed", str_name); - } - - ok - } - )* - } -} - -/// Declares a test to be run with the Godot engine (i.e. not a pure Rust unit test). -/// -/// Creates a wrapper function that catches panics, prints errors and returns true/false. -/// To be manually invoked in higher-level test routine. -/// -/// This macro is designed to be used within the current crate only, hence the #[cfg] attribute. -#[doc(hidden)] -#[allow(unused_macros)] -macro_rules! godot_test { - ($($test_name:ident $body:block)*) => { - $( - godot_test_impl!($test_name $body #[cfg(feature = "gd-test")]); - )* - } -} - -/// Declares a test to be run with the Godot engine (i.e. not a pure Rust unit test). -/// -/// Creates a wrapper function that catches panics, prints errors and returns true/false. -/// To be manually invoked in higher-level test routine. -/// -/// This macro is designed to be used within the `test` crate, hence the method is always declared (not only in certain features). -#[doc(hidden)] -#[macro_export] -macro_rules! godot_itest { - ($($test_name:ident $body:block)*) => { - $( - $crate::godot_test_impl!($test_name $body); - )* - }; - // Convenience - ($(fn $test_name:ident () $body:block)*) => { - $( - $crate::godot_test_impl!($test_name $body); - )* - }; -} - pub(crate) fn expect_panic(context: &str, code: impl FnOnce() + UnwindSafe) { let panic = std::panic::catch_unwind(code); assert!( From 37449b5ec516805d7f8113d237691f0b6953d9c0 Mon Sep 17 00:00:00 2001 From: Jan Haller Date: Sat, 28 Jan 2023 15:17:27 +0100 Subject: [PATCH 34/54] Add check.sh script for local integration; add *.sh to license checks --- .github/external-config/licenserc.yml | 3 +- check.sh | 118 ++++++++++++++++++++++++++ 2 files changed, 120 insertions(+), 1 deletion(-) create mode 100755 check.sh diff --git a/.github/external-config/licenserc.yml b/.github/external-config/licenserc.yml index 54e50d5b2..0f71ed67e 100644 --- a/.github/external-config/licenserc.yml +++ b/.github/external-config/licenserc.yml @@ -11,6 +11,7 @@ header: paths: - '**/*.rs' - '**/*.gd' + - '**/*.sh' - '.github/**/*.yml' paths-ignore: @@ -20,7 +21,7 @@ header: language: GDScript: - extensions: ['.gd'] + extensions: ['.gd', '.sh'] comment_style_id: 'Hashtag' Rust: extensions: ['.rs'] diff --git a/check.sh b/check.sh new file mode 100755 index 000000000..69cbdb309 --- /dev/null +++ b/check.sh @@ -0,0 +1,118 @@ +#!/bin/bash +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. + +# 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. + +# No args specified: do everything +if [ "$#" -eq 0 ]; then + args=("fmt" "clippy" "test" "itest") +else + args=("$@") +fi + +# --help menu +for arg in "${args[@]}"; do + if [ "$arg" == "--help" ]; then + echo "Usage: check.sh []" + echo "" + echo "Each specified command will be run (until one fails)." + echo "If no commands are specified, all checks are run (no doc; may take several minutes)." + echo "" + echo "Commands:" + echo " fmt format code, fail if bad" + echo " clippy validate clippy lints" + echo " test run unit tests (no Godot)" + echo " itest run integration tests (Godot)" + echo " doc generate docs for 'godot' crate" + echo " dok generate docs and open in browser" + echo "" + echo "Examples:" + echo " check.sh fmt clippy" + echo " check.sh" + exit 0 + fi +done + +# For integration tests +function findGodot() { + # User-defined GODOT4_BIN + if [ -n "$GODOT4_BIN" ]; then + echo "Found GODOT4_BIN env var ($GODOT4_BIN)" + godotBin="$GODOT4_BIN" + + # Executable in path + elif command -v godot4 &>/dev/null; then + echo "Found 'godot4' executable" + godotBin="godot4" + + # Special case for Windows when there is a .bat file + # Also consider that 'cmd /c' would need 'cmd //c' (https://stackoverflow.com/q/21357813) + elif + godot4.bat --version + [[ $? -eq 0 ]] + then + echo "Found 'godot4.bat' script" + godotBin="godot4.bat" + + # Error case + else + echo "Godot executable not found" + exit 2 + fi +} + +features="--features godot-core/convenience" +cmds=() + +for arg in "${args[@]}"; do + case "$arg" in + fmt) + cmds+=("cargo fmt --all -- --check") + ;; + clippy) + cmds+=("cargo clippy $features -- -D clippy::style -D clippy::complexity -D clippy::perf -D clippy::dbg_macro -D clippy::todo -D clippy::unimplemented -D warnings") + ;; + test) + cmds+=("cargo test $features") + ;; + itest) + findGodot + + cmds+=("cargo build -p itest") + cmds+=("$godotBin --path itest/godot --headless") + ;; + doc) + cmds+=("cargo doc --lib -p godot --no-deps $features") + ;; + dok) + cmds+=("cargo doc --lib -p godot --no-deps $features --open") + ;; + *) + echo "Unrecognized command '$arg'" + exit 2 + ;; + esac +done + +RED='\033[1;31m' +GREEN='\033[1;36m' +END='\033[0m' +for cmd in "${cmds[@]}"; do + echo "> $cmd" + $cmd || { + printf "$RED\n==========================" + printf "\ngodot-rust checker FAILED." + printf "\n==========================\n$END" + exit 1 + } +done + +printf "$GREEN\n==============================" +printf "\ngodot-rust checker SUCCESSFUL." +printf "\n==============================\n$END" From 197624691effa1754bdf6804475e91d1ce3f4242 Mon Sep 17 00:00:00 2001 From: Jan Haller Date: Sat, 28 Jan 2023 16:01:58 +0100 Subject: [PATCH 35/54] Implement GodotString/StringName conversions via generic AsRef --- godot-core/src/builtin/string.rs | 23 ++++++++--------------- godot-core/src/builtin/string_name.rs | 18 ++++++++++++++---- 2 files changed, 22 insertions(+), 19 deletions(-) diff --git a/godot-core/src/builtin/string.rs b/godot-core/src/builtin/string.rs index 7eef112ac..013446ab0 100644 --- a/godot-core/src/builtin/string.rs +++ b/godot-core/src/builtin/string.rs @@ -79,21 +79,12 @@ impl_builtin_traits! { } } -impl From<&String> for GodotString { - fn from(s: &String) -> GodotString { - GodotString::from(s.as_str()) - } -} - -impl From for GodotString { - fn from(s: String) -> GodotString { - GodotString::from(s.as_str()) - } -} - -impl From<&str> for GodotString { - fn from(s: &str) -> Self { - let bytes = s.as_bytes(); +impl From for GodotString +where + S: AsRef, +{ + fn from(s: S) -> Self { + let bytes = s.as_ref().as_bytes(); unsafe { Self::from_string_sys_init(|string_ptr| { @@ -125,6 +116,8 @@ impl From<&GodotString> for String { } } +// TODO From<&NodePath> + test + impl FromStr for GodotString { type Err = Infallible; diff --git a/godot-core/src/builtin/string_name.rs b/godot-core/src/builtin/string_name.rs index eb4dda1da..ac5df05d0 100644 --- a/godot-core/src/builtin/string_name.rs +++ b/godot-core/src/builtin/string_name.rs @@ -69,7 +69,7 @@ impl Default for StringName { let self_ptr = (*uninit.as_mut_ptr()).sys_mut(); sys::builtin_call! { string_name_construct_default(self_ptr, std::ptr::null_mut()) - }; + } uninit.assume_init() } @@ -115,9 +115,12 @@ impl From<&GodotString> for StringName { } } -impl From<&str> for StringName { - fn from(s: &str) -> Self { - let intermediate = GodotString::from(s); +impl From for StringName +where + S: AsRef, +{ + fn from(s: S) -> Self { + let intermediate = GodotString::from(s.as_ref()); Self::from(&intermediate) } } @@ -133,3 +136,10 @@ impl From<&StringName> for GodotString { } } } + +impl From<&StringName> for String { + fn from(s: &StringName) -> Self { + let intermediate = GodotString::from(s); + Self::from(&intermediate) + } +} From acc2463dfc1ab75979ad13d115f2a7f3ca99f3b8 Mon Sep 17 00:00:00 2001 From: Jan Haller Date: Sat, 28 Jan 2023 16:02:37 +0100 Subject: [PATCH 36/54] Update Dodge-the-Creeps to new animation API --- examples/dodge-the-creeps/rust/src/mob.rs | 11 +++++------ examples/dodge-the-creeps/rust/src/player.rs | 2 +- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/examples/dodge-the-creeps/rust/src/mob.rs b/examples/dodge-the-creeps/rust/src/mob.rs index f3655971c..45374dffc 100644 --- a/examples/dodge-the-creeps/rust/src/mob.rs +++ b/examples/dodge-the-creeps/rust/src/mob.rs @@ -10,11 +10,11 @@ enum MobType { } impl MobType { - fn to_str(self) -> String { + fn to_str(self) -> GodotString { match self { - MobType::Walk => "walk".to_string(), - MobType::Swim => "swim".to_string(), - MobType::Fly => "fly".to_string(), + MobType::Walk => "walk".into(), + MobType::Swim => "swim".into(), + MobType::Fly => "fly".into(), } } } @@ -63,7 +63,6 @@ impl GodotExt for Mob { let mut sprite = self .base .get_node_as::("AnimatedSprite2D"); - sprite.set_animation(animation_name.as_str().into()); - sprite.set_playing(true); + sprite.set_animation(StringName::from(&animation_name)); } } diff --git a/examples/dodge-the-creeps/rust/src/player.rs b/examples/dodge-the-creeps/rust/src/player.rs index f24265129..7e665c443 100644 --- a/examples/dodge-the-creeps/rust/src/player.rs +++ b/examples/dodge-the-creeps/rust/src/player.rs @@ -95,7 +95,7 @@ impl GodotExt for Player { animated_sprite.set_flip_v(velocity.y > 0.0) } - animated_sprite.play(animation.into(), false); + animated_sprite.play(animation.into(), 1.0, false); } else { animated_sprite.stop(); } From 290afe3d070245bb8e9796ad26c1c718fc30ea14 Mon Sep 17 00:00:00 2001 From: Jan Haller Date: Sat, 28 Jan 2023 15:24:48 +0100 Subject: [PATCH 37/54] More clippy fixes, now that all crates are covered --- examples/dodge-the-creeps/rust/src/player.rs | 4 ++-- godot-core/src/macros.rs | 1 + godot-ffi/src/lib.rs | 2 +- itest/rust/build.rs | 3 ++- itest/rust/src/builtin_test.rs | 4 ++-- itest/rust/src/enum_test.rs | 10 +++++----- itest/rust/src/lib.rs | 3 +-- itest/rust/src/object_test.rs | 4 ++-- itest/rust/src/variant_test.rs | 6 ++---- 9 files changed, 18 insertions(+), 19 deletions(-) diff --git a/examples/dodge-the-creeps/rust/src/player.rs b/examples/dodge-the-creeps/rust/src/player.rs index 7e665c443..1dade77f7 100644 --- a/examples/dodge-the-creeps/rust/src/player.rs +++ b/examples/dodge-the-creeps/rust/src/player.rs @@ -103,8 +103,8 @@ impl GodotExt for Player { let change = velocity * delta as f32; let position = self.base.get_global_position() + change; let position = Vector2::new( - position.x.max(0.0).min(self.screen_size.x), - position.y.max(0.0).min(self.screen_size.y), + position.x.clamp(0.0, self.screen_size.x), + position.y.clamp(0.0, self.screen_size.y), ); self.base.set_global_position(position); } diff --git a/godot-core/src/macros.rs b/godot-core/src/macros.rs index f66930727..d277cd10a 100644 --- a/godot-core/src/macros.rs +++ b/godot-core/src/macros.rs @@ -414,6 +414,7 @@ macro_rules! gdext_ptrcall { <$($RetTy)+ as sys::GodotFfi>::write_sys(&ret_val, $ret); // FIXME should be inc_ref instead of forget + #[allow(clippy::forget_copy)] std::mem::forget(ret_val); }; } diff --git a/godot-ffi/src/lib.rs b/godot-ffi/src/lib.rs index 28d482570..e6ac2f5c8 100644 --- a/godot-ffi/src/lib.rs +++ b/godot-ffi/src/lib.rs @@ -29,7 +29,7 @@ mod plugins; #[doc(hidden)] pub use paste; -pub use godot_ffi::{GodotFfi, GodotFuncMarshal}; +pub use crate::godot_ffi::{GodotFfi, GodotFuncMarshal}; pub use gen::central::*; pub use gen::gdextension_interface::*; diff --git a/itest/rust/build.rs b/itest/rust/build.rs index 933ebbccc..aab8378f7 100644 --- a/itest/rust/build.rs +++ b/itest/rust/build.rs @@ -66,6 +66,7 @@ fn main() { #[class(init)] struct GenFfi {} + #[allow(clippy::bool_comparison)] // i == true #[godot::bind::godot_api] impl GenFfi { #(#methods)* @@ -111,7 +112,7 @@ fn rustfmt_if_needed(out_files: Vec) { Ok(_) => println!("Done."), Err(err) => { println!("Failed."); - println!("Error: {}", err); + println!("Error: {err}"); } } } diff --git a/itest/rust/src/builtin_test.rs b/itest/rust/src/builtin_test.rs index 1b9943f5b..7138bd5d6 100644 --- a/itest/rust/src/builtin_test.rs +++ b/itest/rust/src/builtin_test.rs @@ -28,7 +28,7 @@ fn test_builtins_vector2() { assert_eq!(abs, Vector2::new(3.0, 4.0)); let normalized = inner.is_normalized(); - assert_eq!(normalized, false); + assert!(!normalized); } #[itest] @@ -54,7 +54,7 @@ fn test_builtins_callable() { let cb = Callable::from_object_method(obj.share(), "set_position"); let inner: InnerCallable = cb.as_inner(); - assert_eq!(inner.is_null(), false); + assert!(!inner.is_null()); assert_eq!(inner.get_object_id(), obj.instance_id().to_i64()); assert_eq!(inner.get_method(), StringName::from("set_position")); diff --git a/itest/rust/src/enum_test.rs b/itest/rust/src/enum_test.rs index 4a9c98f56..faa8d908e 100644 --- a/itest/rust/src/enum_test.rs +++ b/itest/rust/src/enum_test.rs @@ -6,7 +6,7 @@ use crate::itest; use godot::engine::input::CursorShape; -use godot::engine::{file_access, time}; +use godot::engine::time; use std::collections::HashSet; pub fn run() -> bool { @@ -43,10 +43,10 @@ fn enum_ords_correct() { fn enum_equality() { // TODO: find 2 overlapping ords in same enum - assert_eq!( - file_access::CompressionMode::COMPRESSION_DEFLATE, - file_access::CompressionMode::COMPRESSION_DEFLATE - ); + // assert_eq!( + // file_access::CompressionMode::COMPRESSION_DEFLATE, + // file_access::CompressionMode::COMPRESSION_DEFLATE + // ); } #[itest] diff --git a/itest/rust/src/lib.rs b/itest/rust/src/lib.rs index fc3b50f7a..027873b3d 100644 --- a/itest/rust/src/lib.rs +++ b/itest/rust/src/lib.rs @@ -72,7 +72,6 @@ pub(crate) fn expect_panic(context: &str, code: impl FnOnce() + UnwindSafe) { let panic = std::panic::catch_unwind(code); assert!( panic.is_err(), - "code should have panicked but did not: {}", - context + "code should have panicked but did not: {context}", ); } diff --git a/itest/rust/src/object_test.rs b/itest/rust/src/object_test.rs index 0e2a10e63..148926fff 100644 --- a/itest/rust/src/object_test.rs +++ b/itest/rust/src/object_test.rs @@ -243,12 +243,12 @@ fn object_engine_up_deref_mut() { // DerefMut chain: Gd -> &mut Node3D -> &mut Node -> &mut Object node3d.set_message_translation(true); - assert_eq!(node3d.can_translate_messages(), true); + assert!(node3d.can_translate_messages()); // DerefMut chain: &mut Node3D -> ... let node3d_ref = &mut *node3d; node3d_ref.set_message_translation(false); - assert_eq!(node3d_ref.can_translate_messages(), false); + assert!(!node3d_ref.can_translate_messages()); node3d.free(); } diff --git a/itest/rust/src/variant_test.rs b/itest/rust/src/variant_test.rs index 6666e090e..75700aad3 100644 --- a/itest/rust/src/variant_test.rs +++ b/itest/rust/src/variant_test.rs @@ -316,15 +316,13 @@ where for rel in true_rels { assert!( rel, - "total_order(rel=true, lhs={:?}, rhs={:?}, exp={:?})", - lhs, rhs, expected_order + "total_order(rel=true, lhs={lhs:?}, rhs={rhs:?}, exp={expected_order:?})", ); } for rel in false_rels { assert!( !rel, - "total_order(rel=false, lhs={:?}, rhs={:?}, exp={:?})", - lhs, rhs, expected_order + "total_order(rel=false, lhs={lhs:?}, rhs={rhs:?}, exp={expected_order:?})", ); } } From 7eae6b676b34ef5f247e79fcb99cacd5ec1d92c8 Mon Sep 17 00:00:00 2001 From: Lili Zoey Date: Thu, 26 Jan 2023 20:27:59 +0100 Subject: [PATCH 38/54] Add basic dictionary impl --- godot-core/src/builtin/arrays.rs | 1 + godot-core/src/builtin/dictionary.rs | 355 +++++++++++++++++++++ godot-core/src/builtin/mod.rs | 4 + godot-core/src/builtin/others.rs | 1 - godot-core/src/builtin/string.rs | 20 ++ godot-core/src/builtin/variant/impls.rs | 1 + itest/rust/src/dictionary_test.rs | 402 ++++++++++++++++++++++++ itest/rust/src/lib.rs | 2 + itest/rust/src/variant_test.rs | 8 +- 9 files changed, 792 insertions(+), 2 deletions(-) create mode 100644 godot-core/src/builtin/dictionary.rs create mode 100644 itest/rust/src/dictionary_test.rs diff --git a/godot-core/src/builtin/arrays.rs b/godot-core/src/builtin/arrays.rs index 4671f4018..44fca959f 100644 --- a/godot-core/src/builtin/arrays.rs +++ b/godot-core/src/builtin/arrays.rs @@ -565,6 +565,7 @@ impl_builtin_traits! { for Array { Default => array_construct_default; Drop => array_destroy; + PartialEq => array_operator_equal; } } diff --git a/godot-core/src/builtin/dictionary.rs b/godot-core/src/builtin/dictionary.rs new file mode 100644 index 000000000..ddc05fba7 --- /dev/null +++ b/godot-core/src/builtin/dictionary.rs @@ -0,0 +1,355 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +use godot_ffi as sys; + +use crate::builtin::{inner, FromVariant, ToVariant, Variant, VariantConversionError}; +use crate::obj::Share; +use std::collections::{HashMap, HashSet}; +use std::fmt; +use sys::types::*; +use sys::{ffi_methods, interface_fn, GodotFfi}; + +use super::Array; + +/// Godot's `Dictionary` type. +/// +/// The keys and values of the array are all `Variant`, so they can all be of different types. +/// Variants are designed to be generally cheap to clone. +/// +/// # Thread safety +/// +/// The same principles apply as for [`Array`]. Consult its documentation for details. +#[repr(C)] +pub struct Dictionary { + opaque: OpaqueDictionary, +} + +impl Dictionary { + fn from_opaque(opaque: OpaqueDictionary) -> Self { + Self { opaque } + } + + /// Constructs an empty `Dictionary`. + pub fn new() -> Self { + Self::default() + } + + /// Removes all key-value pairs from the dictionary. Equivalent to `clear` in Godot. + pub fn clear(&mut self) { + self.as_inner().clear() + } + + /// Returns a deep copy of the dictionary. All nested arrays and dictionaries are duplicated and + /// will not be shared with the original dictionary. Note that any `Object`-derived elements will + /// still be shallow copied. + /// + /// To create a shallow copy, use [`duplicate_shallow()`] instead. To create a new reference to + /// the same array data, use [`share()`]. + /// + /// Equivalent to `dictionary.duplicate(true)` in Godot. + pub fn duplicate_deep(&self) -> Self { + self.as_inner().duplicate(true) + } + + /// Returns a shallow copy of the dictionary. All dictionary keys and values are copied, but + /// any reference types (such as `Array`, `Dictionary` and `Object`) will still refer to the + /// same value. + /// + /// To create a deep copy, use [`duplicate_deep()`] instead. To create a new reference to the + /// same dictionary data, use [`share()`]. + /// + /// Equivalent to `dictionary.duplicate(false)` in Godot. + pub fn duplicate_shallow(&self) -> Self { + self.as_inner().duplicate(false) + } + + /// Removes a key from the map, and returns the value associated with + /// the key if the key was in the dictionary. + pub fn remove(&mut self, key: K) -> Option { + let key = key.to_variant(); + let old_value = self.get(key.clone()); + self.as_inner().erase(key); + old_value + } + + /// Returns the first key whose associated value is `value`, if one exists. + /// + /// Unlike in Godot, this will return `None` if the key does not exist + /// and `Some(nil)` the key is `null`. + pub fn find_key_by_value(&self, value: V) -> Option { + let key = self.as_inner().find_key(value.to_variant()); + + if !key.is_nil() || self.contains_key(key.clone()) { + Some(key) + } else { + None + } + } + + /// Returns the value at the key in the dictionary, if there is + /// one. + /// + /// Unlike `get` in Godot, this will return `None` if there is + /// no value with the given key. + pub fn get(&self, key: K) -> Option { + let key = key.to_variant(); + if !self.contains_key(key.clone()) { + return None; + } + + Some(self.get_or_nil(key)) + } + + /// Returns the value at the key in the dictionary, or nil otherwise. This + /// method does not let you differentiate `NIL` values stored as values from + /// absent keys. If you need that, use `get()`. + /// + /// This is equivalent to `get` in Godot. + pub fn get_or_nil(&self, key: K) -> Variant { + self.as_inner().get(key.to_variant(), Variant::nil()) + } + + /// Returns `true` if the dictionary contains the given key. + /// + /// This is equivalent to `has` in Godot. + pub fn contains_key(&self, key: K) -> bool { + let key = key.to_variant(); + self.as_inner().has(key) + } + + /// Returns `true` if the dictionary contains all the given keys. + /// + /// This is equivalent to `has_all` in Godot. + pub fn contains_all_keys(&self, keys: Array) -> bool { + self.as_inner().has_all(keys) + } + + /// Returns a 32-bit integer hash value representing the dictionary and its contents. + pub fn hash(&self) -> u32 { + self.as_inner().hash().try_into().unwrap() + } + + /// Creates a new `Array` containing all the keys currently in the dictionary. + pub fn keys(&self) -> Array { + self.as_inner().keys() + } + + /// Creates a new `Array` containing all the values currently in the dictionary. + pub fn values(&self) -> Array { + self.as_inner().values() + } + + /// Returns true if the dictionary is empty. + pub fn is_empty(&self) -> bool { + self.as_inner().is_empty() + } + + /// Copies all keys and values from other into self. + /// + /// If overwrite is true, it will overwrite pre-existing keys. Otherwise + /// it will not. + /// + /// This is equivalent to `merge` in Godot. + pub fn extend_dictionary(&mut self, other: Self, overwrite: bool) { + self.as_inner().merge(other, overwrite) + } + + /// Returns the number of entries in the dictionary. + /// + /// This is equivalent to `size` in Godot. + pub fn len(&self) -> usize { + self.as_inner().size().try_into().unwrap() + } + + /// Get the pointer corresponding to the given key in the dictionary, + /// if there exists no value at the given key then a new one is created + /// and initialized to a nil variant. + fn get_ptr_mut(&mut self, key: K) -> *mut Variant { + let key = key.to_variant(); + unsafe { + let ptr = + (interface_fn!(dictionary_operator_index))(self.sys_mut(), key.var_sys_const()); + assert!(!ptr.is_null()); + ptr as *mut Variant + } + } + + /// Insert a value at the given key, returning the value + /// that previously was at that key if there was one. + pub fn insert(&mut self, key: K, value: V) -> Option { + let key = key.to_variant(); + let old_value = self.get(key.clone()); + self.set(key, value); + old_value + } + + /// Set a key to a given value. + pub fn set(&mut self, key: K, value: V) { + let key = key.to_variant(); + unsafe { + *self.get_ptr_mut(key) = value.to_variant(); + } + } + + #[doc(hidden)] + pub fn as_inner(&self) -> inner::InnerDictionary { + inner::InnerDictionary::from_outer(self) + } +} + +/// Creates a Dictionary from the given iterator I over a (&K, &V) key-value pair. +/// Each key and value are converted to a Variant. +impl<'a, 'b, K, V, I> From for Dictionary +where + I: IntoIterator, + K: ToVariant + 'a, + V: ToVariant + 'b, +{ + fn from(iterable: I) -> Self { + iterable + .into_iter() + .map(|(key, value)| (key.to_variant(), value.to_variant())) + .collect() + } +} + +/// Convert this dictionary to a strongly typed rust `HashMap`. If the conversion +/// fails for any key or value, an error is returned. +/// +/// Will be replaced by a proper iteration implementation. +impl TryFrom<&Dictionary> for HashMap { + type Error = VariantConversionError; + + fn try_from(dictionary: &Dictionary) -> Result { + // TODO: try to panic or something if modified while iterating + // Though probably better to fix when implementing iteration proper + dictionary + .keys() + .iter_shared() + .zip(dictionary.values().iter_shared()) + .map(|(key, value)| Ok((K::try_from_variant(&key)?, V::try_from_variant(&value)?))) + .collect() + } +} + +/// Convert the keys of this dictionary to a strongly typed rust `HashSet`. If the +/// conversion fails for any key, an error is returned. +impl TryFrom<&Dictionary> for HashSet { + type Error = VariantConversionError; + + fn try_from(dictionary: &Dictionary) -> Result { + // TODO: try to panic or something if modified while iterating + // Though probably better to fix when implementing iteration proper + dictionary + .keys() + .iter_shared() + .map(|key| K::try_from_variant(&key)) + .collect() + } +} + +/// Inserts all key-values from the iterator into the dictionary, +/// replacing values with existing keys with new values returned +/// from the iterator. +impl Extend<(K, V)> for Dictionary { + fn extend>(&mut self, iter: I) { + for (k, v) in iter.into_iter() { + self.set(k.to_variant(), v.to_variant()) + } + } +} + +impl FromIterator<(K, V)> for Dictionary { + fn from_iter>(iter: I) -> Self { + let mut dict = Dictionary::new(); + dict.extend(iter); + dict + } +} + +impl_builtin_traits! { + for Dictionary { + Default => dictionary_construct_default; + Drop => dictionary_destroy; + PartialEq => dictionary_operator_equal; + } +} + +impl GodotFfi for Dictionary { + ffi_methods! { + type sys::GDExtensionTypePtr = *mut Opaque; + fn from_sys; + fn sys; + fn write_sys; + } + + unsafe fn from_sys_init(init_fn: impl FnOnce(sys::GDExtensionTypePtr)) -> Self { + // Can't use uninitialized pointer -- Dictionary CoW implementation in C++ expects that on + // assignment, the target CoW pointer is either initialized or nullptr + + let mut result = Self::default(); + init_fn(result.sys_mut()); + result + } +} + +impl fmt::Debug for Dictionary { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{:?}", self.to_variant().stringify()) + } +} + +/// Creates a new reference to the data in this dictionary. Changes to the original dictionary will be +/// reflected in the copy and vice versa. +/// +/// To create a (mostly) independent copy instead, see [`Dictionary::duplicate_shallow()`] and +/// [`Dictionary::duplicate_deep()`]. +impl Share for Dictionary { + fn share(&self) -> Self { + unsafe { + Self::from_sys_init(|self_ptr| { + let ctor = sys::builtin_fn!(dictionary_construct_copy); + let args = [self.sys_const()]; + ctor(self_ptr, args.as_ptr()); + }) + } + } +} + +/// Creates a new dictionary with the given keys and values, the syntax mirrors +/// Godot's dictionary creation syntax. +/// +/// Any value can be used as a key, but to use an expression you need to surround it +/// in `()` or `{}`. +/// +/// Example +/// ```no_run +/// use godot::builtin::dict; +/// +/// let key = "my_key"; +/// let d = dict! { +/// "key1": 10, +/// "another": Variant::nil(), +/// key: true, +/// (1 + 2): "final", +/// } +/// ``` +#[macro_export] +macro_rules! dict { + ($($key:tt: $value:expr),* $(,)?) => { + { + let mut d = $crate::builtin::Dictionary::new(); + $( + // `cargo check` complains that `(1 + 2): true` has unused parens, even though it's not + // possible to omit the parens. + #[allow(unused_parens)] + d.set($key, $value); + )* + d + } + }; +} diff --git a/godot-core/src/builtin/mod.rs b/godot-core/src/builtin/mod.rs index 7961df97c..9bca43184 100644 --- a/godot-core/src/builtin/mod.rs +++ b/godot-core/src/builtin/mod.rs @@ -37,6 +37,7 @@ mod vector_macros; mod arrays; mod color; +mod dictionary; mod node_path; mod others; mod packed_array; @@ -52,8 +53,11 @@ mod vector4i; pub mod meta; +pub use crate::dict; + pub use arrays::*; pub use color::*; +pub use dictionary::*; pub use node_path::*; pub use others::*; pub use packed_array::*; diff --git a/godot-core/src/builtin/others.rs b/godot-core/src/builtin/others.rs index acfd77387..f7ededc05 100644 --- a/godot-core/src/builtin/others.rs +++ b/godot-core/src/builtin/others.rs @@ -25,7 +25,6 @@ impl_builtin_stub!(Projection, OpaqueProjection); impl_builtin_stub!(Rid, OpaqueRid); impl_builtin_stub!(Callable, OpaqueCallable); impl_builtin_stub!(Signal, OpaqueSignal); -impl_builtin_stub!(Dictionary, OpaqueDictionary); #[repr(C)] struct InnerRect { diff --git a/godot-core/src/builtin/string.rs b/godot-core/src/builtin/string.rs index 7eef112ac..78005dd23 100644 --- a/godot-core/src/builtin/string.rs +++ b/godot-core/src/builtin/string.rs @@ -10,6 +10,8 @@ use godot_ffi as sys; use sys::types::OpaqueString; use sys::{ffi_methods, interface_fn, GodotFfi}; +use super::{FromVariant, ToVariant, Variant, VariantConversionError}; + #[repr(C, align(8))] pub struct GodotString { opaque: OpaqueString, @@ -147,6 +149,24 @@ impl fmt::Debug for GodotString { } } +impl ToVariant for &str { + fn to_variant(&self) -> Variant { + GodotString::from(*self).to_variant() + } +} + +impl ToVariant for String { + fn to_variant(&self) -> Variant { + GodotString::from(self).to_variant() + } +} + +impl FromVariant for String { + fn try_from_variant(variant: &Variant) -> Result { + Ok(GodotString::try_from_variant(variant)?.to_string()) + } +} + // While this is a nice optimisation for ptrcalls, it's not easily possible // to pass in &GodotString when doing varcalls. /* diff --git a/godot-core/src/builtin/variant/impls.rs b/godot-core/src/builtin/variant/impls.rs index 1bb29a83f..b16fc892b 100644 --- a/godot-core/src/builtin/variant/impls.rs +++ b/godot-core/src/builtin/variant/impls.rs @@ -133,6 +133,7 @@ mod impls { use super::*; impl_variant_traits!(bool, bool_to_variant, bool_from_variant, Bool); + impl_variant_traits!(Dictionary, dictionary_to_variant, dictionary_from_variant, Dictionary); impl_variant_traits!(Vector2, vector2_to_variant, vector2_from_variant, Vector2); impl_variant_traits!(Vector3, vector3_to_variant, vector3_from_variant, Vector3); impl_variant_traits!(Vector4, vector4_to_variant, vector4_from_variant, Vector4); diff --git a/itest/rust/src/dictionary_test.rs b/itest/rust/src/dictionary_test.rs new file mode 100644 index 000000000..2ea9d85a5 --- /dev/null +++ b/itest/rust/src/dictionary_test.rs @@ -0,0 +1,402 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +use std::collections::{HashMap, HashSet}; + +use crate::itest; +use godot::{ + builtin::{dict, Dictionary, FromVariant, ToVariant}, + prelude::{Share, Variant}, +}; + +pub fn run() -> bool { + let mut ok = true; + ok &= dictionary_default(); + ok &= dictionary_new(); + ok &= dictionary_from_iterator(); + ok &= dictionary_from(); + ok &= dictionary_macro(); + ok &= dictionary_try_to_hashmap(); + ok &= dictionary_try_to_hashset(); + ok &= dictionary_clone(); + ok &= dictionary_duplicate_deep(); + ok &= dictionary_hash(); + ok &= dictionary_get(); + ok &= dictionary_insert(); + ok &= dictionary_insert_multiple(); + ok &= dictionary_insert_long(); + ok &= dictionary_extend(); + ok &= dictionary_remove(); + ok &= dictionary_clear(); + ok &= dictionary_find_key(); + ok &= dictionary_contains_keys(); + ok &= dictionary_keys_values(); + ok &= dictionary_equal(); + ok +} + +#[itest] +fn dictionary_default() { + assert_eq!(Dictionary::default().len(), 0); +} + +#[itest] +fn dictionary_new() { + assert_eq!(Dictionary::new().len(), 0); +} + +#[itest] +fn dictionary_from_iterator() { + let dictionary = Dictionary::from_iter([("foo", 1), ("bar", 2)]); + + assert_eq!(dictionary.len(), 2); + assert_eq!(dictionary.get("foo"), Some(1.to_variant()), "key = \"foo\""); + assert_eq!(dictionary.get("bar"), Some(2.to_variant()), "key = \"bar\""); + + let dictionary = Dictionary::from_iter([(1, "foo"), (2, "bar")]); + + assert_eq!(dictionary.len(), 2); + assert_eq!(dictionary.get(1), Some("foo".to_variant()), "key = 1"); + assert_eq!(dictionary.get(2), Some("bar".to_variant()), "key = 2"); +} + +#[itest] +fn dictionary_from() { + let dictionary = Dictionary::from(&HashMap::from([("foo", 1), ("bar", 2)])); + + assert_eq!(dictionary.len(), 2); + assert_eq!(dictionary.get("foo"), Some(1.to_variant()), "key = \"foo\""); + assert_eq!(dictionary.get("bar"), Some(2.to_variant()), "key = \"bar\""); + + let dictionary = Dictionary::from(&HashMap::from([(1, "foo"), (2, "bar")])); + + assert_eq!(dictionary.len(), 2); + assert_eq!(dictionary.get(1), Some("foo".to_variant()), "key = \"foo\""); + assert_eq!(dictionary.get(2), Some("bar".to_variant()), "key = \"bar\""); +} + +#[itest] +fn dictionary_macro() { + let dictionary = dict! { + "foo": 0, + "bar": true, + "baz": "foobar" + }; + + assert_eq!(dictionary.len(), 3); + assert_eq!(dictionary.get("foo"), Some(0.to_variant()), "key = \"foo\""); + assert_eq!( + dictionary.get("bar"), + Some(true.to_variant()), + "key = \"bar\"" + ); + assert_eq!( + dictionary.get("baz"), + Some("foobar".to_variant()), + "key = \"baz\"" + ); + + let empty = dict!(); + assert!(empty.is_empty()); + + let foo = "foo"; + let dict_complex = dict! { + foo: 10, + "bar": true, + (1 + 2): Variant::nil(), + }; + assert_eq!(dict_complex.get("foo"), Some(10.to_variant())); + assert_eq!(dict_complex.get("bar"), Some(true.to_variant())); + assert_eq!(dict_complex.get(3), Some(Variant::nil())); +} + +#[itest] +fn dictionary_try_to_hashmap() { + let dictionary = dict! { + "foo": 0, + "bar": 1, + "baz": 2 + }; + + assert_eq!( + HashMap::::try_from(&dictionary), + Ok(HashMap::from([ + ("foo".into(), 0), + ("bar".into(), 1), + ("baz".into(), 2) + ])) + ); +} + +#[itest] +fn dictionary_try_to_hashset() { + let dictionary = dict! { + "foo": true, + "bar": true, + "baz": true + }; + + assert_eq!( + HashSet::::try_from(&dictionary), + Ok(HashSet::from(["foo".into(), "bar".into(), "baz".into()])) + ); +} + +#[itest] +fn dictionary_clone() { + let subdictionary = dict! { + "baz": true, + "foobar": false + }; + let dictionary = dict! { + "foo": 0, + "bar": subdictionary.share() + }; + #[allow(clippy::redundant_clone)] + let clone = dictionary.share(); + Dictionary::try_from_variant(&clone.get("bar").unwrap()) + .unwrap() + .insert("final", 4); + assert_eq!(subdictionary.get("final"), Some(4.to_variant())); +} + +#[itest] +fn dictionary_hash() { + let dictionary = dict! { + "foo": 0, + "bar": true, + "baz": "foobar" + }; + dictionary.hash(); +} + +#[itest] +fn dictionary_duplicate_deep() { + let subdictionary = dict! { + "baz": true, + "foobar": false + }; + let dictionary = dict! { + "foo": 0, + "bar": subdictionary.share() + }; + let clone = dictionary.duplicate_deep(); + Dictionary::try_from_variant(&clone.get("bar").unwrap()) + .unwrap() + .insert("baz", 4); + assert_eq!( + subdictionary.get("baz"), + Some(true.to_variant()), + "key = \"baz\"" + ); +} + +#[itest] +fn dictionary_duplicate_shallow() { + let subdictionary = dict! { + "baz": true, + "foobar": false + }; + let dictionary = dict! { + "foo": 0, + "bar": subdictionary.share() + }; + let mut clone = dictionary.duplicate_shallow(); + Dictionary::try_from_variant(&clone.get("bar").unwrap()) + .unwrap() + .insert("baz", 4); + assert_eq!( + subdictionary.get("baz"), + Some(4.to_variant()), + "key = \"baz\"" + ); + clone.insert("foo", false.to_variant()); + assert_eq!(dictionary.get("foo"), Some(0.to_variant())); + assert_eq!(clone.get("foo"), Some(false.to_variant())); +} + +#[itest] +fn dictionary_get() { + let mut dictionary = dict! { + "foo": 0, + "bar": true, + "baz": "foobar", + "nil": Variant::nil(), + }; + + dictionary.insert("baz", "foobar"); + + assert_eq!(dictionary.get("foo"), Some(0.to_variant()), "key = \"foo\""); + assert_eq!( + dictionary.get("bar"), + Some(true.to_variant()), + "key = \"bar\"" + ); + assert_eq!(dictionary.get("baz"), Some("foobar".to_variant())); + assert_eq!(dictionary.get("nil"), Some(Variant::nil()), "key = \"nil\""); + assert_eq!(dictionary.get("missing"), None, "key = \"missing\""); + assert_eq!( + dictionary.get_or_nil("nil"), + Variant::nil(), + "key = \"nil\"" + ); + assert_eq!( + dictionary.get_or_nil("missing"), + Variant::nil(), + "key = \"missing\"" + ); + assert_eq!(dictionary.get("foobar"), None, "key = \"foobar\""); +} + +#[itest] +fn dictionary_insert() { + let mut dictionary = dict! { + "foo": 0, + "bar": 1, + }; + + assert_eq!(dictionary.insert("bar", 2), Some(1.to_variant())); + assert_eq!( + HashMap::::try_from(&dictionary), + Ok(HashMap::from([("foo".into(), 0), ("bar".into(), 2)])) + ); + assert_eq!(dictionary.insert("baz", 3), None); + assert_eq!( + HashMap::::try_from(&dictionary), + Ok(HashMap::from([ + ("foo".into(), 0), + ("bar".into(), 2), + ("baz".into(), 3) + ])) + ); +} + +#[itest] +fn dictionary_insert_multiple() { + let mut dictionary = dict! {}; + assert!(dictionary.is_empty()); + + dictionary.insert(1, true); + assert_eq!(dictionary.get(1), Some(true.to_variant())); + + let mut other = dict! {}; + assert!(other.is_empty()); + + other.insert(1, 2); + assert_eq!(other.get(1), Some(2.to_variant())); +} +#[itest] +fn dictionary_insert_long() { + let mut dictionary = dict! {}; + let old = dictionary.insert("abcdefghijklmnopqrstuvwxyz", "zabcdefghijklmnopqrstuvwxy"); + assert_eq!(old, None); + assert_eq!( + dictionary.get("abcdefghijklmnopqrstuvwxyz"), + Some("zabcdefghijklmnopqrstuvwxy".to_variant()) + ); +} + +#[itest] +fn dictionary_extend() { + let mut dictionary = dict! { + "foo": 0, + "bar": true, + }; + assert_eq!(dictionary.get("foo"), Some(0.to_variant())); + let other = dict! { + "bar": "new", + "baz": Variant::nil(), + }; + dictionary.extend_dictionary(other, false); + assert_eq!(dictionary.get("bar"), Some(true.to_variant())); + assert_eq!(dictionary.get("baz"), Some(Variant::nil())); + + let mut dictionary = dict! { + "bar": true, + }; + let other = dict! { + "bar": "new", + }; + dictionary.extend_dictionary(other, true); + assert_eq!(dictionary.get("bar"), Some("new".to_variant())); +} + +#[itest] +fn dictionary_remove() { + let mut dictionary = dict! { + "foo": 0, + }; + assert_eq!(dictionary.remove("foo"), Some(0.to_variant())); + assert!(!dictionary.contains_key("foo")); + assert!(dictionary.is_empty()); +} + +#[itest] +fn dictionary_clear() { + let mut dictionary = dict! { + "foo": 0, + "bar": true, + "baz": "foobar" + }; + + assert!(!dictionary.is_empty()); + dictionary.clear(); + assert!(dictionary.is_empty()); +} + +#[itest] +fn dictionary_find_key() { + let dictionary = dict! { + "foo": 0, + "bar": true, + }; + + assert_eq!(dictionary.find_key_by_value(0), Some("foo".to_variant())); + assert_eq!(dictionary.find_key_by_value(true), Some("bar".to_variant())); +} + +#[itest] +fn dictionary_contains_keys() { + use godot::prelude::Array; + let dictionary = dict! { + "foo": 0, + "bar": true, + }; + + assert!(dictionary.contains_key("foo"), "key = \"foo\""); + assert!(dictionary.contains_key("bar"), "key = \"bar\""); + assert!( + dictionary.contains_all_keys(Array::from(&["foo", "bar"])), + "keys = [\"foo\", \"bar\"]" + ); + assert!(!dictionary.contains_key("missing"), "key = \"missing\""); + assert!( + !dictionary.contains_all_keys(Array::from(&["foo", "bar", "missing"])), + "keys = [\"foo\", \"bar\", \"missing\"]" + ); +} + +#[itest] +fn dictionary_keys_values() { + use godot::prelude::Array; + let dictionary = dict! { + "foo": 0, + "bar": true, + }; + + assert_eq!(dictionary.keys(), Array::from(&["foo", "bar"])); + assert_eq!( + dictionary.values(), + Array::from(&[0.to_variant(), true.to_variant()]) + ); +} + +#[itest] +fn dictionary_equal() { + assert_eq!(dict! {"foo": "bar"}, dict! {"foo": "bar"}); + assert_eq!(dict! {1: f32::NAN}, dict! {1: f32::NAN}); // yes apparently godot considers these equal + assert_ne!(dict! {"foo": "bar"}, dict! {"bar": "foo"}); +} diff --git a/itest/rust/src/lib.rs b/itest/rust/src/lib.rs index 71a5a0026..5e4bda9f4 100644 --- a/itest/rust/src/lib.rs +++ b/itest/rust/src/lib.rs @@ -15,6 +15,7 @@ use std::panic::UnwindSafe; mod array_test; mod base_test; mod builtin_test; +mod dictionary_test; mod enum_test; mod export_test; mod gdscript_ffi_test; @@ -40,6 +41,7 @@ fn run_tests() -> bool { ok &= string_test::run(); ok &= array_test::run(); ok &= packed_array_test::run(); + ok &= dictionary_test::run(); ok &= utilities_test::run(); ok &= variant_test::run(); ok &= virtual_methods_test::run(); diff --git a/itest/rust/src/variant_test.rs b/itest/rust/src/variant_test.rs index 6666e090e..4554389ed 100644 --- a/itest/rust/src/variant_test.rs +++ b/itest/rust/src/variant_test.rs @@ -39,7 +39,6 @@ fn variant_nil() { fn variant_conversions() { roundtrip(false); roundtrip(true); - roundtrip(gstr("some string")); roundtrip(InstanceId::from_nonzero(-9223372036854775808i64)); // roundtrip(Some(InstanceId::from_nonzero(9223372036854775807i64))); // roundtrip(Option::::None); @@ -60,6 +59,13 @@ fn variant_conversions() { roundtrip(2147483647i32); roundtrip(-2147483648i32); roundtrip(9223372036854775807i64); + + // string + roundtrip(gstr("some string")); + roundtrip(String::from("some other string")); + let str_val = "abcdefghijklmnop"; + let back = String::from_variant(&str_val.to_variant()); + assert_eq!(str_val, back.as_str()); } #[itest] From 2e3f8e972ad19b5b6302e85b8d6c2b68c7a77b06 Mon Sep 17 00:00:00 2001 From: mhoff Date: Sat, 21 Jan 2023 00:25:08 -0800 Subject: [PATCH 39/54] Make any Object Variants with a nullptr inside instead be of type Nil. --- godot-core/src/builtin/variant/mod.rs | 31 ++++++++++++++++++++++++--- itest/rust/src/variant_test.rs | 27 +++++++++++++++++++++++ 2 files changed, 55 insertions(+), 3 deletions(-) diff --git a/godot-core/src/builtin/variant/mod.rs b/godot-core/src/builtin/variant/mod.rs index 9dac6f28d..5e249632b 100644 --- a/godot-core/src/builtin/variant/mod.rs +++ b/godot-core/src/builtin/variant/mod.rs @@ -60,13 +60,38 @@ impl Variant { /// Checks whether the variant is empty (`null` value in GDScript). pub fn is_nil(&self) -> bool { - self.sys_type() == sys::GDEXTENSION_VARIANT_TYPE_NIL + // Use get_type rather than sys_type to include checking for the case where the variant is an object but with a null pointer. + self.get_type() == VariantType::Nil } // TODO test + /// Converts the sys_type to the VariantType. + /// Also handles the case where a variant's type is Object but it has a null pointer, instead returning Nil. pub fn get_type(&self) -> VariantType { - let ty_sys = unsafe { interface_fn!(variant_get_type)(self.var_sys()) }; - VariantType::from_sys(ty_sys) + let sys_type = self.sys_type(); + + // There is a special case when the Variant is an Object, but the Object* is null. + let is_an_object = sys_type == sys::GDEXTENSION_VARIANT_TYPE_OBJECT; + let is_a_null_object = if is_an_object { + // Only call object_from_variant if the Variant is of type Object. + unsafe { + // object_from_variant expects sys::GDExtensionTypePtr, which is essentially an Object**. + // The type that sys::GDExtensionTypePtr isn't the actual rust type it would dereference to, but instead GDExtensionObjectPtr. + let mut object_ptr: sys::GDExtensionObjectPtr = std::ptr::null_mut(); + let object_ptr_ptr = std::ptr::addr_of_mut!(object_ptr) as sys::GDExtensionTypePtr; + let converter = sys::builtin_fn!(object_from_variant); + converter(object_ptr_ptr, self.var_sys()); + object_ptr.is_null() + } + } else { + false + }; + + if is_a_null_object { + VariantType::Nil + } else { + VariantType::from_sys(sys_type) + } } // TODO test diff --git a/itest/rust/src/variant_test.rs b/itest/rust/src/variant_test.rs index debd8760b..82b7bff15 100644 --- a/itest/rust/src/variant_test.rs +++ b/itest/rust/src/variant_test.rs @@ -23,6 +23,7 @@ pub fn run() -> bool { ok &= variant_evaluate_total_order(); ok &= variant_sys_conversion(); ok &= variant_sys_conversion2(); + ok &= variant_object_nullptr(); ok } @@ -197,6 +198,32 @@ fn variant_sys_conversion() { assert_eq!(v2, v); } +#[itest] +fn variant_object_nullptr() { + // Some Variants that are of type Object actually contain a null pointer. + // The should report their type as Nil instead, so that they don't create invalid Gd objects. + let variant; + unsafe { + // Create a variant from a null object pointer. + variant = Variant::from_var_sys_init(|self_ptr| { + let null_pointer: godot::sys::GDExtensionObjectPtr = std::ptr::null_mut(); + let converter = godot::sys::builtin_fn!(object_to_variant); + converter( + self_ptr, + std::ptr::addr_of!(null_pointer) as godot::sys::GDExtensionTypePtr, + ); + }); + + // Get the variant's internal type and ensure that it is actually still an Object. + let sys_type: godot::sys::GDExtensionVariantType = + godot::sys::interface_fn!(variant_get_type)(variant.var_sys()); + assert_eq!(sys_type, godot::sys::GDEXTENSION_VARIANT_TYPE_OBJECT); + } + + // The actual test of the public API, ensure that the variant's type is Nil even though the internal type is Object. + assert_eq!(variant.get_type(), VariantType::Nil); +} + #[itest] fn variant_sys_conversion2() { /* From f34500e0508cad406d222d4733958b8b20b874e5 Mon Sep 17 00:00:00 2001 From: Jan Haller Date: Sun, 29 Jan 2023 12:27:04 +0100 Subject: [PATCH 40/54] Integration test `variant_null_object_is_nil` now uses uses Godot API --- godot-core/src/builtin/node_path.rs | 1 + godot-core/src/builtin/variant/impls.rs | 17 +++++++++- godot-core/src/builtin/variant/mod.rs | 27 ++++++++-------- itest/rust/src/variant_test.rs | 43 ++++++++++++------------- 4 files changed, 51 insertions(+), 37 deletions(-) diff --git a/godot-core/src/builtin/node_path.rs b/godot-core/src/builtin/node_path.rs index 6dcbc6fc7..5a38f3a3d 100644 --- a/godot-core/src/builtin/node_path.rs +++ b/godot-core/src/builtin/node_path.rs @@ -62,6 +62,7 @@ impl Display for NodePath { impl_builtin_traits! { for NodePath { + Default => node_path_construct_default; Clone => node_path_construct_copy; Drop => node_path_destroy; } diff --git a/godot-core/src/builtin/variant/impls.rs b/godot-core/src/builtin/variant/impls.rs index b16fc892b..a464b77f5 100644 --- a/godot-core/src/builtin/variant/impls.rs +++ b/godot-core/src/builtin/variant/impls.rs @@ -133,7 +133,6 @@ mod impls { use super::*; impl_variant_traits!(bool, bool_to_variant, bool_from_variant, Bool); - impl_variant_traits!(Dictionary, dictionary_to_variant, dictionary_from_variant, Dictionary); impl_variant_traits!(Vector2, vector2_to_variant, vector2_from_variant, Vector2); impl_variant_traits!(Vector3, vector3_to_variant, vector3_from_variant, Vector3); impl_variant_traits!(Vector4, vector4_to_variant, vector4_from_variant, Vector4); @@ -142,6 +141,21 @@ mod impls { impl_variant_traits!(Color, color_to_variant, color_from_variant, Color); impl_variant_traits!(GodotString, string_to_variant, string_from_variant, String); impl_variant_traits!(StringName, string_name_to_variant, string_name_from_variant, StringName); + impl_variant_traits!(NodePath, node_path_to_variant, node_path_from_variant, NodePath); + /* TODO provide those, as soon as `Default` is available. Also consider auto-generating. + impl_variant_traits!(Rect2, rect2_to_variant, rect2_from_variant, Rect2); + impl_variant_traits!(Rect2i, rect2i_to_variant, rect2i_from_variant, Rect2i); + impl_variant_traits!(Plane, plane_to_variant, plane_from_variant, Plane); + impl_variant_traits!(Quaternion, quaternion_to_variant, quaternion_from_variant, Quaternion); + impl_variant_traits!(Aabb, aabb_to_variant, aabb_from_variant, AABB); + impl_variant_traits!(Basis, basis_to_variant, basis_from_variant, Basis); + impl_variant_traits!(Transform2D, transform_2d_to_variant, transform_2d_from_variant, Transform2D); + impl_variant_traits!(Transform3D, transform_3d_to_variant, transform_3d_from_variant, Transform3D); + impl_variant_traits!(Projection, projection_to_variant, projection_from_variant, Projection); + impl_variant_traits!(Rid, rid_to_variant, rid_from_variant, RID); + impl_variant_traits!(Callable, callable_to_variant, callable_from_variant, Callable); + impl_variant_traits!(Signal, signal_to_variant, signal_from_variant, Signal); + */ impl_variant_traits!(Array, array_to_variant, array_from_variant, Array); impl_variant_traits!(PackedByteArray, packed_byte_array_to_variant, packed_byte_array_from_variant, PackedByteArray); impl_variant_traits!(PackedInt32Array, packed_int32_array_to_variant, packed_int32_array_from_variant, PackedInt32Array); @@ -152,6 +166,7 @@ mod impls { impl_variant_traits!(PackedVector2Array, packed_vector2_array_to_variant, packed_vector2_array_from_variant, PackedVector2Array); impl_variant_traits!(PackedVector3Array, packed_vector3_array_to_variant, packed_vector3_array_from_variant, PackedVector3Array); impl_variant_traits!(PackedColorArray, packed_color_array_to_variant, packed_color_array_from_variant, PackedColorArray); + impl_variant_traits!(Dictionary, dictionary_to_variant, dictionary_from_variant, Dictionary); impl_variant_traits!(i64, int_to_variant, int_from_variant, Int, GDEXTENSION_METHOD_ARGUMENT_METADATA_INT_IS_INT64); impl_variant_traits_int!(i8, GDEXTENSION_METHOD_ARGUMENT_METADATA_INT_IS_INT8); diff --git a/godot-core/src/builtin/variant/mod.rs b/godot-core/src/builtin/variant/mod.rs index 5e249632b..20122cdcf 100644 --- a/godot-core/src/builtin/variant/mod.rs +++ b/godot-core/src/builtin/variant/mod.rs @@ -59,35 +59,36 @@ impl Variant { } /// Checks whether the variant is empty (`null` value in GDScript). + /// + /// See also [`Self::get_type`]. pub fn is_nil(&self) -> bool { - // Use get_type rather than sys_type to include checking for the case where the variant is an object but with a null pointer. + // Use get_type() rather than sys_type(), to also cover nullptr OBJECT as NIL self.get_type() == VariantType::Nil } - // TODO test - /// Converts the sys_type to the VariantType. - /// Also handles the case where a variant's type is Object but it has a null pointer, instead returning Nil. + /// Returns the type that is currently held by this variant. + /// + /// If this variant holds a type `Object` but no instance (represented as a null object pointer), then `Nil` will be returned for + /// consistency. This may deviate from Godot behavior -- for example, calling `Node::get_node_or_null()` with an invalid + /// path returns a variant that has type `Object` but acts like `Nil` for all practical purposes. pub fn get_type(&self) -> VariantType { let sys_type = self.sys_type(); // There is a special case when the Variant is an Object, but the Object* is null. - let is_an_object = sys_type == sys::GDEXTENSION_VARIANT_TYPE_OBJECT; - let is_a_null_object = if is_an_object { - // Only call object_from_variant if the Variant is of type Object. + let is_null_object = if sys_type == sys::GDEXTENSION_VARIANT_TYPE_OBJECT { + // object_from_variant expects sys::GDExtensionTypePtr, which is essentially an Object**. + let mut object_ptr: sys::GDExtensionObjectPtr = ptr::null_mut(); + let object_ptr_ptr = ptr::addr_of_mut!(object_ptr) as sys::GDExtensionTypePtr; unsafe { - // object_from_variant expects sys::GDExtensionTypePtr, which is essentially an Object**. - // The type that sys::GDExtensionTypePtr isn't the actual rust type it would dereference to, but instead GDExtensionObjectPtr. - let mut object_ptr: sys::GDExtensionObjectPtr = std::ptr::null_mut(); - let object_ptr_ptr = std::ptr::addr_of_mut!(object_ptr) as sys::GDExtensionTypePtr; let converter = sys::builtin_fn!(object_from_variant); converter(object_ptr_ptr, self.var_sys()); - object_ptr.is_null() } + object_ptr.is_null() } else { false }; - if is_a_null_object { + if is_null_object { VariantType::Nil } else { VariantType::from_sys(sys_type) diff --git a/itest/rust/src/variant_test.rs b/itest/rust/src/variant_test.rs index 82b7bff15..541568b36 100644 --- a/itest/rust/src/variant_test.rs +++ b/itest/rust/src/variant_test.rs @@ -5,7 +5,10 @@ */ use crate::itest; -use godot::builtin::{FromVariant, GodotString, StringName, ToVariant, Variant, Vector2, Vector3}; +use godot::builtin::{ + FromVariant, GodotString, NodePath, StringName, ToVariant, Variant, Vector2, Vector3, +}; +use godot::engine::Node3D; use godot::obj::InstanceId; use godot::sys::{GodotFfi, VariantOperator, VariantType}; use std::cmp::Ordering; @@ -23,7 +26,7 @@ pub fn run() -> bool { ok &= variant_evaluate_total_order(); ok &= variant_sys_conversion(); ok &= variant_sys_conversion2(); - ok &= variant_object_nullptr(); + ok &= variant_null_object_is_nil(); ok } @@ -199,29 +202,23 @@ fn variant_sys_conversion() { } #[itest] -fn variant_object_nullptr() { - // Some Variants that are of type Object actually contain a null pointer. - // The should report their type as Nil instead, so that they don't create invalid Gd objects. - let variant; - unsafe { - // Create a variant from a null object pointer. - variant = Variant::from_var_sys_init(|self_ptr| { - let null_pointer: godot::sys::GDExtensionObjectPtr = std::ptr::null_mut(); - let converter = godot::sys::builtin_fn!(object_to_variant); - converter( - self_ptr, - std::ptr::addr_of!(null_pointer) as godot::sys::GDExtensionTypePtr, - ); - }); - - // Get the variant's internal type and ensure that it is actually still an Object. - let sys_type: godot::sys::GDExtensionVariantType = - godot::sys::interface_fn!(variant_get_type)(variant.var_sys()); - assert_eq!(sys_type, godot::sys::GDEXTENSION_VARIANT_TYPE_OBJECT); - } +fn variant_null_object_is_nil() { + use godot::sys; + + let mut node = Node3D::new_alloc(); + let node_path = NodePath::from("res://NonExisting.tscn"); - // The actual test of the public API, ensure that the variant's type is Nil even though the internal type is Object. + // Simulates an object that is returned but null + // Use reflection to get a variant as return type + let variant = node.call("get_node_or_null".into(), &[node_path.to_variant()]); + let raw_type: sys::GDExtensionVariantType = + unsafe { sys::interface_fn!(variant_get_type)(variant.var_sys()) }; + + // Verify that this appears as NIL to the user, even though it's internally OBJECT with a null object pointer + assert_eq!(raw_type, sys::GDEXTENSION_VARIANT_TYPE_OBJECT); assert_eq!(variant.get_type(), VariantType::Nil); + + node.free(); } #[itest] From b0c091bc229106a030e0849012661c8f69c664bb Mon Sep 17 00:00:00 2001 From: Jan Haller Date: Sun, 29 Jan 2023 13:09:25 +0100 Subject: [PATCH 41/54] Code reuse: raw_object_init() and ptr_then() functions --- godot-core/src/builtin/variant/mod.rs | 17 +++++----- godot-core/src/obj/gd.rs | 46 +++++++++++++++------------ godot-ffi/src/lib.rs | 13 ++++++++ 3 files changed, 47 insertions(+), 29 deletions(-) diff --git a/godot-core/src/builtin/variant/mod.rs b/godot-core/src/builtin/variant/mod.rs index 20122cdcf..8207a4fc4 100644 --- a/godot-core/src/builtin/variant/mod.rs +++ b/godot-core/src/builtin/variant/mod.rs @@ -74,15 +74,16 @@ impl Variant { pub fn get_type(&self) -> VariantType { let sys_type = self.sys_type(); - // There is a special case when the Variant is an Object, but the Object* is null. + // There is a special case when the Variant has type OBJECT, but the Object* is null. let is_null_object = if sys_type == sys::GDEXTENSION_VARIANT_TYPE_OBJECT { - // object_from_variant expects sys::GDExtensionTypePtr, which is essentially an Object**. - let mut object_ptr: sys::GDExtensionObjectPtr = ptr::null_mut(); - let object_ptr_ptr = ptr::addr_of_mut!(object_ptr) as sys::GDExtensionTypePtr; - unsafe { - let converter = sys::builtin_fn!(object_from_variant); - converter(object_ptr_ptr, self.var_sys()); - } + // SAFETY: we checked that the raw type is OBJECT, so we can interpret the type-ptr as address of an object-ptr. + let object_ptr = unsafe { + crate::obj::raw_object_init(|type_ptr| { + let converter = sys::builtin_fn!(object_from_variant); + converter(type_ptr, self.var_sys()); + }) + }; + object_ptr.is_null() } else { false diff --git a/godot-core/src/obj/gd.rs b/godot-core/src/obj/gd.rs index 0436bbf4f..b9f472711 100644 --- a/godot-core/src/obj/gd.rs +++ b/godot-core/src/obj/gd.rs @@ -366,11 +366,7 @@ impl Gd { let class_tag = interface_fn!(classdb_get_class_tag)(class_name.string_sys()); let cast_object_ptr = interface_fn!(object_cast_to)(self.obj_sys(), class_tag); - if cast_object_ptr.is_null() { - None - } else { - Some(Gd::from_obj_sys(cast_object_ptr)) - } + sys::ptr_then(cast_object_ptr, |ptr| Gd::from_obj_sys(ptr)) } pub(crate) fn as_ref_counted(&self, apply: impl Fn(&mut engine::RefCounted) -> R) -> R { @@ -500,32 +496,40 @@ impl GodotFfi for Gd { impl Gd { /// Runs `init_fn` on the address of a pointer (initialized to null). If that pointer is still null after the `init_fn` call, - /// then `None` will be returned; otherwise `from_obj_sys(ptr)`. + /// then `None` will be returned; otherwise `Gd::from_obj_sys(ptr)`. /// /// # Safety - /// `init_fn` must be a function that correctly handles an "type pointer" pointing to an "object pointer" + /// `init_fn` must be a function that correctly handles a _type pointer_ pointing to an _object pointer_. #[doc(hidden)] // TODO unsafe on init_fn instead of this fn? pub unsafe fn from_sys_init_opt(init_fn: impl FnOnce(sys::GDExtensionTypePtr)) -> Option { // Note: see _call_native_mb_ret_obj() in godot-cpp, which does things quite different (e.g. querying the instance binding). - // return_ptr has type GDExtensionTypePtr = GDExtensionObjectPtr* = OpaqueObject* = Object** - // (in other words, the type-ptr contains the _address_ of an object-ptr). - let mut object_ptr: sys::GDExtensionObjectPtr = ptr::null_mut(); - let return_ptr: *mut sys::GDExtensionObjectPtr = ptr::addr_of_mut!(object_ptr); - - init_fn(return_ptr as sys::GDExtensionTypePtr); - - // We don't need to know if Object** is null, but if Object* is null; return_ptr has the address of a local (never null). - if object_ptr.is_null() { - None - } else { - let obj = Gd::from_obj_sys(object_ptr); // equivalent to Gd::from_sys(return_ptr) - Some(obj) - } + // Initialize pointer with given function, return Some(ptr) on success and None otherwise + let object_ptr = raw_object_init(init_fn); + sys::ptr_then(object_ptr, |ptr| Gd::from_obj_sys(ptr)) } } +/// Runs `init_fn` on the address of a pointer (initialized to null), then returns that pointer, possibly still null. +/// +/// # Safety +/// `init_fn` must be a function that correctly handles a _type pointer_ pointing to an _object pointer_. +#[doc(hidden)] +pub unsafe fn raw_object_init( + init_fn: impl FnOnce(sys::GDExtensionTypePtr), +) -> sys::GDExtensionObjectPtr { + // return_ptr has type GDExtensionTypePtr = GDExtensionObjectPtr* = OpaqueObject* = Object** + // (in other words, the type-ptr contains the _address_ of an object-ptr). + let mut object_ptr: sys::GDExtensionObjectPtr = ptr::null_mut(); + let return_ptr: *mut sys::GDExtensionObjectPtr = ptr::addr_of_mut!(object_ptr); + + init_fn(return_ptr as sys::GDExtensionTypePtr); + + // We don't need to know if Object** is null, but if Object* is null; return_ptr has the address of a local (never null). + object_ptr +} + /// Destructor with semantics depending on memory strategy. /// /// * If this `Gd` smart pointer holds a reference-counted type, this will decrement the reference counter. diff --git a/godot-ffi/src/lib.rs b/godot-ffi/src/lib.rs index e6ac2f5c8..07ea024a9 100644 --- a/godot-ffi/src/lib.rs +++ b/godot-ffi/src/lib.rs @@ -203,3 +203,16 @@ pub fn force_mut_ptr(ptr: *const T) -> *mut T { pub fn to_const_ptr(ptr: *mut T) -> *const T { ptr as *const T } + +/// If `ptr` is not null, returns `Some(mapper(ptr))`; otherwise `None`. +pub fn ptr_then(ptr: *mut T, mapper: F) -> Option +where + F: FnOnce(*mut T) -> R, +{ + // Could also use NonNull in signature, but for this project we always deal with FFI raw pointers + if ptr.is_null() { + None + } else { + Some(mapper(ptr)) + } +} From 156e6bef87e9f5715fda55a3aeeed334604475d6 Mon Sep 17 00:00:00 2001 From: Thomas ten Cate Date: Sat, 28 Jan 2023 14:48:20 +0100 Subject: [PATCH 42/54] Add Contributing.md --- Contributing.md | 64 +++++++++++++++++++++++++++++++++++++++++++++++++ ReadMe.md | 7 +----- 2 files changed, 65 insertions(+), 6 deletions(-) create mode 100644 Contributing.md diff --git a/Contributing.md b/Contributing.md new file mode 100644 index 000000000..b61e0c7fa --- /dev/null +++ b/Contributing.md @@ -0,0 +1,64 @@ +# Contributing to `gdextension` + +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]! + +## Check script + +The script `check.sh` in the project root can be used to mimic a CI run locally. It's useful to run this before you commit, push or create a pull request: + +``` +$ check.sh +``` + +At the time of writing, this will run formatting, clippy, unit tests and integration tests. More checks may be added in the future. Run `./check.sh --help` to see all available options. + +If you like, you can set this as a pre-commit hook in your local clone of the repository: + +``` +$ 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 additional flags might be needed, the preferred way to run unit tests is through the `check.sh` script: + +``` +$ ./check.sh test +``` + +## Integration tests + +The `itest/` directory contains a suite of integration tests that actually exercise `gdextension` 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. + +You can run the integration tests like this: + +``` +$ ./check.sh itest +``` + +Just like when compiling the crate, the `GODOT4_BIN` environment variable can be used to supply the path and filename of your Godot executable. + +## Formatting + +`rustfmt` is used to format code. `check.sh` only warns about formatting issues, but does not fix them. To do that, run: + +``` +$ cargo fmt +``` + +## Clippy + +`clippy` is used for additional lint warnings not implemented in `rustc`. This, too, is best run through `check.sh`: + +``` +$ check.sh clippy +``` + +[GitHub issue]: https://github.com/godot-rust/gdextension/issues +[Discord]: https://discord.gg/aKUCJ8rJsc diff --git a/ReadMe.md b/ReadMe.md index 3fea2ed9e..8aa0605ff 100644 --- a/ReadMe.md +++ b/ReadMe.md @@ -90,12 +90,7 @@ those changes available (and only those, no surrounding code). ## Contributing -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 `#gdext-dev` channel on [Discord]! - +Contributions are very welcome! If you want to help out, see [`Contributing.md`](Contributing.md) for some pointers on getting started! [Godot]: https://godotengine.org [`gdnative`]: https://github.com/godot-rust/gdnative From 2bdab6b168cf469161ceaa89098b4744c7843214 Mon Sep 17 00:00:00 2001 From: Thomas ten Cate Date: Sat, 28 Jan 2023 14:21:41 +0100 Subject: [PATCH 43/54] Add some documentation about memory management and thread safety --- godot/src/lib.rs | 64 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/godot/src/lib.rs b/godot/src/lib.rs index 194f9aebd..94e46fbac 100644 --- a/godot/src/lib.rs +++ b/godot/src/lib.rs @@ -4,6 +4,70 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ +//! Rust bindings for GDExtension, the extension API of [Godot](https://godotengine.org/) 4. +//! +//! This documentation is a work in progress. +//! +//! # Kinds of types +//! +//! Godot is written in C++, which doesn't have the same strict guarantees about safety and +//! mutability that Rust does. As a result, not everything in this crate will look and feel +//! entirely "Rusty". We distinguish four different kinds of types: +//! +//! 1. **Value types**: `i64`, `f64`, and mathematical types like +//! [`Vector2`][crate::builtin::Vector2] and [`Color`][crate::builtin::Color]. +//! +//! These are the simplest to understand and to work with. They implement `Clone` and often +//! `Copy` as well. They are implemented with the same memory layout as their counterparts in +//! Godot itself, and typically have public fields.

+//! +//! 2. **Copy-on-write types**: [`GodotString`][crate::builtin::GodotString], +//! [`StringName`][crate::builtin::StringName], and `Packed*Array` types. +//! +//! These mostly act like value types, similar to Rust's own `Vec`. You can `Clone` them to get +//! a full copy of the entire object, as you would expect. +//! +//! Under the hood in Godot, these types are implemented with copy-on-write, so that data can be +//! shared until one of the copies needs to be modified. However, this performance optimization +//! is entirely hidden from the API and you don't normally need to worry about it.

+//! +//! 3. **Reference-counted types**: [`Array`][crate::builtin::Array], +//! [`Dictionary`][crate::builtin::Dictionary], and [`Gd`][crate::obj::Gd] where `T` inherits +//! from [`RefCounted`][crate::engine::RefCounted]. +//! +//! These types may share their underlying data between multiple instances: changes to one +//! instance are visible in another. Think of them as `Rc>` but without any runtime +//! borrow checking. +//! +//! Since there is no way to prevent or even detect this sharing from Rust, you need to be more +//! careful when using such types. For example, when iterating over an `Array`, make sure that +//! it isn't being modified at the same time through another reference. +//! +//! To avoid confusion these types don't implement `Clone`. You can use +//! [`Share`][crate::obj::Share] to create a new reference to the same instance, and +//! type-specific methods such as +//! [`Array::duplicate_deep()`][crate::builtin::Array::duplicate_deep] to make actual +//! copies.

+//! +//! 4. **Manually managed types**: [`Gd`][crate::obj::Gd] where `T` inherits from +//! [`Object`][crate::engine::Object] but not from [`RefCounted`][crate::engine::RefCounted]; +//! most notably, this includes all `Node` classes. +//! +//! These also share data, but do not use reference counting to manage their memory. Instead, +//! you must either hand over ownership to Godot (e.g. by adding a node to the scene tree) or +//! free them manually using [`Gd::free()`][crate::obj::Gd::free].

+//! +//! # Thread safety +//! +//! [Godot's own thread safety +//! rules](https://docs.godotengine.org/en/latest/tutorials/performance/thread_safe_apis.html) +//! apply. Types in this crate implement (or don't implement) `Send` and `Sync` wherever +//! appropriate, but the Rust compiler cannot check what happens to an object through C++ or +//! GDScript. +//! +//! As a rule of thumb, if you must use threading, prefer to use Rust threads instead of Godot +//! threads. + #[doc(inline)] pub use godot_core::{builtin, engine, log, obj, sys}; From 2ff08c8a7601c7e6faa4c033a97c0b72f32685fc Mon Sep 17 00:00:00 2001 From: Jan Haller Date: Tue, 31 Jan 2023 12:45:01 +0100 Subject: [PATCH 44/54] Update GDExtension header --- godot-codegen/input/gdextension_interface.h | 2 ++ 1 file changed, 2 insertions(+) diff --git a/godot-codegen/input/gdextension_interface.h b/godot-codegen/input/gdextension_interface.h index 12437814b..7afa747cb 100644 --- a/godot-codegen/input/gdextension_interface.h +++ b/godot-codegen/input/gdextension_interface.h @@ -551,6 +551,8 @@ typedef struct { GDExtensionVariantPtr (*array_operator_index)(GDExtensionTypePtr p_self, GDExtensionInt p_index); // p_self should be an Array ptr GDExtensionVariantPtr (*array_operator_index_const)(GDExtensionConstTypePtr p_self, GDExtensionInt p_index); // p_self should be an Array ptr + void (*array_ref)(GDExtensionTypePtr p_self, GDExtensionConstTypePtr p_from); // p_self should be an Array ptr + void (*array_set_typed)(GDExtensionTypePtr p_self, uint32_t p_type, GDExtensionConstStringNamePtr p_class_name, GDExtensionConstVariantPtr p_script); // p_self should be an Array ptr /* Dictionary functions */ From 28e1bf29821aeb85f67d0d1d00153e878b57a3f1 Mon Sep 17 00:00:00 2001 From: Jan Haller Date: Tue, 31 Jan 2023 12:51:55 +0100 Subject: [PATCH 45/54] Codegen: consistent variant types (AABB -> Aabb, RID -> Rid) --- godot-codegen/src/central_generator.rs | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/godot-codegen/src/central_generator.rs b/godot-codegen/src/central_generator.rs index 514a4725d..2d0adafcb 100644 --- a/godot-codegen/src/central_generator.rs +++ b/godot-codegen/src/central_generator.rs @@ -441,14 +441,12 @@ fn make_enumerator( value: i32, ctx: &mut Context, ) -> (Ident, TokenStream, Literal) { - //let shout_name = format_ident!("{}", type_names.shout_case); - let (first, rest) = type_names.json_builtin_name.split_at(1); - - let pascal_name = format_ident!("{}{}", first.to_ascii_uppercase(), rest); - let rust_ty = to_rust_type(&type_names.json_builtin_name, ctx); + let enumerator_name = &type_names.json_builtin_name; + let pascal_name = to_pascal_case(enumerator_name); + let rust_ty = to_rust_type(enumerator_name, ctx); let ord = Literal::i32_unsuffixed(value); - (pascal_name, rust_ty.to_token_stream(), ord) + (ident(&pascal_name), rust_ty.to_token_stream(), ord) } fn make_opaque_type(name: &str, size: usize) -> TokenStream { From 67b2bf7c0d63bddec10eaa826498fdfa2f271997 Mon Sep 17 00:00:00 2001 From: Lili Zoey Date: Tue, 31 Jan 2023 12:08:44 +0100 Subject: [PATCH 46/54] Add Typechecking to FromVariant Add testing for `Variant::get_type()` --- godot-core/src/builtin/variant/impls.rs | 4 +- itest/rust/src/variant_test.rs | 49 +++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 1 deletion(-) diff --git a/godot-core/src/builtin/variant/impls.rs b/godot-core/src/builtin/variant/impls.rs index a464b77f5..ca91560a4 100644 --- a/godot-core/src/builtin/variant/impls.rs +++ b/godot-core/src/builtin/variant/impls.rs @@ -48,7 +48,9 @@ macro_rules! impl_variant_traits { // void String::operator=(const String &p_str) { _cowdata._ref(p_str._cowdata); } // does a copy-on-write and explodes if this->_cowdata is not initialized. // We can thus NOT use Self::from_sys_init(). - + if variant.get_type() != Self::variant_type() { + return Err(VariantConversionError) + } let mut value = <$T>::default(); let result = unsafe { let converter = sys::builtin_fn!($to_fn); diff --git a/itest/rust/src/variant_test.rs b/itest/rust/src/variant_test.rs index 541568b36..d996fc68c 100644 --- a/itest/rust/src/variant_test.rs +++ b/itest/rust/src/variant_test.rs @@ -10,6 +10,7 @@ use godot::builtin::{ }; use godot::engine::Node3D; use godot::obj::InstanceId; +use godot::prelude::{Array, Dictionary, VariantConversionError}; use godot::sys::{GodotFfi, VariantOperator, VariantType}; use std::cmp::Ordering; use std::fmt::{Debug, Display}; @@ -27,6 +28,8 @@ pub fn run() -> bool { ok &= variant_sys_conversion(); ok &= variant_sys_conversion2(); ok &= variant_null_object_is_nil(); + ok &= variant_conversion_fails(); + ok &= variant_type_correct(); ok } @@ -238,6 +241,52 @@ fn variant_sys_conversion2() { */ } +#[itest] +fn variant_conversion_fails() { + assert_eq!( + "hello".to_variant().try_to::(), + Err(VariantConversionError) + ); + assert_eq!(28.to_variant().try_to::(), Err(VariantConversionError)); + assert_eq!( + 10.to_variant().try_to::(), + Err(VariantConversionError) + ); + assert_eq!( + false.to_variant().try_to::(), + Err(VariantConversionError) + ); + assert_eq!( + Array::default().to_variant().try_to::(), + Err(VariantConversionError) + ); + assert_eq!( + Dictionary::default().to_variant().try_to::(), + Err(VariantConversionError) + ); + assert_eq!( + Variant::nil().to_variant().try_to::(), + Err(VariantConversionError) + ); +} + +#[itest] +fn variant_type_correct() { + assert_eq!(Variant::nil().get_type(), VariantType::Nil); + assert_eq!(0.to_variant().get_type(), VariantType::Int); + assert_eq!(3.8.to_variant().get_type(), VariantType::Float); + assert_eq!(false.to_variant().get_type(), VariantType::Bool); + assert_eq!("string".to_variant().get_type(), VariantType::String); + assert_eq!( + StringName::from("string_name").to_variant().get_type(), + VariantType::StringName + ); + assert_eq!(Array::default().to_variant().get_type(), VariantType::Array); + assert_eq!( + Dictionary::default().to_variant().get_type(), + VariantType::Dictionary + ); +} // ---------------------------------------------------------------------------------------------------------------------------------------------- fn roundtrip(value: T) From c1a36da2af5ad09807f64a596a036ce008ca40b7 Mon Sep 17 00:00:00 2001 From: Jan Haller Date: Wed, 1 Feb 2023 19:06:30 +0100 Subject: [PATCH 47/54] Fix inverted UP/DOWN for Vector3 and Vector3i --- godot-core/src/builtin/vector3.rs | 8 ++++---- godot-core/src/builtin/vector3i.rs | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/godot-core/src/builtin/vector3.rs b/godot-core/src/builtin/vector3.rs index 4d00aac33..30786d755 100644 --- a/godot-core/src/builtin/vector3.rs +++ b/godot-core/src/builtin/vector3.rs @@ -45,11 +45,11 @@ impl Vector3 { /// Unit vector in +X direction. Can be interpreted as right in an untransformed 3D world. pub const RIGHT: Self = Self::new(1.0, 0.0, 0.0); - /// Unit vector in -Y direction. Typically interpreted as down in a 3D world. - pub const UP: Self = Self::new(0.0, -1.0, 0.0); - /// Unit vector in +Y direction. Typically interpreted as up in a 3D world. - pub const DOWN: Self = Self::new(0.0, 1.0, 0.0); + pub const UP: Self = Self::new(0.0, 1.0, 0.0); + + /// Unit vector in -Y direction. Typically interpreted as down in a 3D world. + pub const DOWN: Self = Self::new(0.0, -1.0, 0.0); /// Unit vector in -Z direction. Can be interpreted as "into the screen" in an untransformed 3D world. pub const FORWARD: Self = Self::new(0.0, 0.0, -1.0); diff --git a/godot-core/src/builtin/vector3i.rs b/godot-core/src/builtin/vector3i.rs index f42036412..dd7eaa69b 100644 --- a/godot-core/src/builtin/vector3i.rs +++ b/godot-core/src/builtin/vector3i.rs @@ -44,11 +44,11 @@ impl Vector3i { /// Unit vector in +X direction. pub const RIGHT: Self = Self::new(1, 0, 0); - /// Unit vector in -Y direction. - pub const UP: Self = Self::new(0, -1, 0); - /// Unit vector in +Y direction. - pub const DOWN: Self = Self::new(0, 1, 0); + pub const UP: Self = Self::new(0, 1, 0); + + /// Unit vector in -Y direction. + pub const DOWN: Self = Self::new(0, -1, 0); /// Unit vector in -Z direction. pub const FORWARD: Self = Self::new(0, 0, -1); From c9996c3ef2881e682bcaae46aa186eda62253446 Mon Sep 17 00:00:00 2001 From: RealAstolfo Date: Sun, 15 Jan 2023 21:20:56 +0100 Subject: [PATCH 48/54] Add more vector functionality --- godot-core/src/builtin/math.rs | 11 ++ godot-core/src/builtin/mod.rs | 2 + godot-core/src/builtin/vector2.rs | 215 ++++++++++++++++++++++++++++- godot-core/src/builtin/vector3.rs | 217 ++++++++++++++++++++++++++++++ 4 files changed, 439 insertions(+), 6 deletions(-) diff --git a/godot-core/src/builtin/math.rs b/godot-core/src/builtin/math.rs index d2d8966fc..600cfc3e4 100644 --- a/godot-core/src/builtin/math.rs +++ b/godot-core/src/builtin/math.rs @@ -41,6 +41,16 @@ pub fn snapped(mut value: f32, step: f32) -> f32 { value } +pub fn sign(value: f32) -> f32 { + if value == 0.0 { + 0.0 + } else if value < 0.0 { + -1.0 + } else { + 1.0 + } +} + pub fn bezier_derivative(start: f32, control_1: f32, control_2: f32, end: f32, t: f32) -> f32 { let omt = 1.0 - t; let omt2 = omt * omt; @@ -66,6 +76,7 @@ pub fn cubic_interpolate(from: f32, to: f32, pre: f32, post: f32, weight: f32) - + (-pre + 3.0 * from - 3.0 * to + post) * (weight * weight * weight)) } +#[allow(clippy::too_many_arguments)] pub fn cubic_interpolate_in_time( from: f32, to: f32, diff --git a/godot-core/src/builtin/mod.rs b/godot-core/src/builtin/mod.rs index 30d2a8044..60ad180f4 100644 --- a/godot-core/src/builtin/mod.rs +++ b/godot-core/src/builtin/mod.rs @@ -38,6 +38,7 @@ mod vector_macros; mod arrays; mod color; mod dictionary; +mod math; mod node_path; mod others; mod packed_array; @@ -58,6 +59,7 @@ pub use crate::dict; pub use arrays::*; pub use color::*; pub use dictionary::*; +pub use math::*; pub use node_path::*; pub use others::*; pub use packed_array::*; diff --git a/godot-core/src/builtin/vector2.rs b/godot-core/src/builtin/vector2.rs index b4372be43..318fd3b05 100644 --- a/godot-core/src/builtin/vector2.rs +++ b/godot-core/src/builtin/vector2.rs @@ -3,12 +3,13 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ - use std::fmt; +use std::ops::*; use godot_ffi as sys; use sys::{ffi_methods, GodotFfi}; +use crate::builtin::math::*; use crate::builtin::{inner, Vector2i}; /// Vector used for 2D math using floating point coordinates. @@ -70,11 +71,6 @@ impl Vector2 { } } - /// Returns the result of rotating this vector by `angle` (in radians). - pub fn rotated(self, angle: f32) -> Self { - Self::from_glam(glam::Affine2::from_angle(angle).transform_vector2(self.to_glam())) - } - /// Converts the corresponding `glam` type to `Self`. fn from_glam(v: glam::Vec2) -> Self { Self::new(v.x, v.y) @@ -85,6 +81,213 @@ impl Vector2 { glam::Vec2::new(self.x, self.y) } + pub fn angle(self) -> f32 { + self.y.atan2(self.x) + } + + pub fn angle_to(self, to: Self) -> f32 { + self.to_glam().angle_between(to.to_glam()) + } + + pub fn angle_to_point(self, to: Self) -> f32 { + (to - self).angle() + } + + pub fn aspect(self) -> f32 { + self.x / self.y + } + + pub fn bezier_derivative(self, control_1: Self, control_2: Self, end: Self, t: f32) -> Self { + let x = bezier_derivative(self.x, control_1.x, control_2.x, end.x, t); + let y = bezier_derivative(self.y, control_1.y, control_2.y, end.y, t); + + Self::new(x, y) + } + + pub fn bezier_interpolate(self, control_1: Self, control_2: Self, end: Self, t: f32) -> Self { + let x = bezier_interpolate(self.x, control_1.x, control_2.x, end.x, t); + let y = bezier_interpolate(self.y, control_1.y, control_2.y, end.y, t); + + Self::new(x, y) + } + + pub fn bounce(self, normal: Self) -> Self { + -self.reflect(normal) + } + + pub fn ceil(self) -> Self { + Self::from_glam(self.to_glam().ceil()) + } + + pub fn clamp(self, min: Self, max: Self) -> Self { + Self::from_glam(self.to_glam().clamp(min.to_glam(), max.to_glam())) + } + + pub fn cross(self, with: Self) -> f32 { + self.to_glam().perp_dot(with.to_glam()) + } + + pub fn cubic_interpolate(self, b: Self, pre_a: Self, post_b: Self, weight: f32) -> Self { + let x = cubic_interpolate(self.x, b.x, pre_a.x, post_b.x, weight); + let y = cubic_interpolate(self.y, b.y, pre_a.y, post_b.y, weight); + + Self::new(x, y) + } + + #[allow(clippy::too_many_arguments)] + pub fn cubic_interpolate_in_time( + self, + b: Self, + pre_a: Self, + post_b: Self, + weight: f32, + b_t: f32, + pre_a_t: f32, + post_b_t: f32, + ) -> Self { + let x = cubic_interpolate_in_time( + self.x, b.x, pre_a.x, post_b.x, weight, b_t, pre_a_t, post_b_t, + ); + let y = cubic_interpolate_in_time( + self.y, b.y, pre_a.y, post_b.y, weight, b_t, pre_a_t, post_b_t, + ); + + Self::new(x, y) + } + + pub fn direction_to(self, to: Self) -> Self { + (to - self).normalized() + } + + pub fn distance_squared_to(self, to: Self) -> f32 { + (to - self).length_squared() + } + + pub fn distance_to(self, to: Self) -> f32 { + (to - self).length() + } + + pub fn dot(self, other: Self) -> f32 { + self.to_glam().dot(other.to_glam()) + } + + pub fn floor(self) -> Self { + Self::from_glam(self.to_glam().floor()) + } + + pub fn from_angle(angle: f32) -> Self { + Self::from_glam(glam::Vec2::from_angle(angle)) + } + + pub fn is_equal_approx(self, to: Self) -> bool { + is_equal_approx(self.x, to.x) && is_equal_approx(self.y, to.y) + } + + pub fn is_finite(self) -> bool { + self.to_glam().is_finite() + } + + pub fn is_normalized(self) -> bool { + self.to_glam().is_normalized() + } + + pub fn is_zero_approx(self) -> bool { + is_zero_approx(self.x) && is_zero_approx(self.y) + } + + pub fn length_squared(self) -> f32 { + self.to_glam().length_squared() + } + + pub fn lerp(self, to: Self, weight: f32) -> Self { + Self::from_glam(self.to_glam().lerp(to.to_glam(), weight)) + } + + pub fn limit_length(self, length: Option) -> Self { + Self::from_glam(self.to_glam().clamp_length_max(length.unwrap_or(1.0))) + } + + pub fn max_axis_index(self) -> Vector2Axis { + if self.x < self.y { + Vector2Axis::Y + } else { + Vector2Axis::X + } + } + + pub fn min_axis_index(self) -> Vector2Axis { + if self.x < self.y { + Vector2Axis::X + } else { + Vector2Axis::Y + } + } + + pub fn move_toward(self, to: Self, delta: f32) -> Self { + let vd = to - self; + let len = vd.length(); + if len <= delta || len < CMP_EPSILON { + to + } else { + self + vd / len * delta + } + } + + pub fn orthogonal(self) -> Self { + Self::new(self.y, -self.x) + } + + pub fn posmod(self, pmod: f32) -> Self { + Self::new(fposmod(self.x, pmod), fposmod(self.y, pmod)) + } + + pub fn posmodv(self, modv: Self) -> Self { + Self::new(fposmod(self.x, modv.x), fposmod(self.y, modv.y)) + } + + pub fn project(self, b: Self) -> Self { + Self::from_glam(self.to_glam().project_onto(b.to_glam())) + } + + pub fn reflect(self, normal: Self) -> Self { + Self::from_glam(self.to_glam().reject_from(normal.to_glam())) + } + + pub fn round(self) -> Self { + Self::from_glam(self.to_glam().round()) + } + + pub fn sign(self) -> Self { + Self::new(sign(self.x), sign(self.y)) + } + + // TODO compare with gdnative implementation: + // https://github.com/godot-rust/gdnative/blob/master/gdnative-core/src/core_types/vector3.rs#L335-L343 + pub fn slerp(self, to: Self, weight: f32) -> Self { + let start_length_sq = self.length_squared(); + let end_length_sq = to.length_squared(); + if start_length_sq == 0.0 || end_length_sq == 0.0 { + return self.lerp(to, weight); + } + let start_length = start_length_sq.sqrt(); + let result_length = lerp(start_length, end_length_sq.sqrt(), weight); + let angle = self.angle_to(to); + self.rotated(angle * weight) * (result_length / start_length) + } + + pub fn slide(self, normal: Self) -> Self { + self - normal * self.dot(normal) + } + + pub fn snapped(self, step: Self) -> Self { + Self::new(snapped(self.x, step.x), snapped(self.y, step.y)) + } + + /// Returns the result of rotating this vector by `angle` (in radians). + pub fn rotated(self, angle: f32) -> Self { + Self::from_glam(glam::Affine2::from_angle(angle).transform_vector2(self.to_glam())) + } + #[doc(hidden)] pub fn as_inner(&self) -> inner::InnerVector2 { inner::InnerVector2::from_outer(self) diff --git a/godot-core/src/builtin/vector3.rs b/godot-core/src/builtin/vector3.rs index 30786d755..92d156c51 100644 --- a/godot-core/src/builtin/vector3.rs +++ b/godot-core/src/builtin/vector3.rs @@ -3,12 +3,14 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ +use std::ops::*; use std::fmt; use godot_ffi as sys; use sys::{ffi_methods, GodotFfi}; +use crate::builtin::math::*; use crate::builtin::Vector3i; /// Vector used for 3D math using floating point coordinates. @@ -85,6 +87,221 @@ impl Vector3 { fn to_glam(self) -> glam::Vec3 { glam::Vec3::new(self.x, self.y, self.z) } + + pub fn angle_to(self, to: Self) -> f32 { + self.to_glam().angle_between(to.to_glam()) + } + + pub fn bezier_derivative(self, control_1: Self, control_2: Self, end: Self, t: f32) -> Self { + let x = bezier_derivative(self.x, control_1.x, control_2.x, end.x, t); + let y = bezier_derivative(self.y, control_1.y, control_2.y, end.y, t); + let z = bezier_derivative(self.z, control_1.z, control_2.z, end.z, t); + + Self::new(x, y, z) + } + + pub fn bezier_interpolate(self, control_1: Self, control_2: Self, end: Self, t: f32) -> Self { + let x = bezier_interpolate(self.x, control_1.x, control_2.x, end.x, t); + let y = bezier_interpolate(self.y, control_1.y, control_2.y, end.y, t); + let z = bezier_interpolate(self.z, control_1.z, control_2.z, end.z, t); + + Self::new(x, y, z) + } + + pub fn bounce(self, normal: Self) -> Self { + -self.reflect(normal) + } + + pub fn ceil(self) -> Self { + Self::from_glam(self.to_glam().ceil()) + } + + pub fn clamp(self, min: Self, max: Self) -> Self { + Self::from_glam(self.to_glam().clamp(min.to_glam(), max.to_glam())) + } + + pub fn cross(self, with: Self) -> Self { + Self::from_glam(self.to_glam().cross(with.to_glam())) + } + + pub fn cubic_interpolate(self, b: Self, pre_a: Self, post_b: Self, weight: f32) -> Self { + let x = cubic_interpolate(self.x, b.x, pre_a.x, post_b.x, weight); + let y = cubic_interpolate(self.y, b.y, pre_a.y, post_b.y, weight); + let z = cubic_interpolate(self.z, b.z, pre_a.z, post_b.z, weight); + + Self::new(x, y, z) + } + + #[allow(clippy::too_many_arguments)] + pub fn cubic_interpolate_in_time( + self, + b: Self, + pre_a: Self, + post_b: Self, + weight: f32, + b_t: f32, + pre_a_t: f32, + post_b_t: f32, + ) -> Self { + let x = cubic_interpolate_in_time( + self.x, b.x, pre_a.x, post_b.x, weight, b_t, pre_a_t, post_b_t, + ); + let y = cubic_interpolate_in_time( + self.y, b.y, pre_a.y, post_b.y, weight, b_t, pre_a_t, post_b_t, + ); + let z = cubic_interpolate_in_time( + self.z, b.z, pre_a.z, post_b.z, weight, b_t, pre_a_t, post_b_t, + ); + + Self::new(x, y, z) + } + + pub fn direction_to(self, to: Self) -> Self { + (to - self).normalized() + } + + pub fn distance_squared_to(self, to: Self) -> f32 { + (to - self).length_squared() + } + + pub fn distance_to(self, to: Self) -> f32 { + (to - self).length() + } + + pub fn dot(self, with: Self) -> f32 { + self.to_glam().dot(with.to_glam()) + } + + pub fn floor(self) -> Self { + Self::from_glam(self.to_glam().floor()) + } + + pub fn inverse(self) -> Self { + Self::new(1.0 / self.x, 1.0 / self.y, 1.0 / self.z) + } + + pub fn is_equal_approx(self, to: Self) -> bool { + is_equal_approx(self.x, to.x) + && is_equal_approx(self.y, to.y) + && is_equal_approx(self.z, to.z) + } + + pub fn is_finite(self) -> bool { + self.to_glam().is_finite() + } + + pub fn is_normalized(self) -> bool { + self.to_glam().is_normalized() + } + + pub fn is_zero_approx(self) -> bool { + is_zero_approx(self.x) && is_zero_approx(self.y) && is_zero_approx(self.z) + } + + pub fn length_squared(self) -> f32 { + self.to_glam().length_squared() + } + + pub fn lerp(self, to: Self, weight: f32) -> Self { + Self::from_glam(self.to_glam().lerp(to.to_glam(), weight)) + } + + pub fn limit_length(self, length: Option) -> Self { + Self::from_glam(self.to_glam().clamp_length_max(length.unwrap_or(1.0))) + } + + pub fn max_axis_index(self) -> Vector3Axis { + if self.x < self.y { + if self.y < self.z { + Vector3Axis::Z + } else { + Vector3Axis::Y + } + } else if self.x < self.z { + Vector3Axis::Z + } else { + Vector3Axis::X + } + } + + pub fn min_axis_index(self) -> Vector3Axis { + if self.x < self.y { + if self.x < self.z { + Vector3Axis::X + } else { + Vector3Axis::Z + } + } else if self.y < self.z { + Vector3Axis::Y + } else { + Vector3Axis::Z + } + } + + pub fn move_toward(self, to: Self, delta: f32) -> Self { + let vd = to - self; + let len = vd.length(); + if len <= delta || len < CMP_EPSILON { + to + } else { + self + vd / len * delta + } + } + + pub fn posmod(self, pmod: f32) -> Self { + Self::new( + fposmod(self.x, pmod), + fposmod(self.y, pmod), + fposmod(self.z, pmod), + ) + } + + pub fn posmodv(self, modv: Self) -> Self { + Self::new( + fposmod(self.x, modv.x), + fposmod(self.y, modv.y), + fposmod(self.z, modv.z), + ) + } + + pub fn project(self, b: Self) -> Self { + Self::from_glam(self.to_glam().project_onto(b.to_glam())) + } + + pub fn reflect(self, normal: Self) -> Self { + Self::from_glam(self.to_glam().reject_from(normal.to_glam())) + } + + pub fn round(self) -> Self { + Self::from_glam(self.to_glam().round()) + } + + pub fn sign(self) -> Self { + Self::new(sign(self.x), sign(self.y), sign(self.z)) + } + + pub fn signed_angle_to(self, to: Self, axis: Self) -> f32 { + let cross_to = self.cross(to); + let unsigned_angle = self.dot(to).atan2(cross_to.length()); + let sign = cross_to.dot(axis); + if sign < 0.0 { + -unsigned_angle + } else { + unsigned_angle + } + } + + pub fn slide(self, normal: Self) -> Self { + self - normal * self.dot(normal) + } + + pub fn snapped(self, step: Self) -> Self { + Self::new( + snapped(self.x, step.x), + snapped(self.y, step.y), + snapped(self.z, step.z), + ) + } } /// Formats the vector like Godot: `(x, y, z)`. From 637018399f96109028f53b4eb66147b3a20d2800 Mon Sep 17 00:00:00 2001 From: RealAstolfo Date: Mon, 16 Jan 2023 21:22:01 +0100 Subject: [PATCH 49/54] Update player.rs --- examples/dodge-the-creeps/rust/src/player.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/examples/dodge-the-creeps/rust/src/player.rs b/examples/dodge-the-creeps/rust/src/player.rs index 1dade77f7..c933ed41e 100644 --- a/examples/dodge-the-creeps/rust/src/player.rs +++ b/examples/dodge-the-creeps/rust/src/player.rs @@ -67,16 +67,16 @@ impl GodotExt for Player { // Note: exact=false by default, in Rust we have to provide it explicitly let input = Input::singleton(); if input.is_action_pressed("ui_right".into(), false) { - velocity.x += 1.0; + velocity += Vector2::RIGHT; } if input.is_action_pressed("ui_left".into(), false) { - velocity.x -= 1.0; + velocity += Vector2::LEFT; } if input.is_action_pressed("ui_down".into(), false) { - velocity.y += 1.0; + velocity += Vector2::DOWN; } if input.is_action_pressed("ui_up".into(), false) { - velocity.y -= 1.0; + velocity += Vector2::UP; } if velocity.length() > 0.0 { From dd7ee64c563222f1be5c77010e2d2691d7e88bbc Mon Sep 17 00:00:00 2001 From: Thomas ten Cate Date: Tue, 31 Jan 2023 20:50:30 +0100 Subject: [PATCH 50/54] Add a simple test runner in GDScript --- .gitignore | 13 +-- examples/dodge-the-creeps/godot/.gitignore | 3 +- .../.godot/global_script_class_cache.cfg | 13 +++ itest/godot/ManualFfiTests.gd | 49 +++-------- itest/godot/TestRunner.gd | 87 +++++++++++++++---- itest/godot/TestRunner.tscn | 10 +-- itest/godot/TestStats.gd | 33 +++++++ itest/godot/TestSuite.gd | 29 +++++++ itest/godot/input/GenFfiTests.template.gd | 22 ++--- itest/godot/project.godot | 5 ++ itest/rust/src/lib.rs | 2 +- 11 files changed, 179 insertions(+), 87 deletions(-) create mode 100644 itest/godot/.godot/global_script_class_cache.cfg create mode 100644 itest/godot/TestStats.gd create mode 100644 itest/godot/TestSuite.gd diff --git a/.gitignore b/.gitignore index 44fe68e0b..28bf18c16 100644 --- a/.gitignore +++ b/.gitignore @@ -7,11 +7,12 @@ target Cargo.lock -# Godot (extension list needed to run tests without prior editor opening) -.godot -godot.log -!/itest/.godot/extension_list.cfg -*.import +# Godot +**/.import/ +**/.godot/** +# Needed to run projects without having to open the editor first. +!**/.godot/extension_list.cfg +!**/.godot/global_script_class_cache.cfg # This project: input JSONs and code generation gen @@ -20,4 +21,4 @@ gen /script *.bat config.toml -exp.rs \ No newline at end of file +exp.rs diff --git a/examples/dodge-the-creeps/godot/.gitignore b/examples/dodge-the-creeps/godot/.gitignore index 1180d9f60..44155935d 100644 --- a/examples/dodge-the-creeps/godot/.gitignore +++ b/examples/dodge-the-creeps/godot/.gitignore @@ -1,2 +1,3 @@ -.import +.godot/ +.import/ logs/ diff --git a/itest/godot/.godot/global_script_class_cache.cfg b/itest/godot/.godot/global_script_class_cache.cfg new file mode 100644 index 000000000..5c07e4994 --- /dev/null +++ b/itest/godot/.godot/global_script_class_cache.cfg @@ -0,0 +1,13 @@ +list=[{ +"base": &"RefCounted", +"class": &"TestStats", +"icon": "", +"language": &"GDScript", +"path": "res://TestStats.gd" +}, { +"base": &"Node", +"class": &"TestSuite", +"icon": "", +"language": &"GDScript", +"path": "res://TestSuite.gd" +}] diff --git a/itest/godot/ManualFfiTests.gd b/itest/godot/ManualFfiTests.gd index 217fcba2d..bf1ea86d4 100644 --- a/itest/godot/ManualFfiTests.gd +++ b/itest/godot/ManualFfiTests.gd @@ -2,60 +2,39 @@ # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at https://mozilla.org/MPL/2.0/. -extends Node +extends TestSuite -func run() -> bool: - print("[GD] Test ManualFfi...") - var ok = true - ok = ok && test_missing_init() - ok = ok && test_to_string() - ok = ok && test_export() - - print("[GD] ManualFfi tested (passed=", ok, ")") - return ok - -func test_missing_init() -> bool: - return true # TODO: fix dynamic eval +func test_missing_init(): + return # TODO: fix dynamic eval var expr = Expression.new() var error = expr.parse("WithoutInit.new()") - if error != OK: - print("Failed to parse dynamic expression") - return false + if !assert_eq(error, OK, "Failed to parse dynamic expression"): + return var instance = expr.execute() - if expr.has_execute_failed(): - print("Failed to evaluate dynamic expression") - return false + if !assert_that(!expr.has_execute_failed(), "Failed to evaluate dynamic expression"): + return print("[GD] WithoutInit is: ", instance) - return true -func test_to_string() -> bool: +func test_to_string(): var ffi = VirtualMethodTest.new() - var s = str(ffi) - - print("to_string: ", s) - print("to_string: ", ffi) - return true -func test_export() -> bool: + assert_eq(str(ffi), "VirtualMethodTest[integer=0]") + +func test_export(): var obj = HasProperty.new() obj.int_val = 5 - print("[GD] HasProperty's int_val property is: ", obj.int_val, " and should be 5") - var int_val_correct = obj.int_val == 5 + assert_eq(obj.int_val, 5) obj.string_val = "test val" - print("[GD] HasProperty's string_val property is: ", obj.string_val, " and should be \"test val\"") - var string_val_correct = obj.string_val == "test val" + assert_eq(obj.string_val, "test val") var node = Node.new() obj.object_val = node - print("[GD] HasProperty's object_val property is: ", obj.object_val, " and should be ", node) - var object_val_correct = obj.object_val == node + assert_eq(obj.object_val, node) obj.free() node.free() - - return int_val_correct && string_val_correct && object_val_correct diff --git a/itest/godot/TestRunner.gd b/itest/godot/TestRunner.gd index 8881a877e..2183779c4 100644 --- a/itest/godot/TestRunner.gd +++ b/itest/godot/TestRunner.gd @@ -5,23 +5,74 @@ extends Node func _ready(): - var rust_tests := IntegrationTests.new() - - var rust = rust_tests.run() - var manual = $ManualFfiTests.run() - var generated = $GenFfiTests.run() - - var status: bool = rust && manual && generated - + var test_suites: Array = [ + IntegrationTests.new(), + preload("res://ManualFfiTests.gd").new(), + preload("res://gen/GenFfiTests.gd").new(), + ] + + var tests: Array[_Test] = [] + for suite in test_suites: + for method in suite.get_method_list(): + var method_name: String = method.name + if method_name.begins_with("test_"): + tests.push_back(_Test.new(suite, method_name)) + print() - var exit_code: int - if status: - print(" All tests PASSED.") - exit_code = 0 - else: - print(" Tests FAILED.") - exit_code = 1 - - print(" -- exiting.") - rust_tests.free() + print_rich(" [b][color=green]Running[/color][/b] test project %s" % [ + ProjectSettings.get_setting("application/config/name", ""), + ]) + print() + + var stats: TestStats = TestStats.new() + stats.start_stopwatch() + for test in tests: + printraw(" -- %s ... " % [test.test_name]) + var ok: bool = test.run() + print_rich("[color=green]ok[/color]" if ok else "[color=red]FAILED[/color]") + stats.add(ok) + stats.stop_stopwatch() + + print() + print_rich("test result: %s. %d passed; %d failed; finished in %.2fs" % [ + "[color=green]ok[/color]" if stats.all_passed() else "[color=red]FAILED[/color]", + stats.num_ok, + stats.num_failed, + stats.runtime_seconds(), + ]) + print() + + for suite in test_suites: + suite.free() + + var exit_code: int = 0 if stats.all_passed() else 1 get_tree().quit(exit_code) + +class _Test: + var suite: Object + var method_name: String + var test_name: String + + func _init(suite: Object, method_name: String): + self.suite = suite + self.method_name = method_name + self.test_name = "%s::%s" % [_suite_name(suite), method_name] + + func run(): + # This is a no-op if the suite doesn't have this property. + suite.set("_assertion_failed", false) + var result = suite.call(method_name) + var ok: bool = ( + (result == true or result == null) + && !suite.get("_assertion_failed") + ) + return ok + + static func _suite_name(suite: Object) -> String: + var script := suite.get_script() + if script: + # Test suite written in GDScript. + return script.resource_path.get_file().get_basename() + else: + # Test suite written in Rust. + return suite.get_class() diff --git a/itest/godot/TestRunner.tscn b/itest/godot/TestRunner.tscn index 3e11d6577..dbd5cd740 100644 --- a/itest/godot/TestRunner.tscn +++ b/itest/godot/TestRunner.tscn @@ -1,14 +1,6 @@ -[gd_scene load_steps=4 format=3 uid="uid://dgcj68l8n6wpb"] +[gd_scene load_steps=2 format=3 uid="uid://dgcj68l8n6wpb"] [ext_resource type="Script" path="res://TestRunner.gd" id="1_wdbrf"] -[ext_resource type="Script" path="res://ManualFfiTests.gd" id="2_gmtlc"] -[ext_resource type="Script" path="res://gen/GenFfiTests.gd" id="3_vivae"] [node name="TestRunner" type="Node"] script = ExtResource("1_wdbrf") - -[node name="ManualFfiTests" type="Node" parent="."] -script = ExtResource("2_gmtlc") - -[node name="GenFfiTests" type="Node" parent="."] -script = ExtResource("3_vivae") diff --git a/itest/godot/TestStats.gd b/itest/godot/TestStats.gd new file mode 100644 index 000000000..c6804d27d --- /dev/null +++ b/itest/godot/TestStats.gd @@ -0,0 +1,33 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. + +class_name TestStats +extends RefCounted + +var num_run := 0 +var num_ok := 0 +var num_failed := 0 + +var _start_time_usec := 0 +var _runtime_usec := 0 + +func add(ok: bool): + num_run += 1 + if ok: + num_ok += 1 + else: + num_failed += 1 + +func all_passed() -> bool: + # Consider 0 tests run as a failure too, because it's probably a problem with the run itself. + return num_failed == 0 && num_run > 0 + +func start_stopwatch(): + _start_time_usec = Time.get_ticks_usec() + +func stop_stopwatch(): + _runtime_usec += Time.get_ticks_usec() - _start_time_usec + +func runtime_seconds() -> float: + return _runtime_usec * 1.0e-6 diff --git a/itest/godot/TestSuite.gd b/itest/godot/TestSuite.gd new file mode 100644 index 000000000..88f3a7c82 --- /dev/null +++ b/itest/godot/TestSuite.gd @@ -0,0 +1,29 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. + +class_name TestSuite +extends Node + +var _assertion_failed: bool = false + +## Asserts that `what` is `true`, but does not abort the test. Returns `what` so you can return +## early from the test function if the assertion failed. +func assert_that(what: bool, message: String = "") -> bool: + if !what: + _assertion_failed = true + if message: + print("assertion failed: %s" % message) + else: + print("assertion failed") + return what + +func assert_eq(left, right, message: String = "") -> bool: + if left != right: + _assertion_failed = true + if message: + print("assertion failed: %s\n left: %s\n right: %s" % [message, left, right]) + else: + print("assertion failed: `(left == right)`\n left: %s\n right: %s" % [left, right]) + return false + return true diff --git a/itest/godot/input/GenFfiTests.template.gd b/itest/godot/input/GenFfiTests.template.gd index 507f99982..91ee508f9 100644 --- a/itest/godot/input/GenFfiTests.template.gd +++ b/itest/godot/input/GenFfiTests.template.gd @@ -2,28 +2,16 @@ # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at https://mozilla.org/MPL/2.0/. -extends Node +extends TestSuite -func run() -> bool: +#( +func test_varcall_IDENT(): var ffi = GenFfi.new() - print("[GD] GenFfi constructed: ", ffi.get_instance_id()) - var ok := true#( - if !test_varcall_IDENT(ffi): - ok = false - push_error(" -- FFI test failed: test_varcall_IDENT") - #) - - print("[GD] GenFfi destructing...") - return ok -#( -func test_varcall_IDENT(ffi: GenFfi) -> bool: var from_rust = ffi.return_IDENT() - var ok1: bool = ffi.accept_IDENT(from_rust) + assert_that(ffi.accept_IDENT(from_rust)) var from_gdscript = VAL var mirrored = ffi.mirror_IDENT(from_gdscript) - var ok2: bool = (mirrored == from_gdscript) - - return ok1 && ok2 + assert_eq(mirrored, from_gdscript) #) diff --git a/itest/godot/project.godot b/itest/godot/project.godot index d2c4c01ed..ffccee723 100644 --- a/itest/godot/project.godot +++ b/itest/godot/project.godot @@ -13,3 +13,8 @@ config_version=5 config/name="IntegrationTests" run/main_scene="res://TestRunner.tscn" config/features=PackedStringArray("4.0") +run/flush_stdout_on_print=true + +[debug] + +gdscript/warnings/shadowed_variable=0 diff --git a/itest/rust/src/lib.rs b/itest/rust/src/lib.rs index 7d6dfb1d7..94e65f266 100644 --- a/itest/rust/src/lib.rs +++ b/itest/rust/src/lib.rs @@ -61,7 +61,7 @@ struct IntegrationTests {} #[godot_api] impl IntegrationTests { #[func] - fn run(&mut self) -> bool { + fn test_all(&mut self) -> bool { println!("Run Godot integration tests..."); run_tests() } From bcad368a4d3ab4a6f704b0aa813b12e18ec53277 Mon Sep 17 00:00:00 2001 From: Jan Haller Date: Sat, 4 Feb 2023 14:58:17 +0100 Subject: [PATCH 51/54] Elevate `clippy::suspicious` and `warnings` (both default warn) to deny level --- .github/workflows/full-ci.yml | 7 ++++--- .github/workflows/minimal-ci.yml | 6 ++++-- check.sh | 2 +- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/.github/workflows/full-ci.yml b/.github/workflows/full-ci.yml index 789a5f3ca..5d83d10ec 100644 --- a/.github/workflows/full-ci.yml +++ b/.github/workflows/full-ci.yml @@ -14,7 +14,7 @@ on: - trying env: - GDEXT_FEATURES: 'godot-core/convenience' + GDEXT_FEATURES: '--features godot-core/convenience' # GDEXT_CRATE_ARGS: '-p godot-codegen -p godot-ffi -p godot-core -p godot-macros -p godot' defaults: @@ -59,8 +59,9 @@ jobs: binary-filename: godot.linuxbsd.editor.dev.x86_64 - name: "Check clippy" - run: cargo clippy -- -D clippy::style -D clippy::complexity -D clippy::perf -D clippy::dbg_macro -D clippy::todo -D clippy::unimplemented - + run: | + cargo clippy --all-targets $GDEXT_FEATURES -- -D clippy::suspicious -D clippy::style -D clippy::complexity -D clippy::perf \ + -D clippy::dbg_macro -D clippy::todo -D clippy::unimplemented -D warnings unit-test: name: unit-test (${{ matrix.name }}${{ matrix.rust-special }}) diff --git a/.github/workflows/minimal-ci.yml b/.github/workflows/minimal-ci.yml index 0997e9724..4646248ce 100644 --- a/.github/workflows/minimal-ci.yml +++ b/.github/workflows/minimal-ci.yml @@ -14,7 +14,7 @@ on: - master env: - GDEXT_FEATURES: 'godot-core/convenience' + GDEXT_FEATURES: '--features godot-core/convenience' # GDEXT_CRATE_ARGS: '-p godot-codegen -p godot-ffi -p godot-core -p godot-macros -p godot' defaults: @@ -59,7 +59,9 @@ jobs: binary-filename: godot.linuxbsd.editor.dev.x86_64 - name: "Check clippy" - run: cargo clippy -- -D clippy::style -D clippy::complexity -D clippy::perf -D clippy::dbg_macro -D clippy::todo -D clippy::unimplemented + run: | + cargo clippy --all-targets $GDEXT_FEATURES -- -D clippy::suspicious -D clippy::style -D clippy::complexity -D clippy::perf \ + -D clippy::dbg_macro -D clippy::todo -D clippy::unimplemented -D warnings unit-test: diff --git a/check.sh b/check.sh index 69cbdb309..e7aed976b 100755 --- a/check.sh +++ b/check.sh @@ -76,7 +76,7 @@ for arg in "${args[@]}"; do cmds+=("cargo fmt --all -- --check") ;; clippy) - cmds+=("cargo clippy $features -- -D clippy::style -D clippy::complexity -D clippy::perf -D clippy::dbg_macro -D clippy::todo -D clippy::unimplemented -D warnings") + cmds+=("cargo clippy $features -- -D clippy::suspicious -D clippy::style -D clippy::complexity -D clippy::perf -D clippy::dbg_macro -D clippy::todo -D clippy::unimplemented -D warnings") ;; test) cmds+=("cargo test $features") From 5ea54479876cccfc68030cca654a63f6ca813011 Mon Sep 17 00:00:00 2001 From: Jan Haller Date: Sat, 4 Feb 2023 14:58:59 +0100 Subject: [PATCH 52/54] Smaller fixes for clippy and doctest --- godot-core/src/builtin/dictionary.rs | 4 ++-- godot-ffi/src/godot_ffi.rs | 18 ++++++++++++++++-- godot/src/lib.rs | 1 + 3 files changed, 19 insertions(+), 4 deletions(-) diff --git a/godot-core/src/builtin/dictionary.rs b/godot-core/src/builtin/dictionary.rs index ddc05fba7..273d1829b 100644 --- a/godot-core/src/builtin/dictionary.rs +++ b/godot-core/src/builtin/dictionary.rs @@ -328,7 +328,7 @@ impl Share for Dictionary { /// /// Example /// ```no_run -/// use godot::builtin::dict; +/// use godot::builtin::{dict, Variant}; /// /// let key = "my_key"; /// let d = dict! { @@ -336,7 +336,7 @@ impl Share for Dictionary { /// "another": Variant::nil(), /// key: true, /// (1 + 2): "final", -/// } +/// }; /// ``` #[macro_export] macro_rules! dict { diff --git a/godot-ffi/src/godot_ffi.rs b/godot-ffi/src/godot_ffi.rs index 96effeaab..6d3449860 100644 --- a/godot-ffi/src/godot_ffi.rs +++ b/godot-ffi/src/godot_ffi.rs @@ -8,12 +8,19 @@ use crate as sys; use std::fmt::Debug; /// Adds methods to convert from and to Godot FFI pointers. -#[doc(hidden)] +/// See [crate::ffi_methods] for ergonomic implementation. pub trait GodotFfi { /// Construct from Godot opaque pointer. + /// + /// # Safety + /// `ptr` must be a valid _type ptr_: it must follow Godot's convention to encode `Self`, + /// which is different depending on the type. unsafe fn from_sys(ptr: sys::GDExtensionTypePtr) -> Self; - /// Construct uninitialized opaque data, then initialize it with `init` function. + /// Construct uninitialized opaque data, then initialize it with `init_fn` function. + /// + /// # Safety + /// `init_fn` must be a function that correctly handles a (possibly-uninitialized) _type ptr_. unsafe fn from_sys_init(init_fn: impl FnOnce(sys::GDExtensionTypePtr)) -> Self; /// Return Godot opaque pointer, for an immutable operation. @@ -38,6 +45,11 @@ pub trait GodotFfi { self.sys() } + /// Write the contents of `self` into the pointer `dst`. + /// + /// # Safety + /// `dst` must be a valid _type ptr_: it must follow Godot's convention to encode `Self`, + /// which is different depending on the type. unsafe fn write_sys(&self, dst: sys::GDExtensionTypePtr); } @@ -66,6 +78,7 @@ pub trait GodotFuncMarshal: Sized { // See doc comment of `ffi_methods!` for information #[macro_export] +#[doc(hidden)] macro_rules! ffi_methods_one { // type $Ptr = *mut Opaque (OpaquePtr $Ptr:ty; $( #[$attr:meta] )? $vis:vis $from_sys:ident = from_sys) => { @@ -159,6 +172,7 @@ macro_rules! ffi_methods_one { } #[macro_export] +#[doc(hidden)] macro_rules! ffi_methods_rest { ( // impl T: each method has a custom name and is annotated with 'pub' $Impl:ident $Ptr:ty; $( fn $user_fn:ident = $sys_fn:ident; )* diff --git a/godot/src/lib.rs b/godot/src/lib.rs index 94e46fbac..16207cf68 100644 --- a/godot/src/lib.rs +++ b/godot/src/lib.rs @@ -99,6 +99,7 @@ pub use godot_core::private; /// Often-imported symbols. pub mod prelude { pub use super::bind::{godot_api, GodotClass, GodotExt}; + pub use super::builtin::dict; // Re-export macros. pub use super::builtin::*; pub use super::engine::{ load, try_load, utilities, AudioStreamPlayer, Camera2D, Camera3D, Input, Node, Node2D, From 93b82010b6a00d0df28b722693cc9a83b733e5dc Mon Sep 17 00:00:00 2001 From: Jan Haller Date: Sat, 4 Feb 2023 15:07:12 +0100 Subject: [PATCH 53/54] Add integration tests for Godot with address sanitizers Currently allowed to fail, until memory errors are fixed. --- .github/workflows/full-ci.yml | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/.github/workflows/full-ci.yml b/.github/workflows/full-ci.yml index 5d83d10ec..6650769fe 100644 --- a/.github/workflows/full-ci.yml +++ b/.github/workflows/full-ci.yml @@ -63,6 +63,7 @@ jobs: cargo clippy --all-targets $GDEXT_FEATURES -- -D clippy::suspicious -D clippy::style -D clippy::complexity -D clippy::perf \ -D clippy::dbg_macro -D clippy::todo -D clippy::unimplemented -D warnings + unit-test: name: unit-test (${{ matrix.name }}${{ matrix.rust-special }}) runs-on: ${{ matrix.os }} @@ -127,7 +128,7 @@ jobs: binary-filename: ${{ matrix.godot-binary }} - name: "Compile tests" - run: cargo test --no-run + run: cargo test $GDEXT_FEATURES --no-run - name: "Test" run: cargo test $GDEXT_FEATURES @@ -136,7 +137,8 @@ jobs: godot-itest: name: godot-itest (${{ matrix.name }}) runs-on: ${{ matrix.os }} - continue-on-error: false + # TODO: continue-on-error: false, as soon as memory errors are fixed + continue-on-error: ${{ contains(matrix.name, 'memcheck') }} timeout-minutes: 24 strategy: fail-fast: false # cancel all jobs as soon as one fails? @@ -162,6 +164,16 @@ jobs: rust-toolchain: stable godot-binary: godot.linuxbsd.editor.dev.x86_64 + - name: linux-memcheck-gcc + os: ubuntu-20.04 + rust-toolchain: stable + godot-binary: godot.linuxbsd.editor.dev.x86_64.san + + - name: linux-memcheck-clang + os: ubuntu-20.04 + rust-toolchain: stable + godot-binary: godot.linuxbsd.editor.dev.x86_64.llvm.san + steps: - uses: actions/checkout@v3 From ac02be1a4692445a980f646e97e4c0f020bdbfa4 Mon Sep 17 00:00:00 2001 From: Jan Haller Date: Sat, 4 Feb 2023 22:09:06 +0100 Subject: [PATCH 54/54] Explicit Gd initialization as strong/weak ref; fix refcount in Variant <-> Object conversion Changes: * Gd::from_obj_sys() now always initializes a strong ref (the default); previously needed .ready() * Gd::from_obj_sys_weak() for the rare cases where refcount is not incremented * Fixes several cases where accidental weak initialization was used * Also remove mem::forget() for ptrcall return values for now; consider explicit inc_ref later * Unit tests for Variant <-> Object conversions with ref counts --- godot-core/src/builtin/meta/signature.rs | 4 +- godot-core/src/macros.rs | 6 +-- godot-core/src/obj/base.rs | 3 +- godot-core/src/obj/gd.rs | 63 ++++++++++++++---------- itest/rust/src/object_test.rs | 40 +++++++++++++++ itest/rust/src/variant_test.rs | 1 + 6 files changed, 84 insertions(+), 33 deletions(-) diff --git a/godot-core/src/builtin/meta/signature.rs b/godot-core/src/builtin/meta/signature.rs index 8ba1b5092..415f8f1d4 100644 --- a/godot-core/src/builtin/meta/signature.rs +++ b/godot-core/src/builtin/meta/signature.rs @@ -162,8 +162,8 @@ macro_rules! impl_signature_for_tuple { unsafe { <$R as sys::GodotFuncMarshal>::try_write_sys(&ret_val, ret) } .unwrap_or_else(|e| return_error::<$R>(method_name, &e)); - // FIXME should be inc_ref instead of forget - std::mem::forget(ret_val); + // FIXME is inc_ref needed here? + // std::mem::forget(ret_val); } } }; diff --git a/godot-core/src/macros.rs b/godot-core/src/macros.rs index d277cd10a..4d8cdd55f 100644 --- a/godot-core/src/macros.rs +++ b/godot-core/src/macros.rs @@ -413,8 +413,8 @@ macro_rules! gdext_ptrcall { )*); <$($RetTy)+ as sys::GodotFfi>::write_sys(&ret_val, $ret); - // FIXME should be inc_ref instead of forget - #[allow(clippy::forget_copy)] - std::mem::forget(ret_val); + // FIXME is inc_ref needed here? + // #[allow(clippy::forget_copy)] + // std::mem::forget(ret_val); }; } diff --git a/godot-core/src/obj/base.rs b/godot-core/src/obj/base.rs index 698b0cce1..4844d4c25 100644 --- a/godot-core/src/obj/base.rs +++ b/godot-core/src/obj/base.rs @@ -39,7 +39,8 @@ impl Base { pub(crate) unsafe fn from_sys(base_ptr: sys::GDExtensionObjectPtr) -> Self { assert!(!base_ptr.is_null(), "instance base is null pointer"); - let obj = Gd::from_obj_sys(base_ptr); + // Initialize only as weak pointer (don't increment reference count) + let obj = Gd::from_obj_sys_weak(base_ptr); // This obj does not contribute to the strong count, otherwise we create a reference cycle: // 1. RefCounted (dropped in GDScript) diff --git a/godot-core/src/obj/gd.rs b/godot-core/src/obj/gd.rs index b9f472711..700396cf0 100644 --- a/godot-core/src/obj/gd.rs +++ b/godot-core/src/obj/gd.rs @@ -101,13 +101,10 @@ where T::Mem::maybe_init_ref(&result); result*/ - let result = unsafe { + unsafe { let object_ptr = callbacks::create::(ptr::null_mut()); Gd::from_obj_sys(object_ptr) - }; - - T::Mem::maybe_init_ref(&result); - result + } } // FIXME use ```no_run instead of ```ignore, as soon as unit test #[cfg] mess is cleaned up @@ -138,10 +135,7 @@ where F: FnOnce(crate::obj::Base) -> T, { let object_ptr = callbacks::create_custom(init); - let result = unsafe { Gd::from_obj_sys(object_ptr) }; - - T::Mem::maybe_init_ref(&result); - result + unsafe { Gd::from_obj_sys(object_ptr) } } /// Hands out a guard for a shared borrow, through which the user instance can be read. @@ -210,7 +204,7 @@ impl Gd { None } else { // SAFETY: assumes that the returned GDExtensionObjectPtr is convertible to Object* (i.e. C++ upcast doesn't modify the pointer) - let untyped = unsafe { Gd::::from_obj_sys(ptr).ready() }; + let untyped = unsafe { Gd::::from_obj_sys(ptr) }; untyped.owned_cast::().ok() } } @@ -277,14 +271,6 @@ impl Gd { } } - /// Needed to initialize ref count -- must be explicitly invoked. - /// - /// Could be made part of FFI methods, but there are some edge cases where this is not intended. - pub(crate) fn ready(self) -> Self { - T::Mem::maybe_inc_ref(&self); - self - } - /// **Upcast:** convert into a smart pointer to a base class. Always succeeds. /// /// Moves out of this value. If you want to create _another_ smart pointer instance, @@ -366,7 +352,8 @@ impl Gd { let class_tag = interface_fn!(classdb_get_class_tag)(class_name.string_sys()); let cast_object_ptr = interface_fn!(object_cast_to)(self.obj_sys(), class_tag); - sys::ptr_then(cast_object_ptr, |ptr| Gd::from_obj_sys(ptr)) + // Create weak object, as ownership will be moved and reference-counter stays the same + sys::ptr_then(cast_object_ptr, |ptr| Gd::from_obj_sys_weak(ptr)) } pub(crate) fn as_ref_counted(&self, apply: impl Fn(&mut engine::RefCounted) -> R) -> R { @@ -401,11 +388,30 @@ impl Gd { ffi_methods! { type sys::GDExtensionObjectPtr = Opaque; - fn from_obj_sys = from_sys; - fn from_obj_sys_init = from_sys_init; + fn from_obj_sys_weak = from_sys; fn obj_sys = sys; fn write_obj_sys = write_sys; } + + /// Initializes this `Gd` from the object pointer as a **strong ref**, meaning + /// it initializes/increments the reference counter and keeps the object alive. + /// + /// This is the default for most initializations from FFI. In cases where reference counter + /// should explicitly **not** be updated, [`Self::from_obj_sys_weak`] is available. + #[doc(hidden)] + pub unsafe fn from_obj_sys(ptr: sys::GDExtensionObjectPtr) -> Self { + // Initialize reference counter, if needed + Self::from_obj_sys_weak(ptr).with_inc_refcount() + } + + /// Returns `self` but with initialized ref-count. + fn with_inc_refcount(self) -> Self { + // Note: use init_ref and not inc_ref, since this might be the first reference increment. + // Godot expects RefCounted::init_ref to be called instead of RefCounted::reference in that case. + // init_ref also doesn't hurt (except 1 possibly unnecessary check). + T::Mem::maybe_init_ref(&self); + self + } } /// _The methods in this impl block are only available for objects `T` that are manually managed, @@ -501,7 +507,6 @@ impl Gd { /// # Safety /// `init_fn` must be a function that correctly handles a _type pointer_ pointing to an _object pointer_. #[doc(hidden)] - // TODO unsafe on init_fn instead of this fn? pub unsafe fn from_sys_init_opt(init_fn: impl FnOnce(sys::GDExtensionTypePtr)) -> Option { // Note: see _call_native_mb_ret_obj() in godot-cpp, which does things quite different (e.g. querying the instance binding). @@ -569,7 +574,7 @@ impl Drop for Gd { impl Share for Gd { fn share(&self) -> Self { out!("Gd::share"); - Self::from_opaque(self.opaque).ready() + Self::from_opaque(self.opaque).with_inc_refcount() } } @@ -579,19 +584,23 @@ impl Share for Gd { impl FromVariant for Gd { fn try_from_variant(variant: &Variant) -> Result { let result = unsafe { - let result = Self::from_sys_init(|self_ptr| { + Self::from_sys_init(|self_ptr| { let converter = sys::builtin_fn!(object_from_variant); converter(self_ptr, variant.var_sys()); - }); - result.ready() + }) }; - Ok(result) + // The conversion method `variant_to_object` does NOT increment the reference-count of the object; we need to do that manually. + // (This behaves differently in the opposite direction `object_to_variant`.) + Ok(result.with_inc_refcount()) } } impl ToVariant for Gd { fn to_variant(&self) -> Variant { + // The conversion method `object_to_variant` DOES increment the reference-count of the object; so nothing to do here. + // (This behaves differently in the opposite direction `variant_to_object`.) + unsafe { Variant::from_var_sys_init(|variant_ptr| { let converter = sys::builtin_fn!(object_to_variant); diff --git a/itest/rust/src/object_test.rs b/itest/rust/src/object_test.rs index 148926fff..6dadd0c9e 100644 --- a/itest/rust/src/object_test.rs +++ b/itest/rust/src/object_test.rs @@ -36,6 +36,8 @@ pub fn run() -> bool { ok &= object_from_instance_id_unrelated_type(); ok &= object_user_convert_variant(); ok &= object_engine_convert_variant(); + ok &= object_user_convert_variant_refcount(); + ok &= object_engine_convert_variant_refcount(); ok &= object_engine_up_deref(); ok &= object_engine_up_deref_mut(); ok &= object_engine_upcast(); @@ -225,6 +227,44 @@ fn object_engine_convert_variant() { obj.free(); } +#[itest] +fn object_user_convert_variant_refcount() { + let obj: Gd = Gd::new(ObjPayload { value: -22222 }); + let obj = obj.upcast::(); + check_convert_variant_refcount(obj) +} + +#[itest] +fn object_engine_convert_variant_refcount() { + let obj = RefCounted::new(); + check_convert_variant_refcount(obj); +} + +/// Converts between Object <-> Variant and verifies the reference counter at each stage. +fn check_convert_variant_refcount(obj: Gd) { + // Freshly created -> refcount 1 + assert_eq!(obj.get_reference_count(), 1); + + { + // Variant created from object -> increment + let variant = obj.to_variant(); + assert_eq!(obj.get_reference_count(), 2); + + { + // Yet another object created *from* variant -> increment + let another_object = variant.to::>(); + assert_eq!(obj.get_reference_count(), 3); + assert_eq!(another_object.get_reference_count(), 3); + } + + // `another_object` destroyed -> decrement + assert_eq!(obj.get_reference_count(), 2); + } + + // `variant` destroyed -> decrement + assert_eq!(obj.get_reference_count(), 1); +} + #[itest] fn object_engine_up_deref() { let node3d: Gd = Node3D::new_alloc(); diff --git a/itest/rust/src/variant_test.rs b/itest/rust/src/variant_test.rs index d996fc68c..8d4cbafb3 100644 --- a/itest/rust/src/variant_test.rs +++ b/itest/rust/src/variant_test.rs @@ -287,6 +287,7 @@ fn variant_type_correct() { VariantType::Dictionary ); } + // ---------------------------------------------------------------------------------------------------------------------------------------------- fn roundtrip(value: T)