From 9fa4ea592b5d30d9160b1daf3b31519a62bd6319 Mon Sep 17 00:00:00 2001 From: Pedro Ciambra Date: Fri, 6 Jan 2023 22:03:55 -0300 Subject: [PATCH 01/29] 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/29] 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/29] 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/29] 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/29] 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/29] 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/29] 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/29] 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/29] 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/29] 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/29] 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/29] 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/29] 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/29] 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/29] 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/29] 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/29] 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/29] 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/29] 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/29] 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/29] 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/29] 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/29] 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/29] 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/29] 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/29] 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/29] 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/29] 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/29] 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! {