From e420f5f910443d4b64c2f4856d29ec47d3aa85b7 Mon Sep 17 00:00:00 2001 From: Mivort Date: Tue, 30 Sep 2025 01:23:22 +0100 Subject: [PATCH 01/54] Provide error context for typed array clone check This patch adds output of `ConvertError`'s `Display` to debug-only check's panic message which provides additional context when type mismatch happens. Panic message would include the intended type name and what was given instead of it. The output would look roughly like this: ``` copied array should have same type as original array: expected array of type Builtin(DICTIONARY), got Untyped: [] ``` --- godot-core/src/builtin/collections/array.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/godot-core/src/builtin/collections/array.rs b/godot-core/src/builtin/collections/array.rs index cdbd5ba5d..111feda91 100644 --- a/godot-core/src/builtin/collections/array.rs +++ b/godot-core/src/builtin/collections/array.rs @@ -1245,8 +1245,9 @@ impl Clone for Array { // Double-check copy's runtime type in Debug mode. if cfg!(debug_assertions) { - copy.with_checked_type() - .expect("copied array should have same type as original array") + copy.with_checked_type().unwrap_or_else(|e| { + panic!("copied array should have same type as original array: {e}") + }) } else { copy } From fe94254f2e0f8028ca0e064528aeef5f6937cb2f Mon Sep 17 00:00:00 2001 From: Houtamelo Date: Fri, 3 Oct 2025 19:21:16 -0300 Subject: [PATCH 02/54] Fix secondary api blocks not registering docs. --- godot-core/src/docs.rs | 88 +++++++++++-------- godot-core/src/private.rs | 16 ++++ godot-core/src/registry/class.rs | 5 +- godot-core/src/registry/plugin.rs | 10 +-- .../src/class/data_models/inherent_impl.rs | 46 +++++++--- godot-macros/src/docs.rs | 28 +++--- 6 files changed, 120 insertions(+), 73 deletions(-) diff --git a/godot-core/src/docs.rs b/godot-core/src/docs.rs index beaba7219..ef597cd1f 100644 --- a/godot-core/src/docs.rs +++ b/godot-core/src/docs.rs @@ -48,16 +48,16 @@ pub struct StructDocs { /// All fields are XML parts, escaped where necessary. #[derive(Default, Copy, Clone, Debug)] pub struct InherentImplDocs { - pub methods: &'static str, - pub signals_block: &'static str, - pub constants_block: &'static str, + pub methods: Vec<&'static str>, + pub signals: Vec<&'static str>, + pub constants: Vec<&'static str>, } #[derive(Default)] struct DocPieces { definition: StructDocs, inherent: InherentImplDocs, - virtual_methods: &'static str, + virtual_methods: Vec<&'static str>, } /// This function scours the registered plugins to find their documentation pieces, @@ -79,18 +79,21 @@ pub fn gather_xml_docs() -> impl Iterator { crate::private::iterate_plugins(|x| { let class_name = x.class_name; - match x.item { + match &x.item { PluginItem::InherentImpl(InherentImpl { docs, .. }) => { - map.entry(class_name).or_default().inherent = docs + map.entry(class_name).or_default().inherent = docs.clone(); } - PluginItem::ITraitImpl(ITraitImpl { virtual_method_docs, .. - }) => map.entry(class_name).or_default().virtual_methods = virtual_method_docs, + }) => map + .entry(class_name) + .or_default() + .virtual_methods + .push(virtual_method_docs), PluginItem::Struct(Struct { docs, .. }) => { - map.entry(class_name).or_default().definition = docs + map.entry(class_name).or_default().definition = *docs; } _ => (), @@ -98,32 +101,43 @@ pub fn gather_xml_docs() -> impl Iterator { }); map.into_iter().map(|(class, pieces)| { - let StructDocs { - base, - description, - experimental, - deprecated, - members, - } = pieces.definition; - - let InherentImplDocs { - methods, - signals_block, - constants_block, - } = pieces.inherent; - - let virtual_methods = pieces.virtual_methods; - let methods_block = (virtual_methods.is_empty() && methods.is_empty()) - .then(String::new) - .unwrap_or_else(|| format!("{methods}{virtual_methods}")); - - let (brief, description) = match description - .split_once("[br]") { - Some((brief, description)) => (brief, description.trim_start_matches("[br]")), - None => (description, ""), - }; - - format!(r#" + let StructDocs { + base, + description, + experimental, + deprecated, + members, + } = pieces.definition; + + let virtual_methods = pieces.virtual_methods; + + let mut method_docs = String::from_iter(pieces.inherent.methods); + let signal_docs = String::from_iter(pieces.inherent.signals); + let constant_docs = String::from_iter(pieces.inherent.constants); + + method_docs.extend(virtual_methods); + let methods_block = if method_docs.is_empty() { + String::new() + } else { + format!("{method_docs}") + }; + let signals_block = if signal_docs.is_empty() { + String::new() + } else { + format!("{signal_docs}") + }; + let constants_block = if constant_docs.is_empty() { + String::new() + } else { + format!("{constant_docs}") + }; + let (brief, description) = match description + .split_once("[br]") { + Some((brief, description)) => (brief, description.trim_start_matches("[br]")), + None => (description, ""), + }; + + format!(r#" {brief} {description} @@ -132,8 +146,8 @@ pub fn gather_xml_docs() -> impl Iterator { {signals_block} {members} "#) - }, - ) + }, + ) } /// # Safety diff --git a/godot-core/src/private.rs b/godot-core/src/private.rs index 74aaed0ef..fbaa54555 100644 --- a/godot-core/src/private.rs +++ b/godot-core/src/private.rs @@ -142,6 +142,22 @@ pub fn next_class_id() -> u16 { NEXT_CLASS_ID.fetch_add(1, atomic::Ordering::Relaxed) } +// Don't touch unless you know what you're doing. +#[doc(hidden)] +pub fn edit_inherent_impl(class_name: crate::meta::ClassName, f: impl FnOnce(&mut InherentImpl)) { + let mut plugins = __GODOT_PLUGIN_REGISTRY.lock().unwrap(); + + for elem in plugins.iter_mut().filter(|p| p.class_name == class_name) { + match &mut elem.item { + PluginItem::InherentImpl(inherent_impl) => { + f(inherent_impl); + return; + } + PluginItem::Struct(_) | PluginItem::ITraitImpl(_) | PluginItem::DynTraitImpl(_) => {} + } + } +} + pub(crate) fn iterate_plugins(mut visitor: impl FnMut(&ClassPlugin)) { sys::plugin_foreach!(__GODOT_PLUGIN_REGISTRY; visitor); } diff --git a/godot-core/src/registry/class.rs b/godot-core/src/registry/class.rs index 4a13622d5..d9335745d 100644 --- a/godot-core/src/registry/class.rs +++ b/godot-core/src/registry/class.rs @@ -115,7 +115,10 @@ impl ClassRegistrationInfo { // Note: when changing this match, make sure the array has sufficient size. let index = match item { PluginItem::Struct { .. } => 0, - PluginItem::InherentImpl(_) => 1, + PluginItem::InherentImpl(_) => { + // Inherent impls don't need to be unique. + return; + } PluginItem::ITraitImpl { .. } => 2, // Multiple dyn traits can be registered, thus don't validate for uniqueness. diff --git a/godot-core/src/registry/plugin.rs b/godot-core/src/registry/plugin.rs index 7d2ad995f..614828f0c 100644 --- a/godot-core/src/registry/plugin.rs +++ b/godot-core/src/registry/plugin.rs @@ -39,10 +39,10 @@ pub struct ClassPlugin { /// Incorrectly setting this value should not cause any UB but will likely cause errors during registration time. // Init-level is per ClassPlugin and not per PluginItem, because all components of all classes are mixed together in one // huge linker list. There is no per-class aggregation going on, so this allows to easily filter relevant classes. - pub(crate) init_level: InitLevel, + pub init_level: InitLevel, /// The actual item being registered. - pub(crate) item: PluginItem, + pub item: PluginItem, } impl ClassPlugin { @@ -300,9 +300,7 @@ pub struct InherentImpl { } impl InherentImpl { - pub fn new( - #[cfg(all(since_api = "4.3", feature = "register-docs"))] docs: InherentImplDocs, - ) -> Self { + pub fn new() -> Self { Self { register_methods_constants_fn: ErasedRegisterFn { raw: callbacks::register_user_methods_constants::, @@ -311,7 +309,7 @@ impl InherentImpl { raw: callbacks::register_user_rpcs::, }), #[cfg(all(since_api = "4.3", feature = "register-docs"))] - docs, + docs: Default::default(), } } } diff --git a/godot-macros/src/class/data_models/inherent_impl.rs b/godot-macros/src/class/data_models/inherent_impl.rs index 63f988ad5..53c2ade94 100644 --- a/godot-macros/src/class/data_models/inherent_impl.rs +++ b/godot-macros/src/class/data_models/inherent_impl.rs @@ -93,8 +93,6 @@ pub fn transform_inherent_impl( #[cfg(all(feature = "register-docs", since_api = "4.3"))] let docs = crate::docs::document_inherent_impl(&funcs, &consts, &signals); - #[cfg(not(all(feature = "register-docs", since_api = "4.3")))] - let docs = quote! {}; // Container struct holding names of all registered #[func]s. // The struct is declared by #[derive(GodotClass)]. @@ -127,17 +125,41 @@ pub fn transform_inherent_impl( let method_storage_name = format_ident!("__registration_methods_{class_name}"); let constants_storage_name = format_ident!("__registration_constants_{class_name}"); - let fill_storage = quote! { - ::godot::sys::plugin_execute_pre_main!({ - #method_storage_name.lock().unwrap().push(|| { - #( #method_registrations )* - #( #signal_registrations )* - }); + let fill_storage = { + #[cfg(all(feature = "register-docs", since_api = "4.3"))] + let push_docs = { + let crate::docs::InherentImplXmlDocs { + method_xml_elems, + constant_xml_elems, + signal_xml_elems, + } = docs; + + quote! { + #prv::edit_inherent_impl(#class_name_obj, |inherent_impl| { + inherent_impl.docs.methods.push(#method_xml_elems); + inherent_impl.docs.constants.push(#constant_xml_elems); + inherent_impl.docs.signals.push(#signal_xml_elems); + }); + } + }; + + #[cfg(not(all(feature = "register-docs", since_api = "4.3")))] + let push_docs = TokenStream::new(); - #constants_storage_name.lock().unwrap().push(|| { - #constant_registration + quote! { + ::godot::sys::plugin_execute_pre_main!({ + #method_storage_name.lock().unwrap().push(|| { + #( #method_registrations )* + #( #signal_registrations )* + }); + + #constants_storage_name.lock().unwrap().push(|| { + #constant_registration + }); + + #push_docs }); - }); + } }; if !meta.secondary { @@ -175,7 +197,7 @@ pub fn transform_inherent_impl( let class_registration = quote! { ::godot::sys::plugin_add!(#prv::__GODOT_PLUGIN_REGISTRY; #prv::ClassPlugin::new::<#class_name>( - #prv::PluginItem::InherentImpl(#prv::InherentImpl::new::<#class_name>(#docs)) + #prv::PluginItem::InherentImpl(#prv::InherentImpl::new::<#class_name>()) )); }; diff --git a/godot-macros/src/docs.rs b/godot-macros/src/docs.rs index de57c209b..cfdc65b0a 100644 --- a/godot-macros/src/docs.rs +++ b/godot-macros/src/docs.rs @@ -24,6 +24,12 @@ struct XmlParagraphs { deprecated_attr: String, } +pub struct InherentImplXmlDocs { + pub method_xml_elems: String, + pub constant_xml_elems: String, + pub signal_xml_elems: String, +} + /// Returns code containing the doc information of a `#[derive(GodotClass)] struct MyClass` declaration iff class or any of its members is documented. pub fn document_struct( base: String, @@ -59,39 +65,27 @@ pub fn document_inherent_impl( functions: &[FuncDefinition], constants: &[ConstDefinition], signals: &[SignalDefinition], -) -> TokenStream { - let group_xml_block = |s: String, tag: &str| -> String { - if s.is_empty() { - s - } else { - format!("<{tag}>{s}") - } - }; - +) -> InherentImplXmlDocs { let signal_xml_elems = signals .iter() .filter_map(format_signal_xml) .collect::(); - let signals_block = group_xml_block(signal_xml_elems, "signals"); let constant_xml_elems = constants .iter() .map(|ConstDefinition { raw_constant }| raw_constant) .filter_map(format_constant_xml) .collect::(); - let constants_block = group_xml_block(constant_xml_elems, "constants"); let method_xml_elems = functions .iter() .filter_map(format_method_xml) .collect::(); - quote! { - ::godot::docs::InherentImplDocs { - methods: #method_xml_elems, - signals_block: #signals_block, - constants_block: #constants_block, - } + InherentImplXmlDocs { + method_xml_elems, + constant_xml_elems, + signal_xml_elems, } } From 663535e7f5568c330f8c704b3621dd12de0db65a Mon Sep 17 00:00:00 2001 From: Luo Zhihao Date: Wed, 8 Oct 2025 12:43:59 +0800 Subject: [PATCH 03/54] Add main loop callbacks to ExtensionLibrary --- godot-core/src/init/mod.rs | 84 ++++++++++++++++--- itest/rust/src/lib.rs | 15 ++++ .../rust/src/object_tests/init_level_test.rs | 46 +++++++++- 3 files changed, 134 insertions(+), 11 deletions(-) diff --git a/godot-core/src/init/mod.rs b/godot-core/src/init/mod.rs index f1ebd978c..8aefef1cb 100644 --- a/godot-core/src/init/mod.rs +++ b/godot-core/src/init/mod.rs @@ -20,6 +20,28 @@ mod reexport_pub { } pub use reexport_pub::*; +#[repr(C)] +struct InitUserData { + library: sys::GDExtensionClassLibraryPtr, + #[cfg(since_api = "4.5")] + main_loop_callbacks: sys::GDExtensionMainLoopCallbacks, +} + +#[cfg(since_api = "4.5")] +unsafe extern "C" fn startup_func() { + E::on_main_loop_startup(); +} + +#[cfg(since_api = "4.5")] +unsafe extern "C" fn frame_func() { + E::on_main_loop_frame(); +} + +#[cfg(since_api = "4.5")] +unsafe extern "C" fn shutdown_func() { + E::on_main_loop_shutdown(); +} + #[doc(hidden)] #[deny(unsafe_op_in_unsafe_fn)] pub unsafe fn __gdext_load_library( @@ -60,10 +82,20 @@ pub unsafe fn __gdext_load_library( // Currently no way to express failure; could be exposed to E if necessary. // No early exit, unclear if Godot still requires output parameters to be set. let success = true; + // Leak the userdata. It will be dropped in core level deinitialization. + let userdata = Box::into_raw(Box::new(InitUserData { + library, + #[cfg(since_api = "4.5")] + main_loop_callbacks: sys::GDExtensionMainLoopCallbacks { + startup_func: Some(startup_func::), + frame_func: Some(frame_func::), + shutdown_func: Some(shutdown_func::), + }, + })); let godot_init_params = sys::GDExtensionInitialization { minimum_initialization_level: E::min_level().to_sys(), - userdata: std::ptr::null_mut(), + userdata: userdata.cast::(), initialize: Some(ffi_initialize_layer::), deinitialize: Some(ffi_deinitialize_layer::), }; @@ -88,13 +120,14 @@ pub unsafe fn __gdext_load_library( static LEVEL_SERVERS_CORE_LOADED: AtomicBool = AtomicBool::new(false); unsafe extern "C" fn ffi_initialize_layer( - _userdata: *mut std::ffi::c_void, + userdata: *mut std::ffi::c_void, init_level: sys::GDExtensionInitializationLevel, ) { + let userdata = userdata.cast::().as_ref().unwrap(); let level = InitLevel::from_sys(init_level); let ctx = || format!("failed to initialize GDExtension level `{level:?}`"); - fn try_load(level: InitLevel) { + fn try_load(level: InitLevel, userdata: &InitUserData) { // Workaround for https://github.com/godot-rust/gdext/issues/629: // When using editor plugins, Godot may unload all levels but only reload from Scene upward. // Manually run initialization of lower levels. @@ -102,8 +135,8 @@ unsafe extern "C" fn ffi_initialize_layer( // TODO: Remove this workaround once after the upstream issue is resolved. if level == InitLevel::Scene { if !LEVEL_SERVERS_CORE_LOADED.load(Ordering::Relaxed) { - try_load::(InitLevel::Core); - try_load::(InitLevel::Servers); + try_load::(InitLevel::Core, userdata); + try_load::(InitLevel::Servers, userdata); } } else if level == InitLevel::Core { // When it's normal initialization, the `Servers` level is normally initialized. @@ -112,18 +145,18 @@ unsafe extern "C" fn ffi_initialize_layer( // SAFETY: Godot will call this from the main thread, after `__gdext_load_library` where the library is initialized, // and only once per level. - unsafe { gdext_on_level_init(level) }; + unsafe { gdext_on_level_init(level, userdata) }; E::on_level_init(level); } // Swallow panics. TODO consider crashing if gdext init fails. let _ = crate::private::handle_panic(ctx, || { - try_load::(level); + try_load::(level, userdata); }); } unsafe extern "C" fn ffi_deinitialize_layer( - _userdata: *mut std::ffi::c_void, + userdata: *mut std::ffi::c_void, init_level: sys::GDExtensionInitializationLevel, ) { let level = InitLevel::from_sys(init_level); @@ -134,6 +167,9 @@ unsafe extern "C" fn ffi_deinitialize_layer( if level == InitLevel::Core { // Once the CORE api is unloaded, reset the flag to initial state. LEVEL_SERVERS_CORE_LOADED.store(false, Ordering::Relaxed); + + // Drop the userdata. + drop(Box::from_raw(userdata.cast::())); } E::on_level_deinit(level); @@ -149,7 +185,7 @@ unsafe extern "C" fn ffi_deinitialize_layer( /// - The interface must have been initialized. /// - Must only be called once per level. #[deny(unsafe_op_in_unsafe_fn)] -unsafe fn gdext_on_level_init(level: InitLevel) { +unsafe fn gdext_on_level_init(level: InitLevel, userdata: &InitUserData) { // TODO: in theory, a user could start a thread in one of the early levels, and run concurrent code that messes with the global state // (e.g. class registration). This would break the assumption that the load_class_method_table() calls are exclusive. // We could maybe protect globals with a mutex until initialization is complete, and then move it to a directly-accessible, read-only static. @@ -158,6 +194,15 @@ unsafe fn gdext_on_level_init(level: InitLevel) { unsafe { sys::load_class_method_table(level) }; match level { + InitLevel::Core => { + #[cfg(since_api = "4.5")] + unsafe { + sys::interface_fn!(register_main_loop_callbacks)( + userdata.library, + &raw const userdata.main_loop_callbacks, + ) + }; + } InitLevel::Servers => { // SAFETY: called from the main thread, sys::initialized has already been called. unsafe { sys::discover_main_thread() }; @@ -173,7 +218,6 @@ unsafe fn gdext_on_level_init(level: InitLevel) { crate::docs::register(); } } - _ => (), } crate::registry::class::auto_register_classes(level); @@ -303,6 +347,26 @@ pub unsafe trait ExtensionLibrary { // Nothing by default. } + /// Callback that is called after all initialization levels when Godot is fully initialized. + #[cfg(since_api = "4.5")] + fn on_main_loop_startup() { + // Nothing by default. + } + + /// Callback that is called for every process frame. + /// + /// This will run after all `_process()` methods on Node, and before `ScriptServer::frame()`. + #[cfg(since_api = "4.5")] + fn on_main_loop_frame() { + // Nothing by default. + } + + /// Callback that is called before Godot is shutdown when it is still fully initialized. + #[cfg(since_api = "4.5")] + fn on_main_loop_shutdown() { + // Nothing by default. + } + /// Whether to override the Wasm binary filename used by your GDExtension which the library should expect at runtime. Return `None` /// to use the default where gdext expects either `{YourCrate}.wasm` (default binary name emitted by Rust) or /// `{YourCrate}.threads.wasm` (for builds producing separate single-threaded and multi-threaded binaries). diff --git a/itest/rust/src/lib.rs b/itest/rust/src/lib.rs index 9ed9c0460..201977657 100644 --- a/itest/rust/src/lib.rs +++ b/itest/rust/src/lib.rs @@ -27,4 +27,19 @@ unsafe impl ExtensionLibrary for framework::IntegrationTests { fn on_level_init(level: InitLevel) { object_tests::on_level_init(level); } + + #[cfg(since_api = "4.5")] + fn on_main_loop_startup() { + object_tests::on_main_loop_startup(); + } + + #[cfg(since_api = "4.5")] + fn on_main_loop_frame() { + object_tests::on_main_loop_frame(); + } + + #[cfg(since_api = "4.5")] + fn on_main_loop_shutdown() { + object_tests::on_main_loop_shutdown(); + } } diff --git a/itest/rust/src/object_tests/init_level_test.rs b/itest/rust/src/object_tests/init_level_test.rs index fecb05308..ad4fe397c 100644 --- a/itest/rust/src/object_tests/init_level_test.rs +++ b/itest/rust/src/object_tests/init_level_test.rs @@ -7,8 +7,10 @@ use std::sync::atomic::{AtomicBool, Ordering}; +use godot::builtin::Rid; +use godot::classes::{Engine, IObject, RenderingServer}; use godot::init::InitLevel; -use godot::obj::{NewAlloc, Singleton}; +use godot::obj::{Base, GodotClass, NewAlloc, Singleton}; use godot::register::{godot_api, GodotClass}; use godot::sys::Global; @@ -120,3 +122,45 @@ fn on_init_scene() { pub fn on_init_editor() { // Nothing yet. } + +#[derive(GodotClass)] +#[class(base=Object)] +struct MainLoopCallbackSingleton { + tex: Rid, +} + +#[godot_api] +impl IObject for MainLoopCallbackSingleton { + fn init(_: Base) -> Self { + Self { + tex: RenderingServer::singleton().texture_2d_placeholder_create(), + } + } +} + +pub fn on_main_loop_startup() { + // RenderingServer should be accessible in MainLoop startup and shutdown. + let singleton = MainLoopCallbackSingleton::new_alloc(); + assert!(singleton.bind().tex.is_valid()); + Engine::singleton().register_singleton( + &MainLoopCallbackSingleton::class_id().to_string_name(), + &singleton, + ); +} + +pub fn on_main_loop_frame() { + // Nothing yet. +} + +pub fn on_main_loop_shutdown() { + let singleton = Engine::singleton() + .get_singleton(&MainLoopCallbackSingleton::class_id().to_string_name()) + .unwrap() + .cast::(); + Engine::singleton() + .unregister_singleton(&MainLoopCallbackSingleton::class_id().to_string_name()); + let tex = singleton.bind().tex; + assert!(tex.is_valid()); + RenderingServer::singleton().free_rid(tex); + singleton.free(); +} From 7187618434224f007b26ead0e73a01bb3c6f6136 Mon Sep 17 00:00:00 2001 From: Yarvin Date: Fri, 10 Oct 2025 07:10:49 +0200 Subject: [PATCH 04/54] Codegen: Support sys types in outgoing Ptrcalls. Adds support for sys types (defined in `gdextension_interface.h`) to codegen. --- godot-codegen/src/context.rs | 18 ++++ godot-codegen/src/conv/type_conversions.rs | 9 ++ godot-codegen/src/generator/central_files.rs | 4 + godot-codegen/src/generator/mod.rs | 1 + godot-codegen/src/generator/sys.rs | 82 +++++++++++++++++++ godot-codegen/src/models/domain.rs | 4 + .../special_cases/codegen_special_cases.rs | 1 + 7 files changed, 119 insertions(+) create mode 100644 godot-codegen/src/generator/sys.rs diff --git a/godot-codegen/src/context.rs b/godot-codegen/src/context.rs index 9a62fbbef..c48785604 100644 --- a/godot-codegen/src/context.rs +++ b/godot-codegen/src/context.rs @@ -12,6 +12,7 @@ use quote::{format_ident, ToTokens}; use crate::generator::method_tables::MethodTableKey; use crate::generator::notifications; +use crate::generator::sys::SYS_PARAMS; use crate::models::domain::{ArgPassing, GodotTy, RustTy, TyName}; use crate::models::json::{ JsonBuiltinClass, JsonBuiltinMethod, JsonClass, JsonClassConstant, JsonClassMethod, @@ -28,6 +29,9 @@ pub struct Context<'a> { /// Which interface traits are generated (`false` for "Godot-abstract"/final classes). classes_final: HashMap, cached_rust_types: HashMap, + /// Various pointers defined in `gdextension_interface`, for example `GDExtensionInitializationFunction`. + /// Used in some APIs that are not exposed to GDScript. + sys_types: HashSet<&'a str>, notifications_by_class: HashMap>, classes_with_signals: HashSet, notification_enum_names_by_class: HashMap, @@ -43,6 +47,8 @@ impl<'a> Context<'a> { ctx.singletons.insert(class.name.as_str()); } + Self::populate_sys_types(&mut ctx); + ctx.builtin_types.insert("Variant"); // not part of builtin_classes for builtin in api.builtin_classes.iter() { let ty_name = builtin.name.as_str(); @@ -152,6 +158,14 @@ impl<'a> Context<'a> { ctx } + /// Adds Godot pointer types to [`Context`]. + /// + /// Godot pointer types, for example `GDExtensionInitializationFunction`, are defined in `gdextension_interface` + /// but aren't described in `extension_api.json` – despite being used as parameters in various APIs. + fn populate_sys_types(ctx: &mut Context) { + ctx.sys_types.extend(SYS_PARAMS.iter().map(|p| p.type_())); + } + fn populate_notification_constants( class_name: &TyName, constants: &[JsonClassConstant], @@ -293,6 +307,10 @@ impl<'a> Context<'a> { self.native_structures_types.contains(ty_name) } + pub fn is_sys(&self, ty_name: &str) -> bool { + self.sys_types.contains(ty_name) + } + pub fn is_singleton(&self, class_name: &TyName) -> bool { self.singletons.contains(class_name.godot_ty.as_str()) } diff --git a/godot-codegen/src/conv/type_conversions.rs b/godot-codegen/src/conv/type_conversions.rs index 5e55d922b..e22c49d59 100644 --- a/godot-codegen/src/conv/type_conversions.rs +++ b/godot-codegen/src/conv/type_conversions.rs @@ -172,6 +172,15 @@ fn to_rust_type_uncached(full_ty: &GodotTy, ctx: &mut Context) -> RustTy { // .trim() is necessary here, as Godot places a space between a type and the stars when representing a double pointer. // Example: "int*" but "int **". + if ctx.is_sys(ty.trim()) { + let ty = rustify_ty(&ty); + return RustTy::RawPointer { + inner: Box::new(RustTy::SysIdent { + tokens: quote! { sys::#ty }, + }), + is_const, + }; + } let inner_type = to_rust_type(ty.trim(), None, ctx); return RustTy::RawPointer { inner: Box::new(inner_type), diff --git a/godot-codegen/src/generator/central_files.rs b/godot-codegen/src/generator/central_files.rs index 15a562e2b..8901a3336 100644 --- a/godot-codegen/src/generator/central_files.rs +++ b/godot-codegen/src/generator/central_files.rs @@ -10,6 +10,7 @@ use quote::{format_ident, quote, ToTokens}; use crate::context::Context; use crate::conv; +use crate::generator::sys::make_godotconvert_for_systypes; use crate::generator::{enums, gdext_build_struct}; use crate::models::domain::ExtensionApi; use crate::util::ident; @@ -60,6 +61,7 @@ pub fn make_core_central_code(api: &ExtensionApi, ctx: &mut Context) -> TokenStr let (global_enum_defs, global_reexported_enum_defs) = make_global_enums(api); let variant_type_traits = make_variant_type_enum(api, false); + let sys_types_godotconvert_impl = make_godotconvert_for_systypes(); // TODO impl Clone, Debug, PartialEq, PartialOrd, Hash for VariantDispatch // TODO could use try_to().unwrap_unchecked(), since type is already verified. Also directly overload from_variant(). @@ -121,6 +123,8 @@ pub fn make_core_central_code(api: &ExtensionApi, ctx: &mut Context) -> TokenStr use crate::sys; #( #global_reexported_enum_defs )* } + + #( #sys_types_godotconvert_impl )* } } diff --git a/godot-codegen/src/generator/mod.rs b/godot-codegen/src/generator/mod.rs index 15b315270..68082b711 100644 --- a/godot-codegen/src/generator/mod.rs +++ b/godot-codegen/src/generator/mod.rs @@ -28,6 +28,7 @@ pub mod method_tables; pub mod native_structures; pub mod notifications; pub mod signals; +pub mod sys; pub mod utility_functions; pub mod virtual_definition_consts; pub mod virtual_traits; diff --git a/godot-codegen/src/generator/sys.rs b/godot-codegen/src/generator/sys.rs new file mode 100644 index 000000000..2c265d1d1 --- /dev/null +++ b/godot-codegen/src/generator/sys.rs @@ -0,0 +1,82 @@ +/* + * Copyright (c) godot-rust; Bromeon and contributors. + * 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 proc_macro2::{Ident, TokenStream}; +use quote::{quote, ToTokens}; + +use crate::util::ident; + +#[allow(unused)] +pub enum SysTypeParam { + /// `* mut SysType` + Mut(&'static str), + /// `* const SysType` + Const(&'static str), +} + +impl SysTypeParam { + pub fn type_(&self) -> &'static str { + match self { + SysTypeParam::Mut(s) | SysTypeParam::Const(s) => s, + } + } + + fn to_ident(&self) -> Ident { + match self { + SysTypeParam::Mut(s) | SysTypeParam::Const(s) => ident(s), + } + } +} + +impl ToTokens for SysTypeParam { + fn to_tokens(&self, tokens: &mut TokenStream) { + let type_ident = self.to_ident(); + match self { + SysTypeParam::Mut(_) => quote! { * mut crate::sys::#type_ident }, + SysTypeParam::Const(_) => quote! { * const crate::sys::#type_ident }, + } + .to_tokens(tokens); + } +} + +/// SysTypes used as parameters in various APIs defined in `extension_api.json`. +// Currently hardcoded and it probably will stay this way – extracting types from gdextension_interface is way too messy. +// Must be different abstraction to avoid clashes with other types passed as pointers (e.g. *Glyph). +pub static SYS_PARAMS: &[SysTypeParam] = &[ + #[cfg(since_api = "4.6")] + SysTypeParam::Const("GDExtensionInitializationFunction"), +]; + +/// Creates `GodotConvert`, `ToGodot` and `FromGodot` impl +/// for SysTypes – various pointer types declared in `gdextension_interface`. +pub fn make_godotconvert_for_systypes() -> Vec { + let mut tokens = vec![]; + for sys_type_param in SYS_PARAMS { + tokens.push( + quote! { + impl crate::meta::GodotConvert for #sys_type_param { + type Via = i64; + + } + + impl crate::meta::ToGodot for #sys_type_param { + type Pass = crate::meta::ByValue; + fn to_godot(&self) -> Self::Via { + * self as i64 + } + } + + impl crate::meta::FromGodot for #sys_type_param { + fn try_from_godot(via: Self::Via) -> Result < Self, crate::meta::error::ConvertError > { + Ok(via as Self) + } + } + } + ) + } + tokens +} diff --git a/godot-codegen/src/models/domain.rs b/godot-codegen/src/models/domain.rs index 7da3aa270..d331edc7b 100644 --- a/godot-codegen/src/models/domain.rs +++ b/godot-codegen/src/models/domain.rs @@ -699,6 +699,9 @@ pub struct GodotTy { pub enum RustTy { /// `bool`, `Vector3i`, `Array`, `GString` BuiltinIdent { ty: Ident, arg_passing: ArgPassing }, + /// Pointers declared in `gdextension_interface` such as `sys::GDExtensionInitializationFunction` + /// used as parameters in some APIs. + SysIdent { tokens: TokenStream }, /// `Array` /// @@ -789,6 +792,7 @@ impl ToTokens for RustTy { RustTy::EngineEnum { tokens: path, .. } => path.to_tokens(tokens), RustTy::EngineClass { tokens: path, .. } => path.to_tokens(tokens), RustTy::ExtenderReceiver { tokens: path } => path.to_tokens(tokens), + RustTy::SysIdent { tokens: path } => path.to_tokens(tokens), } } } diff --git a/godot-codegen/src/special_cases/codegen_special_cases.rs b/godot-codegen/src/special_cases/codegen_special_cases.rs index c045e4630..816c30f5c 100644 --- a/godot-codegen/src/special_cases/codegen_special_cases.rs +++ b/godot-codegen/src/special_cases/codegen_special_cases.rs @@ -49,6 +49,7 @@ fn is_type_excluded(ty: &str, ctx: &mut Context) -> bool { RustTy::BuiltinIdent { .. } => false, RustTy::BuiltinArray { .. } => false, RustTy::RawPointer { inner, .. } => is_rust_type_excluded(inner), + RustTy::SysIdent { .. } => true, RustTy::EngineArray { elem_class, .. } => is_class_excluded(elem_class.as_str()), RustTy::EngineEnum { surrounding_class, .. From 88de7c82331579e3a5191a1e4680d04eb123d6cb Mon Sep 17 00:00:00 2001 From: Yarvin Date: Mon, 6 Oct 2025 09:36:24 +0200 Subject: [PATCH 05/54] Codegen for `Array` - required to properly initialize and cache Array runtime type. ----- Methods such as `duplicate_...` were initializing and caching typed `VariantArray` as a return value for outgoing calls. We fix it by initializing proper `Array` in the first place. --- godot-codegen/src/generator/builtins.rs | 33 ++++++++---- .../src/generator/default_parameters.rs | 2 +- .../src/generator/functions_common.rs | 13 +++-- godot-codegen/src/models/domain.rs | 53 +++++++++++++++++++ godot-codegen/src/models/domain_mapping.rs | 17 ++++-- .../special_cases/codegen_special_cases.rs | 1 + .../src/special_cases/special_cases.rs | 20 ++++++- godot-core/src/builtin/collections/array.rs | 37 +++++-------- .../builtin_tests/containers/array_test.rs | 19 +++++++ 9 files changed, 150 insertions(+), 45 deletions(-) diff --git a/godot-codegen/src/generator/builtins.rs b/godot-codegen/src/generator/builtins.rs index 321fd0a8b..bfdcc9acf 100644 --- a/godot-codegen/src/generator/builtins.rs +++ b/godot-codegen/src/generator/builtins.rs @@ -197,14 +197,27 @@ fn make_special_builtin_methods(class_name: &TyName, _ctx: &Context) -> TokenStr /// Get the safety docs of an unsafe method, or `None` if it is safe. fn method_safety_doc(class_name: &TyName, method: &BuiltinMethod) -> Option { - if class_name.godot_ty == "Array" - && &method.return_value().type_tokens().to_string() == "VariantArray" - { - return Some(quote! { - /// # Safety - /// - /// You must ensure that the returned array fulfils the safety invariants of [`Array`](crate::builtin::Array). - }); + if class_name.godot_ty == "Array" { + if method.is_generic() { + return Some(quote! { + /// # Safety + /// You must ensure that the returned array fulfils the safety invariants of [`Array`](crate::builtin::Array), this being: + /// - Any values written to the array must match the runtime type of the array. + /// - Any values read from the array must be convertible to the type `T`. + /// + /// If the safety invariant of `Array` is intact, which it must be for any publicly accessible arrays, then `T` must match + /// the runtime type of the array. This then implies that both of the conditions above hold. This means that you only need + /// to keep the above conditions in mind if you are intentionally violating the safety invariant of `Array`. + /// + /// In the current implementation, both cases will produce a panic rather than undefined behavior, but this should not be relied upon. + }); + } else if &method.return_value().type_tokens().to_string() == "VariantArray" { + return Some(quote! { + /// # Safety + /// + /// You must ensure that the returned array fulfils the safety invariants of [`Array`](crate::builtin::Array). + }); + } } None @@ -252,11 +265,13 @@ fn make_builtin_method_definition( let receiver = functions_common::make_receiver(method.qualifier(), ffi_arg_in); let object_ptr = &receiver.ffi_arg; + let maybe_generic_params = method.return_value().generic_params(); + let ptrcall_invocation = quote! { let method_bind = sys::builtin_method_table().#fptr_access; - Signature::::out_builtin_ptrcall( + Signature::::out_builtin_ptrcall( method_bind, #builtin_name_str, #method_name_str, diff --git a/godot-codegen/src/generator/default_parameters.rs b/godot-codegen/src/generator/default_parameters.rs index 7f0ac3d82..520596a60 100644 --- a/godot-codegen/src/generator/default_parameters.rs +++ b/godot-codegen/src/generator/default_parameters.rs @@ -11,7 +11,7 @@ use quote::{format_ident, quote}; use crate::generator::functions_common; use crate::generator::functions_common::{ - make_arg_expr, make_param_or_field_type, FnArgExpr, FnCode, FnKind, FnParamDecl, FnParamTokens, + make_arg_expr, make_param_or_field_type, FnArgExpr, FnCode, FnKind, FnParamDecl, }; use crate::models::domain::{FnParam, FnQualifier, Function, RustTy, TyName}; use crate::util::{ident, safe_ident}; diff --git a/godot-codegen/src/generator/functions_common.rs b/godot-codegen/src/generator/functions_common.rs index aa14d177b..0adcbd3c6 100644 --- a/godot-codegen/src/generator/functions_common.rs +++ b/godot-codegen/src/generator/functions_common.rs @@ -142,7 +142,6 @@ pub fn make_function_definition( callsig_param_types: param_types, callsig_lifetime_args, arg_exprs: arg_names, - func_general_lifetime: fn_lifetime, } = if sig.is_virtual() { make_params_exprs_virtual(sig.params().iter(), sig) } else { @@ -175,11 +174,14 @@ pub fn make_function_definition( default_structs_code = TokenStream::new(); }; + let maybe_func_generic_params = sig.return_value().generic_params(); + let maybe_func_generic_bounds = sig.return_value().where_clause(); + let call_sig_decl = { let return_ty = &sig.return_value().type_tokens(); quote! { - type CallRet = #return_ty; + type CallRet #maybe_func_generic_params = #return_ty; type CallParams #callsig_lifetime_args = (#(#param_types,)*); } }; @@ -279,10 +281,12 @@ pub fn make_function_definition( quote! { #maybe_safety_doc - #vis #maybe_unsafe fn #primary_fn_name #fn_lifetime ( + #vis #maybe_unsafe fn #primary_fn_name #maybe_func_generic_params ( #receiver_param #( #params, )* - ) #return_decl { + ) #return_decl + #maybe_func_generic_bounds + { #call_sig_decl let args = (#( #arg_names, )*); @@ -489,6 +493,7 @@ pub(crate) fn make_param_or_field_type( .. } | RustTy::BuiltinArray { .. } + | RustTy::GenericArray | RustTy::EngineArray { .. } => { let lft = lifetimes.next(); special_ty = Some(quote! { RefArg<#lft, #ty> }); diff --git a/godot-codegen/src/models/domain.rs b/godot-codegen/src/models/domain.rs index d331edc7b..3500cc3ff 100644 --- a/godot-codegen/src/models/domain.rs +++ b/godot-codegen/src/models/domain.rs @@ -300,28 +300,40 @@ pub trait Function: fmt::Display { fn name(&self) -> &str { &self.common().name } + /// Rust name as `Ident`. Might be cached in future. fn name_ident(&self) -> Ident { safe_ident(self.name()) } + fn godot_name(&self) -> &str { &self.common().godot_name } + fn params(&self) -> &[FnParam] { &self.common().parameters } + fn return_value(&self) -> &FnReturn { &self.common().return_value } + fn is_vararg(&self) -> bool { self.common().is_vararg } + fn is_private(&self) -> bool { self.common().is_private } + fn is_virtual(&self) -> bool { matches!(self.direction(), FnDirection::Virtual { .. }) } + + fn is_generic(&self) -> bool { + matches!(self.return_value().type_, Some(RustTy::GenericArray)) + } + fn direction(&self) -> FnDirection { self.common().direction } @@ -621,6 +633,13 @@ impl FnReturn { Self::with_enum_replacements(return_value, &[], ctx) } + pub fn with_generic_builtin(generic_type: RustTy) -> Self { + Self { + decl: generic_type.return_decl(), + type_: Some(generic_type), + } + } + pub fn with_enum_replacements( return_value: &Option, replacements: EnumReplacements, @@ -669,6 +688,14 @@ impl FnReturn { } } + pub fn generic_params(&self) -> Option { + self.type_.as_ref()?.generic_params() + } + + pub fn where_clause(&self) -> Option { + self.type_.as_ref()?.where_clause() + } + pub fn call_result_decl(&self) -> TokenStream { let ret = self.type_tokens(); quote! { -> Result<#ret, crate::meta::error::CallError> } @@ -708,6 +735,11 @@ pub enum RustTy { /// Note that untyped arrays are mapped as `BuiltinIdent("Array")`. BuiltinArray { elem_type: TokenStream }, + /// Will be included as `Array` in the generated source. + /// + /// Set by [`builtin_method_generic_ret`](crate::special_cases::builtin_method_generic_ret) + GenericArray, + /// C-style raw pointer to a `RustTy`. RawPointer { inner: Box, is_const: bool }, @@ -758,10 +790,30 @@ impl RustTy { pub fn return_decl(&self) -> TokenStream { match self { Self::EngineClass { tokens, .. } => quote! { -> Option<#tokens> }, + Self::GenericArray => quote! { -> Array }, other => quote! { -> #other }, } } + pub fn generic_params(&self) -> Option { + if matches!(self, Self::GenericArray) { + Some(quote! { < Ret > }) + } else { + None + } + } + + pub fn where_clause(&self) -> Option { + if matches!(self, Self::GenericArray) { + Some(quote! { + where + Ret: crate::meta::ArrayElement, + }) + } else { + None + } + } + pub fn is_integer(&self) -> bool { let RustTy::BuiltinIdent { ty, .. } = self else { return false; @@ -792,6 +844,7 @@ impl ToTokens for RustTy { RustTy::EngineEnum { tokens: path, .. } => path.to_tokens(tokens), RustTy::EngineClass { tokens: path, .. } => path.to_tokens(tokens), RustTy::ExtenderReceiver { tokens: path } => path.to_tokens(tokens), + RustTy::GenericArray => quote! { Array }.to_tokens(tokens), RustTy::SysIdent { tokens: path } => path.to_tokens(tokens), } } diff --git a/godot-codegen/src/models/domain_mapping.rs b/godot-codegen/src/models/domain_mapping.rs index dbc89d0ee..477075028 100644 --- a/godot-codegen/src/models/domain_mapping.rs +++ b/godot-codegen/src/models/domain_mapping.rs @@ -366,10 +366,17 @@ impl BuiltinMethod { return None; } - let return_value = method - .return_type - .as_deref() - .map(JsonMethodReturn::from_type_no_meta); + let return_value = if let Some(generic) = + special_cases::builtin_method_generic_ret(builtin_name, method) + { + generic + } else { + let return_value = &method + .return_type + .as_deref() + .map(JsonMethodReturn::from_type_no_meta); + FnReturn::new(return_value, ctx) + }; Some(Self { common: FunctionCommon { @@ -381,7 +388,7 @@ impl BuiltinMethod { parameters: FnParam::builder() .no_defaults() .build_many(&method.arguments, ctx), - return_value: FnReturn::new(&return_value, ctx), + return_value, is_vararg: method.is_vararg, is_private: false, // See 'exposed' below. Could be special_cases::is_method_private(builtin_name, &method.name), is_virtual_required: false, diff --git a/godot-codegen/src/special_cases/codegen_special_cases.rs b/godot-codegen/src/special_cases/codegen_special_cases.rs index 816c30f5c..f254ace0f 100644 --- a/godot-codegen/src/special_cases/codegen_special_cases.rs +++ b/godot-codegen/src/special_cases/codegen_special_cases.rs @@ -48,6 +48,7 @@ fn is_type_excluded(ty: &str, ctx: &mut Context) -> bool { match ty { RustTy::BuiltinIdent { .. } => false, RustTy::BuiltinArray { .. } => false, + RustTy::GenericArray => false, RustTy::RawPointer { inner, .. } => is_rust_type_excluded(inner), RustTy::SysIdent { .. } => true, RustTy::EngineArray { elem_class, .. } => is_class_excluded(elem_class.as_str()), diff --git a/godot-codegen/src/special_cases/special_cases.rs b/godot-codegen/src/special_cases/special_cases.rs index 518a5c1af..afa444f21 100644 --- a/godot-codegen/src/special_cases/special_cases.rs +++ b/godot-codegen/src/special_cases/special_cases.rs @@ -33,7 +33,7 @@ use proc_macro2::Ident; use crate::conv::to_enum_type_uncached; use crate::models::domain::{ - ClassCodegenLevel, Enum, EnumReplacements, RustTy, TyName, VirtualMethodPresence, + ClassCodegenLevel, Enum, EnumReplacements, FnReturn, RustTy, TyName, VirtualMethodPresence, }; use crate::models::json::{JsonBuiltinMethod, JsonClassMethod, JsonSignal, JsonUtilityFunction}; use crate::special_cases::codegen_special_cases; @@ -778,6 +778,24 @@ pub fn is_builtin_method_deleted(_class_name: &TyName, method: &JsonBuiltinMetho codegen_special_cases::is_builtin_method_excluded(method) } +/// Returns some generic type – such as `GenericArray` representing `Array` – if method is marked as generic, `None` otherwise. +/// +/// Usually required to initialize the return value and cache its type (see also https://github.com/godot-rust/gdext/pull/1357). +pub fn builtin_method_generic_ret( + class_name: &TyName, + method: &JsonBuiltinMethod, +) -> Option { + match ( + class_name.rust_ty.to_string().as_str(), + method.name.as_str(), + ) { + ("Array", "duplicate") | ("Array", "slice") => { + Some(FnReturn::with_generic_builtin(RustTy::GenericArray)) + } + _ => None, + } +} + /// True if signal is absent from codegen (only when surrounding class is excluded). pub fn is_signal_deleted(_class_name: &TyName, signal: &JsonSignal) -> bool { // If any argument type (a class) is excluded. diff --git a/godot-core/src/builtin/collections/array.rs b/godot-core/src/builtin/collections/array.rs index cdbd5ba5d..4d263e27b 100644 --- a/godot-core/src/builtin/collections/array.rs +++ b/godot-core/src/builtin/collections/array.rs @@ -533,12 +533,11 @@ impl Array { /// To create a deep copy, use [`duplicate_deep()`][Self::duplicate_deep] instead. /// To create a new reference to the same array data, use [`clone()`][Clone::clone]. pub fn duplicate_shallow(&self) -> Self { - // SAFETY: We never write to the duplicated array, and all values read are read as `Variant`. - let duplicate: VariantArray = unsafe { self.as_inner().duplicate(false) }; + // SAFETY: duplicate() returns a typed array with the same type as Self, and all values are taken from `self` so have the right type + let duplicate: Self = unsafe { self.as_inner().duplicate(false) }; - // SAFETY: duplicate() returns a typed array with the same type as Self, and all values are taken from `self` so have the right type. - let result = unsafe { duplicate.assume_type() }; - result.with_cache(self) + // Note: cache is being set while initializing the duplicate as a return value for above call. + duplicate } /// Returns a deep copy of the array. All nested arrays and dictionaries are duplicated and @@ -548,12 +547,11 @@ impl Array { /// To create a shallow copy, use [`duplicate_shallow()`][Self::duplicate_shallow] instead. /// To create a new reference to the same array data, use [`clone()`][Clone::clone]. pub fn duplicate_deep(&self) -> Self { - // SAFETY: We never write to the duplicated array, and all values read are read as `Variant`. - let duplicate: VariantArray = unsafe { self.as_inner().duplicate(true) }; + // SAFETY: duplicate() returns a typed array with the same type as Self, and all values are taken from `self` so have the right type + let duplicate: Self = unsafe { self.as_inner().duplicate(true) }; - // SAFETY: duplicate() returns a typed array with the same type as Self, and all values are taken from `self` so have the right type. - let result = unsafe { duplicate.assume_type() }; - result.with_cache(self) + // Note: cache is being set while initializing the duplicate as a return value for above call. + duplicate } /// Returns a sub-range `begin..end` as a new `Array`. @@ -588,13 +586,10 @@ impl Array { let (begin, end) = range.signed(); let end = end.unwrap_or(i32::MAX as i64); - // SAFETY: The type of the array is `T` and we convert the returned array to an `Array` immediately. - let subarray: VariantArray = - unsafe { self.as_inner().slice(begin, end, step as i64, deep) }; + // SAFETY: slice() returns a typed array with the same type as Self, and all values are taken from `self` so have the right type. + let subarray: Self = unsafe { self.as_inner().slice(begin, end, step as i64, deep) }; - // SAFETY: slice() returns a typed array with the same type as Self. - let result = unsafe { subarray.assume_type() }; - result.with_cache(self) + subarray } /// Returns an iterator over the elements of the `Array`. Note that this takes the array @@ -950,8 +945,7 @@ impl Array { } } - /// Changes the generic type on this array, without changing its contents. Needed for API - /// functions that return a variant array even though we know its type, and for API functions + /// Changes the generic type on this array, without changing its contents. Needed for API functions /// that take a variant array even though we want to pass a typed one. /// /// # Safety @@ -966,13 +960,6 @@ impl Array { /// Note also that any `GodotType` can be written to a `Variant` array. /// /// In the current implementation, both cases will produce a panic rather than undefined behavior, but this should not be relied upon. - unsafe fn assume_type(self) -> Array { - // The memory layout of `Array` does not depend on `T`. - std::mem::transmute::, Array>(self) - } - - /// # Safety - /// See [`assume_type`](Self::assume_type). unsafe fn assume_type_ref(&self) -> &Array { // The memory layout of `Array` does not depend on `T`. std::mem::transmute::<&Array, &Array>(self) diff --git a/itest/rust/src/builtin_tests/containers/array_test.rs b/itest/rust/src/builtin_tests/containers/array_test.rs index ea8e3de98..ea8fc7a5a 100644 --- a/itest/rust/src/builtin_tests/containers/array_test.rs +++ b/itest/rust/src/builtin_tests/containers/array_test.rs @@ -677,6 +677,25 @@ func make_array() -> Array[CustomScriptForArrays]: assert_eq!(script.get_global_name(), "CustomScriptForArrays".into()); } +// Test that proper type has been set&cached while creating new Array. +// https://github.com/godot-rust/gdext/pull/1357 +#[itest] +fn array_inner_type() { + let primary = Array::::new(); + + let secondary = primary.duplicate_shallow(); + assert_eq!(secondary.element_type(), primary.element_type()); + + let secondary = primary.duplicate_deep(); + assert_eq!(secondary.element_type(), primary.element_type()); + + let subarray = primary.subarray_deep(.., None); + assert_eq!(subarray.element_type(), primary.element_type()); + + let subarray = primary.subarray_shallow(.., None); + assert_eq!(subarray.element_type(), primary.element_type()); +} + // ---------------------------------------------------------------------------------------------------------------------------------------------- // Class definitions From 66e74c9d66e1278f6da3abbcd120de472ec47912 Mon Sep 17 00:00:00 2001 From: Yarvin Date: Mon, 6 Oct 2025 19:59:11 +0200 Subject: [PATCH 06/54] =?UTF-8?q?Remove=20`func=5Fgeneral=5Flifetime`=20fr?= =?UTF-8?q?om=20FnParamTokens=20since=20it=20was=20never=20set.=20Remove?= =?UTF-8?q?=20`ExBuilderConstructor`=20=E2=80=93=20it=20was=20doing=20noth?= =?UTF-8?q?ing,=20and=20in=20practice=20it=20was=20supposed=20to=20do=20th?= =?UTF-8?q?e=20same=20thing=20as=20`ExBuilderConstructorLifetimed`.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- godot-codegen/src/generator/default_parameters.rs | 11 +---------- godot-codegen/src/generator/functions_common.rs | 7 +------ 2 files changed, 2 insertions(+), 16 deletions(-) diff --git a/godot-codegen/src/generator/default_parameters.rs b/godot-codegen/src/generator/default_parameters.rs index 520596a60..8ec204cb2 100644 --- a/godot-codegen/src/generator/default_parameters.rs +++ b/godot-codegen/src/generator/default_parameters.rs @@ -59,15 +59,6 @@ pub fn make_function_definition_with_defaults( &default_fn_params, ); - // ExBuilder::new() constructor signature. - let FnParamTokens { - func_general_lifetime: simple_fn_lifetime, - .. - } = fns::make_params_exprs( - required_fn_params.iter().cloned(), - FnKind::ExBuilderConstructor, - ); - let return_decl = &sig.return_value().decl; // If either the builder has a lifetime (non-static/global method), or one of its parameters is a reference, @@ -119,7 +110,7 @@ pub fn make_function_definition_with_defaults( // Lifetime is set if any parameter is a reference. #[doc = #default_parameter_usage] #[inline] - #vis fn #simple_fn_name #simple_fn_lifetime ( + #vis fn #simple_fn_name ( #simple_receiver_param #( #class_method_required_params, )* ) #return_decl { diff --git a/godot-codegen/src/generator/functions_common.rs b/godot-codegen/src/generator/functions_common.rs index 0adcbd3c6..e1600587c 100644 --- a/godot-codegen/src/generator/functions_common.rs +++ b/godot-codegen/src/generator/functions_common.rs @@ -98,7 +98,6 @@ pub struct FnParamTokens { /// Generic argument list `<'a0, 'a1, ...>` after `type CallSig`, if available. pub callsig_lifetime_args: Option, pub arg_exprs: Vec, - pub func_general_lifetime: Option, } pub fn make_function_definition( @@ -361,10 +360,7 @@ pub(crate) enum FnKind { /// `call()` forwarding to `try_call()`. DelegateTry, - /// Default extender `new()` associated function -- optional receiver and required parameters. - ExBuilderConstructor, - - /// Same as [`ExBuilderConstructor`], but for a builder with an explicit lifetime. + /// Default extender `new()` associated function -- optional receiver and required parameters. Has an explicit lifetime. ExBuilderConstructorLifetimed, /// Default extender `new()` associated function -- only default parameters. @@ -577,7 +573,6 @@ pub(crate) fn make_params_exprs<'a>( // Methods relevant in the context of default parameters. Flow in this order. // Note that for builder methods of Ex* structs, there's a direct call in default_parameters.rs to the parameter manipulation methods, // bypassing this method. So one case is missing here. - FnKind::ExBuilderConstructor => (FnParamDecl::FnPublic, FnArgExpr::StoreInField), FnKind::ExBuilderConstructorLifetimed => { (FnParamDecl::FnPublicLifetime, FnArgExpr::StoreInField) } From ae07e11bb2c1e35fd574608f30a11bc3043cc966 Mon Sep 17 00:00:00 2001 From: Yarvin Date: Sat, 4 Oct 2025 11:02:33 +0200 Subject: [PATCH 07/54] Doc bugfixes and improvements - Bugfix: Handle docs in `#[godot_api(secondary)]` - Code tweaks: Separate docs modules to not pollute rest of the code with `#[cfg(all(feature = "register-docs", since_api = "4.3"))]`. Simplify registration logic. - Create separate PluginRegistry for Docs and Docs only. --- godot-core/src/docs.rs | 161 +++++++++++------- godot-core/src/private.rs | 25 +-- godot-core/src/registry/class.rs | 11 +- godot-core/src/registry/plugin.rs | 31 +--- .../src/class/data_models/inherent_impl.rs | 27 +-- .../class/data_models/interface_trait_impl.rs | 13 +- godot-macros/src/class/derive_godot_class.rs | 18 +- .../src/{docs.rs => docs/extract_docs.rs} | 4 +- godot-macros/src/docs/mod.rs | 115 +++++++++++++ godot-macros/src/lib.rs | 1 - .../rust/src/register_tests/constant_test.rs | 2 - .../src/register_tests/register_docs_test.rs | 14 ++ .../register_tests/res/registered_docs.xml | 24 ++- 13 files changed, 284 insertions(+), 162 deletions(-) rename godot-macros/src/{docs.rs => docs/extract_docs.rs} (99%) create mode 100644 godot-macros/src/docs/mod.rs diff --git a/godot-core/src/docs.rs b/godot-core/src/docs.rs index ef597cd1f..46d04a8a3 100644 --- a/godot-core/src/docs.rs +++ b/godot-core/src/docs.rs @@ -8,7 +8,41 @@ use std::collections::HashMap; use crate::meta::ClassId; -use crate::registry::plugin::{ITraitImpl, InherentImpl, PluginItem, Struct}; +use crate::obj::GodotClass; + +/// Piece of information that is gathered by the self-registration ("plugin") system. +/// +/// You should not manually construct this struct, but rather use [`DocsPlugin::new()`]. +#[derive(Debug)] +pub struct DocsPlugin { + /// The name of the class to register docs for. + class_name: ClassId, + + /// The actual item being registered. + item: DocsItem, +} + +impl DocsPlugin { + /// Creates a new `DocsPlugin`, automatically setting the `class_name` to the values defined in [`GodotClass`]. + pub fn new(item: DocsItem) -> Self { + Self { + class_name: T::class_id(), + item, + } + } +} + +type ITraitImplDocs = &'static str; + +#[derive(Debug)] +pub enum DocsItem { + /// Docs for `#[derive(GodotClass)] struct MyClass`. + Struct(StructDocs), + /// Docs for `#[godot_api] impl MyClass`. + InherentImpl(InherentImplDocs), + /// Docs for `#[godot_api] impl ITrait for MyClass`. + ITraitImpl(ITraitImplDocs), +} /// Created for documentation on /// ```ignore @@ -29,7 +63,7 @@ pub struct StructDocs { pub members: &'static str, } -/// Keeps documentation for inherent `impl` blocks, such as: +/// Keeps documentation for inherent `impl` blocks (primary and secondary), such as: /// ```ignore /// #[godot_api] /// impl Struct { @@ -46,18 +80,19 @@ pub struct StructDocs { /// } /// ``` /// All fields are XML parts, escaped where necessary. -#[derive(Default, Copy, Clone, Debug)] +#[derive(Default, Clone, Debug)] pub struct InherentImplDocs { - pub methods: Vec<&'static str>, - pub signals: Vec<&'static str>, - pub constants: Vec<&'static str>, + pub methods: &'static str, + pub signals: &'static str, + pub constants: &'static str, } #[derive(Default)] struct DocPieces { definition: StructDocs, - inherent: InherentImplDocs, - virtual_methods: Vec<&'static str>, + methods: Vec<&'static str>, + signals: Vec<&'static str>, + constants: Vec<&'static str>, } /// This function scours the registered plugins to find their documentation pieces, @@ -76,68 +111,66 @@ struct DocPieces { #[doc(hidden)] pub fn gather_xml_docs() -> impl Iterator { let mut map = HashMap::::new(); - crate::private::iterate_plugins(|x| { + crate::private::iterate_docs_plugins(|x| { let class_name = x.class_name; - match &x.item { - PluginItem::InherentImpl(InherentImpl { docs, .. }) => { - map.entry(class_name).or_default().inherent = docs.clone(); + DocsItem::Struct(s) => { + map.entry(class_name).or_default().definition = *s; } - PluginItem::ITraitImpl(ITraitImpl { - virtual_method_docs, - .. - }) => map - .entry(class_name) - .or_default() - .virtual_methods - .push(virtual_method_docs), - - PluginItem::Struct(Struct { docs, .. }) => { - map.entry(class_name).or_default().definition = *docs; + DocsItem::InherentImpl(trait_docs) => { + let InherentImplDocs { + methods, + constants, + signals, + } = trait_docs; + map.entry(class_name).or_default().methods.push(methods); + map.entry(class_name) + .and_modify(|pieces| pieces.constants.push(constants)); + map.entry(class_name) + .and_modify(|pieces| pieces.signals.push(signals)); + } + DocsItem::ITraitImpl(methods) => { + map.entry(class_name).or_default().methods.push(methods); } - - _ => (), } }); map.into_iter().map(|(class, pieces)| { - let StructDocs { - base, - description, - experimental, - deprecated, - members, - } = pieces.definition; - - let virtual_methods = pieces.virtual_methods; - - let mut method_docs = String::from_iter(pieces.inherent.methods); - let signal_docs = String::from_iter(pieces.inherent.signals); - let constant_docs = String::from_iter(pieces.inherent.constants); - - method_docs.extend(virtual_methods); - let methods_block = if method_docs.is_empty() { - String::new() - } else { - format!("{method_docs}") - }; - let signals_block = if signal_docs.is_empty() { - String::new() - } else { - format!("{signal_docs}") - }; - let constants_block = if constant_docs.is_empty() { - String::new() - } else { - format!("{constant_docs}") - }; - let (brief, description) = match description - .split_once("[br]") { - Some((brief, description)) => (brief, description.trim_start_matches("[br]")), - None => (description, ""), - }; - - format!(r#" + let StructDocs { + base, + description, + experimental, + deprecated, + members, + } = pieces.definition; + + + let method_docs = String::from_iter(pieces.methods); + let signal_docs = String::from_iter(pieces.signals); + let constant_docs = String::from_iter(pieces.constants); + + let methods_block = if method_docs.is_empty() { + String::new() + } else { + format!("{method_docs}") + }; + let signals_block = if signal_docs.is_empty() { + String::new() + } else { + format!("{signal_docs}") + }; + let constants_block = if constant_docs.is_empty() { + String::new() + } else { + format!("{constant_docs}") + }; + let (brief, description) = match description + .split_once("[br]") { + Some((brief, description)) => (brief, description.trim_start_matches("[br]")), + None => (description, ""), + }; + + format!(r#" {brief} {description} @@ -146,8 +179,8 @@ pub fn gather_xml_docs() -> impl Iterator { {signals_block} {members} "#) - }, - ) + }, + ) } /// # Safety diff --git a/godot-core/src/private.rs b/godot-core/src/private.rs index fbaa54555..9f1ac4009 100644 --- a/godot-core/src/private.rs +++ b/godot-core/src/private.rs @@ -22,6 +22,8 @@ use crate::{classes, sys}; // Public re-exports mod reexport_pub { + #[cfg(all(since_api = "4.3", feature = "register-docs"))] + pub use crate::docs::{DocsItem, DocsPlugin, InherentImplDocs, StructDocs}; pub use crate::gen::classes::class_macros; #[cfg(feature = "trace")] pub use crate::meta::trace; @@ -52,6 +54,8 @@ static CALL_ERRORS: Global = Global::default(); static ERROR_PRINT_LEVEL: atomic::AtomicU8 = atomic::AtomicU8::new(2); sys::plugin_registry!(pub __GODOT_PLUGIN_REGISTRY: ClassPlugin); +#[cfg(all(since_api = "4.3", feature = "register-docs"))] +sys::plugin_registry!(pub __GODOT_DOCS_REGISTRY: DocsPlugin); // ---------------------------------------------------------------------------------------------------------------------------------------------- // Call error handling @@ -142,26 +146,15 @@ pub fn next_class_id() -> u16 { NEXT_CLASS_ID.fetch_add(1, atomic::Ordering::Relaxed) } -// Don't touch unless you know what you're doing. -#[doc(hidden)] -pub fn edit_inherent_impl(class_name: crate::meta::ClassName, f: impl FnOnce(&mut InherentImpl)) { - let mut plugins = __GODOT_PLUGIN_REGISTRY.lock().unwrap(); - - for elem in plugins.iter_mut().filter(|p| p.class_name == class_name) { - match &mut elem.item { - PluginItem::InherentImpl(inherent_impl) => { - f(inherent_impl); - return; - } - PluginItem::Struct(_) | PluginItem::ITraitImpl(_) | PluginItem::DynTraitImpl(_) => {} - } - } -} - pub(crate) fn iterate_plugins(mut visitor: impl FnMut(&ClassPlugin)) { sys::plugin_foreach!(__GODOT_PLUGIN_REGISTRY; visitor); } +#[cfg(all(since_api = "4.3", feature = "register-docs"))] +pub(crate) fn iterate_docs_plugins(mut visitor: impl FnMut(&DocsPlugin)) { + sys::plugin_foreach!(__GODOT_DOCS_REGISTRY; visitor); +} + #[cfg(feature = "codegen-full")] // Remove if used in other scenarios. pub(crate) fn find_inherent_impl(class_name: crate::meta::ClassId) -> Option { // We do this manually instead of using `iterate_plugins()` because we want to break as soon as we find a match. diff --git a/godot-core/src/registry/class.rs b/godot-core/src/registry/class.rs index d9335745d..1715313ec 100644 --- a/godot-core/src/registry/class.rs +++ b/godot-core/src/registry/class.rs @@ -115,10 +115,7 @@ impl ClassRegistrationInfo { // Note: when changing this match, make sure the array has sufficient size. let index = match item { PluginItem::Struct { .. } => 0, - PluginItem::InherentImpl(_) => { - // Inherent impls don't need to be unique. - return; - } + PluginItem::InherentImpl(_) => 1, PluginItem::ITraitImpl { .. } => 2, // Multiple dyn traits can be registered, thus don't validate for uniqueness. @@ -427,8 +424,6 @@ fn fill_class_info(item: PluginItem, c: &mut ClassRegistrationInfo) { is_editor_plugin, is_internal, is_instantiable, - #[cfg(all(since_api = "4.3", feature = "register-docs"))] - docs: _, reference_fn, unreference_fn, }) => { @@ -476,8 +471,6 @@ fn fill_class_info(item: PluginItem, c: &mut ClassRegistrationInfo) { PluginItem::InherentImpl(InherentImpl { register_methods_constants_fn, register_rpcs_fn: _, - #[cfg(all(since_api = "4.3", feature = "register-docs"))] - docs: _, }) => { c.register_methods_constants_fn = Some(register_methods_constants_fn); } @@ -495,8 +488,6 @@ fn fill_class_info(item: PluginItem, c: &mut ClassRegistrationInfo) { user_free_property_list_fn, user_property_can_revert_fn, user_property_get_revert_fn, - #[cfg(all(since_api = "4.3", feature = "register-docs"))] - virtual_method_docs: _, validate_property_fn, }) => { c.user_register_fn = user_register_fn; diff --git a/godot-core/src/registry/plugin.rs b/godot-core/src/registry/plugin.rs index 614828f0c..f03da5fae 100644 --- a/godot-core/src/registry/plugin.rs +++ b/godot-core/src/registry/plugin.rs @@ -8,8 +8,6 @@ use std::any::Any; use std::{any, fmt}; -#[cfg(all(since_api = "4.3", feature = "register-docs"))] -use crate::docs::*; use crate::init::InitLevel; use crate::meta::ClassId; use crate::obj::{bounds, cap, Bounds, DynGd, Gd, GodotClass, Inherits, UserClass}; @@ -39,10 +37,10 @@ pub struct ClassPlugin { /// Incorrectly setting this value should not cause any UB but will likely cause errors during registration time. // Init-level is per ClassPlugin and not per PluginItem, because all components of all classes are mixed together in one // huge linker list. There is no per-class aggregation going on, so this allows to easily filter relevant classes. - pub init_level: InitLevel, + pub(crate) init_level: InitLevel, /// The actual item being registered. - pub item: PluginItem, + pub(crate) item: PluginItem, } impl ClassPlugin { @@ -197,16 +195,10 @@ pub struct Struct { /// Whether the class has a default constructor. pub(crate) is_instantiable: bool, - - /// Documentation extracted from the struct's RustDoc. - #[cfg(all(since_api = "4.3", feature = "register-docs"))] - pub(crate) docs: StructDocs, } impl Struct { - pub fn new( - #[cfg(all(since_api = "4.3", feature = "register-docs"))] docs: StructDocs, - ) -> Self { + pub fn new() -> Self { let refcounted = ::IS_REF_COUNTED; Self { @@ -222,8 +214,6 @@ impl Struct { is_editor_plugin: false, is_internal: false, is_instantiable: false, - #[cfg(all(since_api = "4.3", feature = "register-docs"))] - docs, // While Godot doesn't do anything with these callbacks for non-RefCounted classes, we can avoid instantiating them in Rust. reference_fn: refcounted.then_some(callbacks::reference::), unreference_fn: refcounted.then_some(callbacks::unreference::), @@ -294,9 +284,6 @@ pub struct InherentImpl { // This field is only used during codegen-full. #[cfg_attr(not(feature = "codegen-full"), expect(dead_code))] pub(crate) register_rpcs_fn: Option, - - #[cfg(all(since_api = "4.3", feature = "register-docs"))] - pub docs: InherentImplDocs, } impl InherentImpl { @@ -308,18 +295,12 @@ impl InherentImpl { register_rpcs_fn: Some(ErasedRegisterRpcsFn { raw: callbacks::register_user_rpcs::, }), - #[cfg(all(since_api = "4.3", feature = "register-docs"))] - docs: Default::default(), } } } #[derive(Default, Clone, Debug)] pub struct ITraitImpl { - #[cfg(all(since_api = "4.3", feature = "register-docs"))] - /// Virtual method documentation. - pub(crate) virtual_method_docs: &'static str, - /// Callback to user-defined `register_class` function. pub(crate) user_register_fn: Option, @@ -434,12 +415,8 @@ pub struct ITraitImpl { } impl ITraitImpl { - pub fn new( - #[cfg(all(since_api = "4.3", feature = "register-docs"))] virtual_method_docs: &'static str, - ) -> Self { + pub fn new() -> Self { Self { - #[cfg(all(since_api = "4.3", feature = "register-docs"))] - virtual_method_docs, get_virtual_fn: Some(callbacks::get_virtual::), ..Default::default() } diff --git a/godot-macros/src/class/data_models/inherent_impl.rs b/godot-macros/src/class/data_models/inherent_impl.rs index 53c2ade94..2bcd8ac74 100644 --- a/godot-macros/src/class/data_models/inherent_impl.rs +++ b/godot-macros/src/class/data_models/inherent_impl.rs @@ -91,8 +91,8 @@ pub fn transform_inherent_impl( let (funcs, signals) = process_godot_fns(&class_name, &mut impl_block, meta.secondary)?; let consts = process_godot_constants(&mut impl_block)?; - #[cfg(all(feature = "register-docs", since_api = "4.3"))] - let docs = crate::docs::document_inherent_impl(&funcs, &consts, &signals); + let inherent_impl_docs = + crate::docs::make_trait_docs_registration(&funcs, &consts, &signals, &class_name, &prv); // Container struct holding names of all registered #[func]s. // The struct is declared by #[derive(GodotClass)]. @@ -126,26 +126,6 @@ pub fn transform_inherent_impl( let constants_storage_name = format_ident!("__registration_constants_{class_name}"); let fill_storage = { - #[cfg(all(feature = "register-docs", since_api = "4.3"))] - let push_docs = { - let crate::docs::InherentImplXmlDocs { - method_xml_elems, - constant_xml_elems, - signal_xml_elems, - } = docs; - - quote! { - #prv::edit_inherent_impl(#class_name_obj, |inherent_impl| { - inherent_impl.docs.methods.push(#method_xml_elems); - inherent_impl.docs.constants.push(#constant_xml_elems); - inherent_impl.docs.signals.push(#signal_xml_elems); - }); - } - }; - - #[cfg(not(all(feature = "register-docs", since_api = "4.3")))] - let push_docs = TokenStream::new(); - quote! { ::godot::sys::plugin_execute_pre_main!({ #method_storage_name.lock().unwrap().push(|| { @@ -157,7 +137,6 @@ pub fn transform_inherent_impl( #constant_registration }); - #push_docs }); } }; @@ -211,6 +190,7 @@ pub fn transform_inherent_impl( #( #func_name_constants )* } #signal_symbol_types + #inherent_impl_docs }; Ok(result) @@ -224,6 +204,7 @@ pub fn transform_inherent_impl( impl #funcs_collection { #( #func_name_constants )* } + #inherent_impl_docs }; Ok(result) diff --git a/godot-macros/src/class/data_models/interface_trait_impl.rs b/godot-macros/src/class/data_models/interface_trait_impl.rs index 5d78ef3ca..e373cdce7 100644 --- a/godot-macros/src/class/data_models/interface_trait_impl.rs +++ b/godot-macros/src/class/data_models/interface_trait_impl.rs @@ -20,10 +20,11 @@ pub fn transform_trait_impl(mut original_impl: venial::Impl) -> ParseResult ParseResult(#docs); + let mut item = #prv::ITraitImpl::new::<#class_name>(); #(#modifications)* item } @@ -202,6 +203,8 @@ pub fn transform_trait_impl(mut original_impl: venial::Impl) -> ParseResult( #prv::PluginItem::ITraitImpl(#item_constructor) )); + + #register_docs }; // #decls still holds a mutable borrow to `original_impl`, so we mutate && append it afterwards. diff --git a/godot-macros/src/class/derive_godot_class.rs b/godot-macros/src/class/derive_godot_class.rs index ea6057997..09d5c5f96 100644 --- a/godot-macros/src/class/derive_godot_class.rs +++ b/godot-macros/src/class/derive_godot_class.rs @@ -69,18 +69,21 @@ pub fn derive_godot_class(item: venial::Item) -> ParseResult { modifiers.push(quote! { with_internal }) } let base_ty = &struct_cfg.base_ty; - #[cfg(all(feature = "register-docs", since_api = "4.3"))] - let docs = - crate::docs::document_struct(base_ty.to_string(), &class.attributes, &fields.all_fields); - #[cfg(not(all(feature = "register-docs", since_api = "4.3")))] - let docs = quote! {}; + let prv = quote! { ::godot::private }; + + let struct_docs_registration = crate::docs::make_struct_docs_registration( + base_ty.to_string(), + &class.attributes, + &fields.all_fields, + class_name, + &prv, + ); let base_class = quote! { ::godot::classes::#base_ty }; // Use this name because when typing a non-existent class, users will be met with the following error: // could not find `inherit_from_OS__ensure_class_exists` in `class_macros`. let inherits_macro_ident = format_ident!("inherit_from_{}__ensure_class_exists", base_ty); - let prv = quote! { ::godot::private }; let godot_exports_impl = make_property_impl(class_name, &fields); let godot_withbase_impl = if let Some(Field { name, ty, .. }) = &fields.base_field { @@ -196,9 +199,10 @@ pub fn derive_godot_class(item: venial::Item) -> ParseResult { #( #deprecations )* #( #errors )* + #struct_docs_registration ::godot::sys::plugin_add!(#prv::__GODOT_PLUGIN_REGISTRY; #prv::ClassPlugin::new::<#class_name>( #prv::PluginItem::Struct( - #prv::Struct::new::<#class_name>(#docs)#(.#modifiers())* + #prv::Struct::new::<#class_name>()#(.#modifiers())* ) )); diff --git a/godot-macros/src/docs.rs b/godot-macros/src/docs/extract_docs.rs similarity index 99% rename from godot-macros/src/docs.rs rename to godot-macros/src/docs/extract_docs.rs index cfdc65b0a..44ed4fd62 100644 --- a/godot-macros/src/docs.rs +++ b/godot-macros/src/docs/extract_docs.rs @@ -4,13 +4,11 @@ * 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/. */ - -mod markdown_converter; - use proc_macro2::{Ident, TokenStream}; use quote::{quote, ToTokens}; use crate::class::{ConstDefinition, Field, FuncDefinition, SignalDefinition}; +use crate::docs::markdown_converter; #[derive(Default)] struct XmlParagraphs { diff --git a/godot-macros/src/docs/mod.rs b/godot-macros/src/docs/mod.rs new file mode 100644 index 000000000..4bb1b80f5 --- /dev/null +++ b/godot-macros/src/docs/mod.rs @@ -0,0 +1,115 @@ +/* + * Copyright (c) godot-rust; Bromeon and contributors. + * 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/. + */ +#[cfg(all(feature = "register-docs", since_api = "4.3"))] +mod extract_docs; +#[cfg(all(feature = "register-docs", since_api = "4.3"))] +mod markdown_converter; + +use proc_macro2::{Ident, TokenStream}; + +use crate::class::{ConstDefinition, Field, FuncDefinition, SignalDefinition}; + +#[cfg(all(feature = "register-docs", since_api = "4.3"))] +mod docs_generators { + use quote::quote; + + use super::*; + + pub fn make_struct_docs_registration( + base: String, + description: &[venial::Attribute], + fields: &[Field], + class_name: &Ident, + prv: &TokenStream, + ) -> TokenStream { + let struct_docs = extract_docs::document_struct(base, description, fields); + quote! { + ::godot::sys::plugin_add!(#prv::__GODOT_DOCS_REGISTRY; #prv::DocsPlugin::new::<#class_name>( + #prv::DocsItem::Struct( + #struct_docs + ) + )); + } + } + + pub fn make_trait_docs_registration( + functions: &[FuncDefinition], + constants: &[ConstDefinition], + signals: &[SignalDefinition], + class_name: &Ident, + prv: &TokenStream, + ) -> TokenStream { + let extract_docs::InherentImplXmlDocs { + method_xml_elems, + constant_xml_elems, + signal_xml_elems, + } = extract_docs::document_inherent_impl(functions, constants, signals); + + quote! { + ::godot::sys::plugin_add!(#prv::__GODOT_DOCS_REGISTRY; #prv::DocsPlugin::new::<#class_name>( + #prv::DocsItem::InherentImpl(#prv::InherentImplDocs { + methods: #method_xml_elems, + signals: #signal_xml_elems, + constants: #constant_xml_elems + }) + )); + } + } + + pub fn make_interface_impl_docs_registration( + impl_members: &[venial::ImplMember], + class_name: &Ident, + prv: &TokenStream, + ) -> TokenStream { + let virtual_methods = extract_docs::document_interface_trait_impl(impl_members); + + quote! { + ::godot::sys::plugin_add!(#prv::__GODOT_DOCS_REGISTRY; #prv::DocsPlugin::new::<#class_name>( + #prv::DocsItem::ITraitImpl(#virtual_methods) + )); + } + } +} + +#[cfg(all(feature = "register-docs", since_api = "4.3"))] +pub use docs_generators::*; + +#[cfg(not(all(feature = "register-docs", since_api = "4.3")))] +mod placeholders { + use super::*; + + pub fn make_struct_docs_registration( + _base: String, + _description: &[venial::Attribute], + _fields: &[Field], + _class_name: &Ident, + _prv: &TokenStream, + ) -> TokenStream { + TokenStream::new() + } + + pub fn make_trait_docs_registration( + _functions: &[FuncDefinition], + _constants: &[ConstDefinition], + _signals: &[SignalDefinition], + _class_name: &Ident, + _prv: &proc_macro2::TokenStream, + ) -> TokenStream { + TokenStream::new() + } + + pub fn make_interface_impl_docs_registration( + _impl_members: &[venial::ImplMember], + _class_name: &Ident, + _prv: &TokenStream, + ) -> TokenStream { + TokenStream::new() + } +} + +#[cfg(not(all(feature = "register-docs", since_api = "4.3")))] +pub use placeholders::*; diff --git a/godot-macros/src/lib.rs b/godot-macros/src/lib.rs index 8ba7d92ae..0f35ff4d5 100644 --- a/godot-macros/src/lib.rs +++ b/godot-macros/src/lib.rs @@ -13,7 +13,6 @@ mod bench; mod class; mod derive; -#[cfg(all(feature = "register-docs", since_api = "4.3"))] mod docs; mod ffi_macros; mod gdextension; diff --git a/itest/rust/src/register_tests/constant_test.rs b/itest/rust/src/register_tests/constant_test.rs index ae8806403..9ab6f0066 100644 --- a/itest/rust/src/register_tests/constant_test.rs +++ b/itest/rust/src/register_tests/constant_test.rs @@ -172,8 +172,6 @@ godot::sys::plugin_add!( godot::private::ClassPlugin::new::( godot::private::PluginItem::InherentImpl( godot::private::InherentImpl::new::( - #[cfg(feature = "register-docs")] - Default::default() ) ) ) diff --git a/itest/rust/src/register_tests/register_docs_test.rs b/itest/rust/src/register_tests/register_docs_test.rs index a7951c656..d0c23037e 100644 --- a/itest/rust/src/register_tests/register_docs_test.rs +++ b/itest/rust/src/register_tests/register_docs_test.rs @@ -305,6 +305,20 @@ impl FairlyDocumented { fn other_signal(x: i64); } +#[godot_api(secondary)] +impl FairlyDocumented { + /// Documented method in godot_api secondary block + #[func] + fn secondary_but_documented(&self, _smth: i64) {} +} + +#[godot_api(secondary)] +impl FairlyDocumented { + /// Documented method in other godot_api secondary block + #[func] + fn trinary_but_documented(&self, _smth: i64) {} +} + #[itest] fn test_register_docs() { let xml = find_class_docs("FairlyDocumented"); diff --git a/itest/rust/src/register_tests/res/registered_docs.xml b/itest/rust/src/register_tests/res/registered_docs.xml index a0551a2ba..bf378abdb 100644 --- a/itest/rust/src/register_tests/res/registered_docs.xml +++ b/itest/rust/src/register_tests/res/registered_docs.xml @@ -17,6 +17,14 @@ public class Player : Node2D code&nbsp;block </div>[/codeblock][br][br][url=https://www.google.com/search?q=2+%2B+2+<+5]Google: 2 + 2 < 5[/url][br][br]connect these[br][br]¹ [url=https://example.org]https://example.org[/url][br]² because the glorb doesn't flibble.[br]³ This is the third footnote in order of definition.[br]⁴ Fourth footnote in order of definition.[br]⁵ This is the fifth footnote in order of definition.[br]⁶ sixth footnote in order of definition. + + + + + initialize this + + + @@ -65,11 +73,19 @@ public class Player : Node2D - - - + + + - initialize this + Documented method in godot_api secondary block + + + + + + + + Documented method in other godot_api secondary block From f08c48395b139456557adeead332170be002f016 Mon Sep 17 00:00:00 2001 From: Jan Haller Date: Sun, 20 Jul 2025 21:15:18 +0200 Subject: [PATCH 08/54] Retain spans in newly generated TokenStreams when possible --- godot-macros/src/class/data_models/func.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/godot-macros/src/class/data_models/func.rs b/godot-macros/src/class/data_models/func.rs index 4c31d799c..9c38484a6 100644 --- a/godot-macros/src/class/data_models/func.rs +++ b/godot-macros/src/class/data_models/func.rs @@ -440,8 +440,12 @@ fn maybe_change_parameter_type( && param_ty.tokens.len() == 1 && param_ty.tokens[0].to_string() == "f32" { + // Retain span of input parameter -> for error messages, IDE support, etc. + let mut f64_ident = ident("f64"); + f64_ident.set_span(param_ty.span()); + Ok(venial::TypeExpr { - tokens: vec![TokenTree::Ident(ident("f64"))], + tokens: vec![TokenTree::Ident(f64_ident)], }) } else { Err(param_ty) From fff255c3d35e3cd937cc958c6f0ff760be7bffe1 Mon Sep 17 00:00:00 2001 From: Jan Haller Date: Sun, 20 Jul 2025 22:25:56 +0200 Subject: [PATCH 09/54] Move virtual definitions: sys::godot_virtual_consts -> private::virtuals --- godot-codegen/src/generator/mod.rs | 10 ++-------- ...ual_definition_consts.rs => virtual_definitions.rs} | 4 ++-- godot-codegen/src/lib.rs | 9 ++++++++- .../src/class/data_models/interface_trait_impl.rs | 6 +++--- godot-macros/src/class/derive_godot_class.rs | 2 +- 5 files changed, 16 insertions(+), 15 deletions(-) rename godot-codegen/src/generator/{virtual_definition_consts.rs => virtual_definitions.rs} (92%) diff --git a/godot-codegen/src/generator/mod.rs b/godot-codegen/src/generator/mod.rs index 68082b711..4aa0c379f 100644 --- a/godot-codegen/src/generator/mod.rs +++ b/godot-codegen/src/generator/mod.rs @@ -30,7 +30,7 @@ pub mod notifications; pub mod signals; pub mod sys; pub mod utility_functions; -pub mod virtual_definition_consts; +pub mod virtual_definitions; pub mod virtual_traits; // ---------------------------------------------------------------------------------------------------------------------------------------------- @@ -57,7 +57,6 @@ pub fn generate_sys_module_file(sys_gen_path: &Path, submit_fn: &mut SubmitFn) { pub mod central; pub mod gdextension_interface; pub mod interface; - pub mod virtual_consts; }; submit_fn(sys_gen_path.join("mod.rs"), code); @@ -87,12 +86,6 @@ pub fn generate_sys_classes_file( submit_fn(sys_gen_path.join(filename), code); watch.record(format!("generate_classes_{}_file", api_level.lower())); } - - // From 4.4 onward, generate table that maps all virtual methods to their known hashes. - // This allows Godot to fall back to an older compatibility function if one is not supported. - let code = virtual_definition_consts::make_virtual_consts_file(api, ctx); - submit_fn(sys_gen_path.join("virtual_consts.rs"), code); - watch.record("generate_virtual_consts_file"); } pub fn generate_sys_utilities_file( @@ -132,6 +125,7 @@ pub fn generate_core_mod_file(gen_path: &Path, submit_fn: &mut SubmitFn) { pub mod builtin_classes; pub mod utilities; pub mod native; + pub mod virtuals; }; submit_fn(gen_path.join("mod.rs"), code); diff --git a/godot-codegen/src/generator/virtual_definition_consts.rs b/godot-codegen/src/generator/virtual_definitions.rs similarity index 92% rename from godot-codegen/src/generator/virtual_definition_consts.rs rename to godot-codegen/src/generator/virtual_definitions.rs index 9aa25f632..3e6b636f3 100644 --- a/godot-codegen/src/generator/virtual_definition_consts.rs +++ b/godot-codegen/src/generator/virtual_definitions.rs @@ -9,9 +9,9 @@ use proc_macro2::TokenStream; use quote::quote; use crate::context::Context; -use crate::models::domain::{Class, ClassLike, ExtensionApi, FnDirection, Function}; +use crate::models::domain::{Class, ExtensionApi, FnDirection}; -pub fn make_virtual_consts_file(api: &ExtensionApi, ctx: &mut Context) -> TokenStream { +pub fn make_virtual_definitions_file(api: &ExtensionApi, ctx: &mut Context) -> TokenStream { make_virtual_hashes_for_all_classes(&api.classes, ctx) } diff --git a/godot-codegen/src/lib.rs b/godot-codegen/src/lib.rs index 955c27e13..eb9e4ea75 100644 --- a/godot-codegen/src/lib.rs +++ b/godot-codegen/src/lib.rs @@ -37,7 +37,7 @@ use crate::generator::utility_functions::generate_utilities_file; use crate::generator::{ generate_core_central_file, generate_core_mod_file, generate_sys_builtin_lifecycle_file, generate_sys_builtin_methods_file, generate_sys_central_file, generate_sys_classes_file, - generate_sys_module_file, generate_sys_utilities_file, + generate_sys_module_file, generate_sys_utilities_file, virtual_definitions, }; use crate::models::domain::{ApiView, ExtensionApi}; use crate::models::json::{load_extension_api, JsonExtensionApi}; @@ -173,6 +173,13 @@ pub fn generate_core_files(core_gen_path: &Path) { generate_utilities_file(&api, core_gen_path, &mut submit_fn); watch.record("generate_utilities_file"); + // From 4.4 onward, generate table that maps all virtual methods to their known hashes. + // This allows Godot to fall back to an older compatibility function if one is not supported. + // Also expose tuple signatures of virtual methods. + let code = virtual_definitions::make_virtual_definitions_file(&api, &mut ctx); + submit_fn(core_gen_path.join("virtuals.rs"), code); + watch.record("generate_virtual_definitions"); + // Class files -- currently output in godot-core; could maybe be separated cleaner // Note: deletes entire generated directory! generate_class_files( diff --git a/godot-macros/src/class/data_models/interface_trait_impl.rs b/godot-macros/src/class/data_models/interface_trait_impl.rs index e373cdce7..554191ed6 100644 --- a/godot-macros/src/class/data_models/interface_trait_impl.rs +++ b/godot-macros/src/class/data_models/interface_trait_impl.rs @@ -139,7 +139,7 @@ pub fn transform_trait_impl(mut original_impl: venial::Impl) -> ParseResult ParseResult ::godot::sys::GDExtensionClassCallVirtual { //println!("virtual_call: {}.{}", std::any::type_name::(), name); use ::godot::obj::UserClass as _; - use ::godot::sys::godot_virtual_consts::#trait_base_class as virtuals; + use ::godot::private::virtuals::#trait_base_class as virtuals; #tool_check match #match_expr { @@ -610,7 +610,7 @@ fn handle_regular_virtual_fn<'a>( cfg_attrs, rust_method_name: virtual_method_name, // If ever the `I*` verbatim validation is relaxed (it won't work with use-renames or other weird edge cases), the approach - // with godot_virtual_consts module could be changed to something like the following (GodotBase = nearest Godot base class): + // with godot::private::virtuals module could be changed to something like the following (GodotBase = nearest Godot base class): // __get_virtual_hash::("method") godot_name_hash_constant: quote! { virtuals::#method_name_ident }, signature_info, diff --git a/godot-macros/src/class/derive_godot_class.rs b/godot-macros/src/class/derive_godot_class.rs index 6c6cf49c9..8e2964af6 100644 --- a/godot-macros/src/class/derive_godot_class.rs +++ b/godot-macros/src/class/derive_godot_class.rs @@ -444,7 +444,7 @@ fn make_user_class_impl( if cfg!(since_api = "4.4") { hash_param = quote! { hash: u32, }; matches_ready_hash = quote! { - (name, hash) == ::godot::sys::godot_virtual_consts::Node::ready + (name, hash) == ::godot::private::virtuals::Node::ready }; } else { hash_param = TokenStream::new(); From 854897ba178d51887732c7abf441f25318f14c46 Mon Sep 17 00:00:00 2001 From: Jan Haller Date: Sun, 20 Jul 2025 22:28:54 +0200 Subject: [PATCH 10/54] Declare virtual signatures in central place --- .../src/generator/virtual_definitions.rs | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/godot-codegen/src/generator/virtual_definitions.rs b/godot-codegen/src/generator/virtual_definitions.rs index 3e6b636f3..31b529589 100644 --- a/godot-codegen/src/generator/virtual_definitions.rs +++ b/godot-codegen/src/generator/virtual_definitions.rs @@ -6,7 +6,7 @@ */ use proc_macro2::TokenStream; -use quote::quote; +use quote::{format_ident, quote}; use crate::context::Context; use crate::models::domain::{Class, ExtensionApi, FnDirection}; @@ -21,7 +21,7 @@ fn make_virtual_hashes_for_all_classes(all_classes: &[Class], ctx: &mut Context) .map(|class| make_virtual_hashes_for_class(class, ctx)); quote! { - #![allow(non_snake_case, non_upper_case_globals, unused_imports)] + #![allow(non_snake_case, non_camel_case_types, non_upper_case_globals, unused_imports)] #( #modules )* } @@ -34,6 +34,12 @@ fn make_virtual_hashes_for_class(class: &Class, ctx: &mut Context) -> TokenStrea let use_base_class = if let Some(base_class) = ctx.inheritance_tree().direct_base(class_name) { quote! { pub use super::#base_class::*; + + // For type references in `Sig_*` signature tuples: + pub use crate::builtin::*; + pub use crate::classes::native::*; + pub use crate::obj::Gd; + pub use std::ffi::c_void; } } else { TokenStream::new() @@ -50,14 +56,22 @@ fn make_virtual_hashes_for_class(class: &Class, ctx: &mut Context) -> TokenStrea let rust_name = method.name_ident(); let godot_name_str = method.godot_name(); + let param_types = method.params().iter().map(|p| &p.type_); + + let rust_sig_name = format_ident!("Sig_{rust_name}"); + let sig_decl = quote! { + type #rust_sig_name = ( #(#param_types,)* ); + }; #[cfg(since_api = "4.4")] let constant = quote! { pub const #rust_name: (&'static str, u32) = (#godot_name_str, #hash); + #sig_decl }; #[cfg(before_api = "4.4")] let constant = quote! { pub const #rust_name: &'static str = #godot_name_str; + #sig_decl }; Some(constant) From e3f67f6b2d64f82104293671c9316244f91ada18 Mon Sep 17 00:00:00 2001 From: Jan Haller Date: Fri, 17 Oct 2025 04:26:20 +0200 Subject: [PATCH 11/54] Store signature of virtual methods Attempts to provide type information that's coming from the GDExtension API, rather than from the user (through the macro). This should help in the future with more detailed error messages, by using the API signatures as source of truth. --- .../src/generator/functions_common.rs | 55 ++++++++++------- .../src/generator/virtual_definitions.rs | 17 ++++-- godot-core/src/private.rs | 1 + godot-ffi/src/lib.rs | 1 - godot-macros/src/class/data_models/func.rs | 61 +++++++++++++------ .../class/data_models/interface_trait_impl.rs | 5 +- godot-macros/src/class/derive_godot_class.rs | 19 ++++-- 7 files changed, 104 insertions(+), 55 deletions(-) diff --git a/godot-codegen/src/generator/functions_common.rs b/godot-codegen/src/generator/functions_common.rs index e1600587c..7381bc9ff 100644 --- a/godot-codegen/src/generator/functions_common.rs +++ b/godot-codegen/src/generator/functions_common.rs @@ -603,6 +603,32 @@ pub(crate) fn make_params_exprs<'a>( ret } +/// Returns the type for a virtual method parameter. +/// +/// Generates `Option>` instead of `Gd` for object parameters (which are currently all nullable). +/// +/// Used for consistency between virtual trait definitions and `type Sig = ...` type-safety declarations +/// (which are used to improve compile-time errors on mismatch). +pub(crate) fn make_virtual_param_type( + param_ty: &RustTy, + param_name: &Ident, + function_sig: &dyn Function, +) -> TokenStream { + match param_ty { + // Virtual methods accept Option>, since we don't know whether objects are nullable or required. + RustTy::EngineClass { .. } + if !special_cases::is_class_method_param_required( + function_sig.surrounding_class().unwrap(), + function_sig.godot_name(), + param_name, + ) => + { + quote! { Option<#param_ty> } + } + _ => quote! { #param_ty }, + } +} + /// For virtual functions, returns the parameter declarations, type tokens, and names. pub(crate) fn make_params_exprs_virtual<'a>( method_args: impl Iterator, @@ -614,30 +640,13 @@ pub(crate) fn make_params_exprs_virtual<'a>( let param_name = ¶m.name; let param_ty = ¶m.type_; - match ¶m.type_ { - // Virtual methods accept Option>, since we don't know whether objects are nullable or required. - RustTy::EngineClass { .. } - if !special_cases::is_class_method_param_required( - function_sig.surrounding_class().unwrap(), - function_sig.godot_name(), - param_name, - ) => - { - ret.param_decls - .push(quote! { #param_name: Option<#param_ty> }); - ret.arg_exprs.push(quote! { #param_name }); - ret.callsig_param_types.push(quote! { #param_ty }); - } + // Map parameter types (e.g. virtual functions need Option instead of Gd). + let param_ty_tokens = make_virtual_param_type(param_ty, param_name, function_sig); - // All other methods and parameter types: standard handling. - // For now, virtual methods always receive their parameter by value. - //_ => ret.push_regular(param_name, param_ty, true, false, false), - _ => { - ret.param_decls.push(quote! { #param_name: #param_ty }); - ret.arg_exprs.push(quote! { #param_name }); - ret.callsig_param_types.push(quote! { #param_ty }); - } - } + ret.param_decls + .push(quote! { #param_name: #param_ty_tokens }); + ret.arg_exprs.push(quote! { #param_name }); + ret.callsig_param_types.push(quote! { #param_ty }); } ret diff --git a/godot-codegen/src/generator/virtual_definitions.rs b/godot-codegen/src/generator/virtual_definitions.rs index 31b529589..c8143481b 100644 --- a/godot-codegen/src/generator/virtual_definitions.rs +++ b/godot-codegen/src/generator/virtual_definitions.rs @@ -9,7 +9,8 @@ use proc_macro2::TokenStream; use quote::{format_ident, quote}; use crate::context::Context; -use crate::models::domain::{Class, ExtensionApi, FnDirection}; +use crate::generator::functions_common::make_virtual_param_type; +use crate::models::domain::{Class, ClassLike, ExtensionApi, FnDirection, Function}; pub fn make_virtual_definitions_file(api: &ExtensionApi, ctx: &mut Context) -> TokenStream { make_virtual_hashes_for_all_classes(&api.classes, ctx) @@ -56,21 +57,27 @@ fn make_virtual_hashes_for_class(class: &Class, ctx: &mut Context) -> TokenStrea let rust_name = method.name_ident(); let godot_name_str = method.godot_name(); - let param_types = method.params().iter().map(|p| &p.type_); + + // Generate parameter types, wrapping EngineClass in Option<> just like the trait does + let param_types = method + .params() + .iter() + .map(|param| make_virtual_param_type(¶m.type_, ¶m.name, method)); let rust_sig_name = format_ident!("Sig_{rust_name}"); let sig_decl = quote! { - type #rust_sig_name = ( #(#param_types,)* ); + // Pub to allow "inheritance" from other modules. + pub type #rust_sig_name = ( #(#param_types,)* ); }; #[cfg(since_api = "4.4")] let constant = quote! { - pub const #rust_name: (&'static str, u32) = (#godot_name_str, #hash); + pub const #rust_name: (&str, u32) = (#godot_name_str, #hash); #sig_decl }; #[cfg(before_api = "4.4")] let constant = quote! { - pub const #rust_name: &'static str = #godot_name_str; + pub const #rust_name: &str = #godot_name_str; #sig_decl }; diff --git a/godot-core/src/private.rs b/godot-core/src/private.rs index 9f1ac4009..6e3148a96 100644 --- a/godot-core/src/private.rs +++ b/godot-core/src/private.rs @@ -25,6 +25,7 @@ mod reexport_pub { #[cfg(all(since_api = "4.3", feature = "register-docs"))] pub use crate::docs::{DocsItem, DocsPlugin, InherentImplDocs, StructDocs}; pub use crate::gen::classes::class_macros; + pub use crate::gen::virtuals; // virtual fn names, hashes, signatures #[cfg(feature = "trace")] pub use crate::meta::trace; pub use crate::obj::rtti::ObjectRtti; diff --git a/godot-ffi/src/lib.rs b/godot-ffi/src/lib.rs index a1e2e4156..3807b9356 100644 --- a/godot-ffi/src/lib.rs +++ b/godot-ffi/src/lib.rs @@ -86,7 +86,6 @@ pub use gen::table_editor_classes::*; pub use gen::table_scene_classes::*; pub use gen::table_servers_classes::*; pub use gen::table_utilities::*; -pub use gen::virtual_consts as godot_virtual_consts; pub use global::*; pub use string_cache::StringCache; pub use toolbox::*; diff --git a/godot-macros/src/class/data_models/func.rs b/godot-macros/src/class/data_models/func.rs index 9c38484a6..ecc83c4b0 100644 --- a/godot-macros/src/class/data_models/func.rs +++ b/godot-macros/src/class/data_models/func.rs @@ -51,14 +51,20 @@ impl FuncDefinition { // Virtual methods are non-static by their nature; so there's no support for static ones. pub fn make_virtual_callback( class_name: &Ident, + trait_base_class: &Ident, signature_info: &SignatureInfo, before_kind: BeforeKind, interface_trait: Option<&venial::TypeExpr>, ) -> TokenStream { let method_name = &signature_info.method_name; - let wrapped_method = - make_forwarding_closure(class_name, signature_info, before_kind, interface_trait); + let wrapped_method = make_forwarding_closure( + class_name, + trait_base_class, + signature_info, + before_kind, + interface_trait, + ); let sig_params = signature_info.params_type(); let sig_ret = &signature_info.return_type; @@ -108,6 +114,7 @@ pub fn make_method_registration( let forwarding_closure = make_forwarding_closure( class_name, + class_name, // Not used in this case. signature_info, BeforeKind::Without, interface_trait, @@ -235,6 +242,7 @@ pub enum BeforeKind { /// Returns a closure expression that forwards the parameters to the Rust instance. fn make_forwarding_closure( class_name: &Ident, + trait_base_class: &Ident, signature_info: &SignatureInfo, before_kind: BeforeKind, interface_trait: Option<&venial::TypeExpr>, @@ -269,29 +277,43 @@ fn make_forwarding_closure( ReceiverType::Ref | ReceiverType::Mut => { // Generated default virtual methods (e.g. for ready) may not have an actual implementation (user code), so // all they need to do is call the __before_ready() method. This means the actual method call may be optional. - let method_call = if matches!(before_kind, BeforeKind::OnlyBefore) { - TokenStream::new() + let method_call; + let sig_tuple_annotation; + + if matches!(before_kind, BeforeKind::OnlyBefore) { + sig_tuple_annotation = TokenStream::new(); + method_call = TokenStream::new() + } else if let Some(interface_trait) = interface_trait { + // impl ITrait for Class {...} + // Virtual methods. + + let instance_ref = match signature_info.receiver_type { + ReceiverType::Ref => quote! { &instance }, + ReceiverType::Mut => quote! { &mut instance }, + _ => unreachable!("unexpected receiver type"), // checked above. + }; + + let rust_sig_name = format_ident!("Sig_{method_name}"); + + sig_tuple_annotation = quote! { + : ::godot::private::virtuals::#trait_base_class::#rust_sig_name + }; + method_call = quote! { + <#class_name as #interface_trait>::#method_name( #instance_ref, #(#params),* ) + }; } else { - match interface_trait { - // impl ITrait for Class {...} - Some(interface_trait) => { - let instance_ref = match signature_info.receiver_type { - ReceiverType::Ref => quote! { &instance }, - ReceiverType::Mut => quote! { &mut instance }, - _ => unreachable!("unexpected receiver type"), // checked above. - }; - - quote! { <#class_name as #interface_trait>::#method_name( #instance_ref, #(#params),* ) } - } + // impl Class {...} + // Methods are non-virtual. - // impl Class {...} - None => quote! { instance.#method_name( #(#params),* ) }, - } + sig_tuple_annotation = TokenStream::new(); + method_call = quote! { + instance.#method_name( #(#params),* ) + }; }; quote! { |instance_ptr, params| { - let ( #(#params,)* ) = params; + let ( #(#params,)* ) #sig_tuple_annotation = params; let storage = unsafe { ::godot::private::as_storage::<#class_name>(instance_ptr) }; @@ -307,6 +329,7 @@ fn make_forwarding_closure( // (Absent method is only used in the case of a generated default virtual method, e.g. for ready()). quote! { |instance_ptr, params| { + // Not using `virtual_sig`, because right now, #[func(gd_self)] is only possible for non-virtual methods. let ( #(#params,)* ) = params; let storage = diff --git a/godot-macros/src/class/data_models/interface_trait_impl.rs b/godot-macros/src/class/data_models/interface_trait_impl.rs index 554191ed6..ebcd7525d 100644 --- a/godot-macros/src/class/data_models/interface_trait_impl.rs +++ b/godot-macros/src/class/data_models/interface_trait_impl.rs @@ -178,7 +178,7 @@ pub fn transform_trait_impl(mut original_impl: venial::Impl) -> ParseResult { } impl OverriddenVirtualFn<'_> { - fn make_match_arm(&self, class_name: &Ident) -> TokenStream { + fn make_match_arm(&self, class_name: &Ident, trait_base_class: &Ident) -> TokenStream { let cfg_attrs = self.cfg_attrs.iter(); let godot_name_hash_constant = &self.godot_name_hash_constant; // Lazily generate code for the actual work (calling user function). let method_callback = make_virtual_callback( class_name, + trait_base_class, &self.signature_info, self.before_kind, self.interface_trait.as_ref(), diff --git a/godot-macros/src/class/derive_godot_class.rs b/godot-macros/src/class/derive_godot_class.rs index 8e2964af6..e2b6533b4 100644 --- a/godot-macros/src/class/derive_godot_class.rs +++ b/godot-macros/src/class/derive_godot_class.rs @@ -99,8 +99,12 @@ pub fn derive_godot_class(item: venial::Item) -> ParseResult { TokenStream::new() }; - let (user_class_impl, has_default_virtual) = - make_user_class_impl(class_name, struct_cfg.is_tool, &fields.all_fields); + let (user_class_impl, has_default_virtual) = make_user_class_impl( + class_name, + &struct_cfg.base_ty, + struct_cfg.is_tool, + &fields.all_fields, + ); let mut init_expecter = TokenStream::new(); let mut godot_init_impl = TokenStream::new(); @@ -415,6 +419,7 @@ fn make_oneditor_panic_inits(class_name: &Ident, all_fields: &[Field]) -> TokenS fn make_user_class_impl( class_name: &Ident, + trait_base_class: &Ident, is_tool: bool, all_fields: &[Field], ) -> (TokenStream, bool) { @@ -425,7 +430,6 @@ fn make_user_class_impl( let rpc_registrations = TokenStream::new(); let onready_inits = make_onready_init(all_fields); - let oneditor_panic_inits = make_oneditor_panic_inits(class_name, all_fields); let run_before_ready = !onready_inits.is_empty() || !oneditor_panic_inits.is_empty(); @@ -434,8 +438,13 @@ fn make_user_class_impl( let tool_check = util::make_virtual_tool_check(); let signature_info = SignatureInfo::fn_ready(); - let callback = - make_virtual_callback(class_name, &signature_info, BeforeKind::OnlyBefore, None); + let callback = make_virtual_callback( + class_name, + trait_base_class, + &signature_info, + BeforeKind::OnlyBefore, + None, + ); // See also __virtual_call() codegen. // This doesn't explicitly check if the base class inherits from Node (and thus has `_ready`), but the derive-macro already does From eb29786b63ec28aa29e5cd2d5590d556631c943f Mon Sep 17 00:00:00 2001 From: Yarvin Date: Sun, 19 Oct 2025 16:10:08 +0200 Subject: [PATCH 12/54] Qol: Preserve span of arguments for better compile errors. --- godot-macros/src/class/data_models/func.rs | 41 ++++++++++++++++------ godot-macros/src/util/mod.rs | 10 ++++++ 2 files changed, 41 insertions(+), 10 deletions(-) diff --git a/godot-macros/src/class/data_models/func.rs b/godot-macros/src/class/data_models/func.rs index ecc83c4b0..62b5ec3f8 100644 --- a/godot-macros/src/class/data_models/func.rs +++ b/godot-macros/src/class/data_models/func.rs @@ -5,11 +5,11 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -use proc_macro2::{Group, Ident, TokenStream, TokenTree}; +use proc_macro2::{Group, Ident, Span, TokenStream, TokenTree}; use quote::{format_ident, quote}; use crate::class::RpcAttr; -use crate::util::{bail, bail_fn, ident, safe_ident}; +use crate::util::{bail, bail_fn, ident, safe_ident, to_spanned_tuple}; use crate::{util, ParseResult}; /// Information used for registering a Rust function with Godot. @@ -198,6 +198,7 @@ pub enum ReceiverType { pub struct SignatureInfo { pub method_name: Ident, pub receiver_type: ReceiverType, + pub params_span: Span, pub param_idents: Vec, /// Parameter types *without* receiver. pub param_types: Vec, @@ -214,6 +215,7 @@ impl SignatureInfo { Self { method_name: ident("ready"), receiver_type: ReceiverType::Mut, + params_span: Span::call_site(), param_idents: vec![], param_types: vec![], return_type: quote! { () }, @@ -221,9 +223,14 @@ impl SignatureInfo { } } - pub fn params_type(&self) -> TokenStream { - let param_types = &self.param_types; - quote! { (#(#param_types,)*) } + /// Returns params (e.g. `(v1, v2, v3...)`) of this signature as a properly spanned group. + pub fn params_tuple(&self) -> Group { + to_spanned_tuple(&self.param_idents, self.params_span) + } + + /// Returns param types (e.g. `(f32, f64, GString...)`) of this signature as a properly spanned group. + pub fn params_type(&self) -> Group { + to_spanned_tuple(&self.param_types, self.params_span) } } @@ -249,6 +256,8 @@ fn make_forwarding_closure( ) -> TokenStream { let method_name = &signature_info.method_name; let params = &signature_info.param_idents; + let params_tuple = signature_info.params_tuple(); + let param_ident = Ident::new("params", signature_info.params_span); let instance_decl = match &signature_info.receiver_type { ReceiverType::Ref => quote! { @@ -298,8 +307,18 @@ fn make_forwarding_closure( sig_tuple_annotation = quote! { : ::godot::private::virtuals::#trait_base_class::#rust_sig_name }; + + let method_invocation = TokenStream::from_iter( + quote! {<#class_name as #interface_trait>::#method_name} + .into_iter() + .map(|mut token| { + token.set_span(signature_info.params_span); + token + }), + ); + method_call = quote! { - <#class_name as #interface_trait>::#method_name( #instance_ref, #(#params),* ) + #method_invocation( #instance_ref, #(#params),* ) }; } else { // impl Class {...} @@ -313,7 +332,7 @@ fn make_forwarding_closure( quote! { |instance_ptr, params| { - let ( #(#params,)* ) #sig_tuple_annotation = params; + let #params_tuple #sig_tuple_annotation = #param_ident; let storage = unsafe { ::godot::private::as_storage::<#class_name>(instance_ptr) }; @@ -329,8 +348,8 @@ fn make_forwarding_closure( // (Absent method is only used in the case of a generated default virtual method, e.g. for ready()). quote! { |instance_ptr, params| { - // Not using `virtual_sig`, because right now, #[func(gd_self)] is only possible for non-virtual methods. - let ( #(#params,)* ) = params; + // Not using `virtual_sig`, since virtual methods with `#[func(gd_self)]` are being moved out of the trait to inherent impl. + let #params_tuple = #param_ident; let storage = unsafe { ::godot::private::as_storage::<#class_name>(instance_ptr) }; @@ -344,7 +363,7 @@ fn make_forwarding_closure( // No before-call needed, since static methods are not virtual. quote! { |_, params| { - let ( #(#params,)* ) = params; + let #params_tuple = #param_ident; #class_name::#method_name(#(#params),*) } } @@ -388,6 +407,7 @@ pub(crate) fn into_signature_info( }; let num_params = signature.params.inner.len(); + let params_span = signature.span(); let mut param_idents = Vec::with_capacity(num_params); let mut param_types = Vec::with_capacity(num_params); let ret_type = match signature.return_ty { @@ -443,6 +463,7 @@ pub(crate) fn into_signature_info( SignatureInfo { method_name, receiver_type, + params_span, param_idents, param_types, return_type: ret_type, diff --git a/godot-macros/src/util/mod.rs b/godot-macros/src/util/mod.rs index 082dedd71..cfe292034 100644 --- a/godot-macros/src/util/mod.rs +++ b/godot-macros/src/util/mod.rs @@ -269,6 +269,16 @@ pub fn is_cfg_or_cfg_attr(attr: &venial::Attribute) -> bool { false } +/// Returns group representing properly spanned tuple (e.g. `(arg1, arg2, arg3)`). +/// +/// Use it to preserve span in case if tuple in question is empty (will create properly spanned `()` in such a case). +pub fn to_spanned_tuple(items: &[impl ToTokens], span: Span) -> Group { + let mut group = Group::new(Delimiter::Parenthesis, quote! { #(#items,)* }); + group.set_span(span); + + group +} + pub(crate) fn extract_cfg_attrs( attrs: &[venial::Attribute], ) -> impl IntoIterator { From 57f4206c70a9320fbda63f1b81af44054d0a6f82 Mon Sep 17 00:00:00 2001 From: Yarvin Date: Sat, 18 Oct 2025 11:38:11 +0200 Subject: [PATCH 13/54] Bugfix: Ease `AsArg>>` bounds to make it usable with signals. --- godot-core/src/meta/args/as_arg.rs | 44 ++++++++++++++++++++++++++---- 1 file changed, 38 insertions(+), 6 deletions(-) diff --git a/godot-core/src/meta/args/as_arg.rs b/godot-core/src/meta/args/as_arg.rs index 3de88a4aa..035a6bbdd 100644 --- a/godot-core/src/meta/args/as_arg.rs +++ b/godot-core/src/meta/args/as_arg.rs @@ -9,7 +9,7 @@ use crate::builtin::{GString, NodePath, StringName, Variant}; use crate::meta::sealed::Sealed; use crate::meta::traits::{GodotFfiVariant, GodotNullableFfi}; use crate::meta::{CowArg, FfiArg, GodotType, ObjectArg, ToGodot}; -use crate::obj::{bounds, Bounds, DynGd, Gd, GodotClass, Inherits}; +use crate::obj::{DynGd, Gd, GodotClass, Inherits}; /// Implicit conversions for arguments passed to Godot APIs. /// @@ -263,7 +263,7 @@ where impl AsArg>> for &Gd where T: Inherits, - Base: GodotClass + Bounds, + Base: GodotClass, { fn into_arg<'arg>(self) -> CowArg<'arg, Option>> where @@ -286,7 +286,7 @@ where impl AsArg>> for Option<&Gd> where T: Inherits, - Base: GodotClass + Bounds, + Base: GodotClass, { fn into_arg<'arg>(self) -> CowArg<'arg, Option>> where @@ -313,7 +313,7 @@ impl AsArg>> for &DynGd where T: Inherits, D: ?Sized, - Base: GodotClass + Bounds, + Base: GodotClass, { fn into_arg<'arg>(self) -> CowArg<'arg, Option>> where @@ -337,7 +337,7 @@ impl AsArg>> for &DynGd where T: Inherits, D: ?Sized, - Base: GodotClass + Bounds, + Base: GodotClass, { fn into_arg<'arg>(self) -> CowArg<'arg, Option>> where @@ -361,7 +361,7 @@ impl AsArg>> for Option<&DynGd> where T: Inherits, D: ?Sized, - Base: GodotClass + Bounds, + Base: GodotClass, { fn into_arg<'arg>(self) -> CowArg<'arg, Option>> where @@ -764,3 +764,35 @@ where #[doc(hidden)] // Easier for internal use. pub type ToArg<'r, Via, Pass> = ::Output<'r, Via>; + +/// This type exists only as a place to add doctests for `AsArg`, which do not need to be in the public documentation. +/// +/// `AsArg>` can be used with signals correctly: +/// +/// ```no_run +/// # use godot::prelude::*; +/// #[derive(GodotClass)] +/// #[class(init, base = Node)] +/// struct MyClass { +/// base: Base +/// } +/// +/// #[godot_api] +/// impl MyClass { +/// #[signal] +/// fn signal_optional_user_obj(arg1: Option>); +/// +/// fn foo(&mut self) { +/// let arg = self.to_gd(); +/// // Directly: +/// self.signals().signal_optional_user_obj().emit(&arg); +/// // Via Some: +/// self.signals().signal_optional_user_obj().emit(Some(&arg)); +/// // With None (Note: Gd::null_arg() is restricted to engine classes): +/// self.signals().signal_optional_user_obj().emit(None::>.as_ref()); +/// } +/// } +/// ``` +/// +#[allow(dead_code)] +struct PhantomAsArgDoctests; From 2264fdc9aa8e7736cba0e577999cfc0a78ba4710 Mon Sep 17 00:00:00 2001 From: Yarvin Date: Sun, 12 Oct 2025 11:28:18 +0200 Subject: [PATCH 14/54] Codegen: Support sys types in outgoing Ptrcalls. Post-CR fixes (merged ASAP because it was blocking CI + nightly while generated code was alright). - Don't explicitly declare abstraction for Sys Pointer Types. - Simplify execution. --- godot-codegen/src/context.rs | 26 ++---- godot-codegen/src/conv/type_conversions.rs | 11 ++- godot-codegen/src/generator/central_files.rs | 9 +- godot-codegen/src/generator/mod.rs | 2 +- godot-codegen/src/generator/sys.rs | 82 ------------------- .../src/generator/sys_pointer_types.rs | 37 +++++++++ godot-codegen/src/lib.rs | 29 ++++--- godot-codegen/src/models/domain.rs | 11 ++- .../special_cases/codegen_special_cases.rs | 2 +- 9 files changed, 85 insertions(+), 124 deletions(-) delete mode 100644 godot-codegen/src/generator/sys.rs create mode 100644 godot-codegen/src/generator/sys_pointer_types.rs diff --git a/godot-codegen/src/context.rs b/godot-codegen/src/context.rs index c48785604..8419e2b05 100644 --- a/godot-codegen/src/context.rs +++ b/godot-codegen/src/context.rs @@ -12,7 +12,6 @@ use quote::{format_ident, ToTokens}; use crate::generator::method_tables::MethodTableKey; use crate::generator::notifications; -use crate::generator::sys::SYS_PARAMS; use crate::models::domain::{ArgPassing, GodotTy, RustTy, TyName}; use crate::models::json::{ JsonBuiltinClass, JsonBuiltinMethod, JsonClass, JsonClassConstant, JsonClassMethod, @@ -29,9 +28,6 @@ pub struct Context<'a> { /// Which interface traits are generated (`false` for "Godot-abstract"/final classes). classes_final: HashMap, cached_rust_types: HashMap, - /// Various pointers defined in `gdextension_interface`, for example `GDExtensionInitializationFunction`. - /// Used in some APIs that are not exposed to GDScript. - sys_types: HashSet<&'a str>, notifications_by_class: HashMap>, classes_with_signals: HashSet, notification_enum_names_by_class: HashMap, @@ -47,8 +43,6 @@ impl<'a> Context<'a> { ctx.singletons.insert(class.name.as_str()); } - Self::populate_sys_types(&mut ctx); - ctx.builtin_types.insert("Variant"); // not part of builtin_classes for builtin in api.builtin_classes.iter() { let ty_name = builtin.name.as_str(); @@ -158,14 +152,6 @@ impl<'a> Context<'a> { ctx } - /// Adds Godot pointer types to [`Context`]. - /// - /// Godot pointer types, for example `GDExtensionInitializationFunction`, are defined in `gdextension_interface` - /// but aren't described in `extension_api.json` – despite being used as parameters in various APIs. - fn populate_sys_types(ctx: &mut Context) { - ctx.sys_types.extend(SYS_PARAMS.iter().map(|p| p.type_())); - } - fn populate_notification_constants( class_name: &TyName, constants: &[JsonClassConstant], @@ -266,6 +252,14 @@ impl<'a> Context<'a> { .unwrap_or_else(|| panic!("did not register table index for key {key:?}")) } + /// Yields cached sys pointer types – various pointer types declared in `gdextension_interface` + /// and used as parameters in exposed Godot APIs. + pub fn cached_sys_pointer_types(&self) -> impl Iterator { + self.cached_rust_types + .values() + .filter(|rust_ty| rust_ty.is_sys_pointer()) + } + /// Whether an interface trait is generated for a class. /// /// False if the class is "Godot-abstract"/final, thus there are no virtual functions to inherit. @@ -307,10 +301,6 @@ impl<'a> Context<'a> { self.native_structures_types.contains(ty_name) } - pub fn is_sys(&self, ty_name: &str) -> bool { - self.sys_types.contains(ty_name) - } - pub fn is_singleton(&self, class_name: &TyName) -> bool { self.singletons.contains(class_name.godot_ty.as_str()) } diff --git a/godot-codegen/src/conv/type_conversions.rs b/godot-codegen/src/conv/type_conversions.rs index e22c49d59..443154290 100644 --- a/godot-codegen/src/conv/type_conversions.rs +++ b/godot-codegen/src/conv/type_conversions.rs @@ -170,17 +170,20 @@ fn to_rust_type_uncached(full_ty: &GodotTy, ctx: &mut Context) -> RustTy { ty = ty.replace("const ", ""); } - // .trim() is necessary here, as Godot places a space between a type and the stars when representing a double pointer. - // Example: "int*" but "int **". - if ctx.is_sys(ty.trim()) { + // Sys pointer type defined in `gdextension_interface` and used as param for given method, e.g. `GDExtensionInitializationFunction`. + // Note: we branch here to avoid clashes with actual GDExtension classes. + if ty.starts_with("GDExtension") { let ty = rustify_ty(&ty); return RustTy::RawPointer { - inner: Box::new(RustTy::SysIdent { + inner: Box::new(RustTy::SysPointerType { tokens: quote! { sys::#ty }, }), is_const, }; } + + // .trim() is necessary here, as Godot places a space between a type and the stars when representing a double pointer. + // Example: "int*" but "int **". let inner_type = to_rust_type(ty.trim(), None, ctx); return RustTy::RawPointer { inner: Box::new(inner_type), diff --git a/godot-codegen/src/generator/central_files.rs b/godot-codegen/src/generator/central_files.rs index 8901a3336..5d662073a 100644 --- a/godot-codegen/src/generator/central_files.rs +++ b/godot-codegen/src/generator/central_files.rs @@ -10,7 +10,7 @@ use quote::{format_ident, quote, ToTokens}; use crate::context::Context; use crate::conv; -use crate::generator::sys::make_godotconvert_for_systypes; +use crate::generator::sys_pointer_types::make_godotconvert_for_systypes; use crate::generator::{enums, gdext_build_struct}; use crate::models::domain::ExtensionApi; use crate::util::ident; @@ -61,7 +61,7 @@ pub fn make_core_central_code(api: &ExtensionApi, ctx: &mut Context) -> TokenStr let (global_enum_defs, global_reexported_enum_defs) = make_global_enums(api); let variant_type_traits = make_variant_type_enum(api, false); - let sys_types_godotconvert_impl = make_godotconvert_for_systypes(); + let sys_types_godotconvert_impl = make_godotconvert_for_systypes(ctx); // TODO impl Clone, Debug, PartialEq, PartialOrd, Hash for VariantDispatch // TODO could use try_to().unwrap_unchecked(), since type is already verified. Also directly overload from_variant(). @@ -124,7 +124,10 @@ pub fn make_core_central_code(api: &ExtensionApi, ctx: &mut Context) -> TokenStr #( #global_reexported_enum_defs )* } - #( #sys_types_godotconvert_impl )* + pub mod sys_pointer_types { + use crate::sys; + #( #sys_types_godotconvert_impl )* + } } } diff --git a/godot-codegen/src/generator/mod.rs b/godot-codegen/src/generator/mod.rs index 4aa0c379f..e440c5411 100644 --- a/godot-codegen/src/generator/mod.rs +++ b/godot-codegen/src/generator/mod.rs @@ -28,7 +28,7 @@ pub mod method_tables; pub mod native_structures; pub mod notifications; pub mod signals; -pub mod sys; +pub mod sys_pointer_types; pub mod utility_functions; pub mod virtual_definitions; pub mod virtual_traits; diff --git a/godot-codegen/src/generator/sys.rs b/godot-codegen/src/generator/sys.rs deleted file mode 100644 index 2c265d1d1..000000000 --- a/godot-codegen/src/generator/sys.rs +++ /dev/null @@ -1,82 +0,0 @@ -/* - * Copyright (c) godot-rust; Bromeon and contributors. - * 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 proc_macro2::{Ident, TokenStream}; -use quote::{quote, ToTokens}; - -use crate::util::ident; - -#[allow(unused)] -pub enum SysTypeParam { - /// `* mut SysType` - Mut(&'static str), - /// `* const SysType` - Const(&'static str), -} - -impl SysTypeParam { - pub fn type_(&self) -> &'static str { - match self { - SysTypeParam::Mut(s) | SysTypeParam::Const(s) => s, - } - } - - fn to_ident(&self) -> Ident { - match self { - SysTypeParam::Mut(s) | SysTypeParam::Const(s) => ident(s), - } - } -} - -impl ToTokens for SysTypeParam { - fn to_tokens(&self, tokens: &mut TokenStream) { - let type_ident = self.to_ident(); - match self { - SysTypeParam::Mut(_) => quote! { * mut crate::sys::#type_ident }, - SysTypeParam::Const(_) => quote! { * const crate::sys::#type_ident }, - } - .to_tokens(tokens); - } -} - -/// SysTypes used as parameters in various APIs defined in `extension_api.json`. -// Currently hardcoded and it probably will stay this way – extracting types from gdextension_interface is way too messy. -// Must be different abstraction to avoid clashes with other types passed as pointers (e.g. *Glyph). -pub static SYS_PARAMS: &[SysTypeParam] = &[ - #[cfg(since_api = "4.6")] - SysTypeParam::Const("GDExtensionInitializationFunction"), -]; - -/// Creates `GodotConvert`, `ToGodot` and `FromGodot` impl -/// for SysTypes – various pointer types declared in `gdextension_interface`. -pub fn make_godotconvert_for_systypes() -> Vec { - let mut tokens = vec![]; - for sys_type_param in SYS_PARAMS { - tokens.push( - quote! { - impl crate::meta::GodotConvert for #sys_type_param { - type Via = i64; - - } - - impl crate::meta::ToGodot for #sys_type_param { - type Pass = crate::meta::ByValue; - fn to_godot(&self) -> Self::Via { - * self as i64 - } - } - - impl crate::meta::FromGodot for #sys_type_param { - fn try_from_godot(via: Self::Via) -> Result < Self, crate::meta::error::ConvertError > { - Ok(via as Self) - } - } - } - ) - } - tokens -} diff --git a/godot-codegen/src/generator/sys_pointer_types.rs b/godot-codegen/src/generator/sys_pointer_types.rs new file mode 100644 index 000000000..ef8833406 --- /dev/null +++ b/godot-codegen/src/generator/sys_pointer_types.rs @@ -0,0 +1,37 @@ +/* + * Copyright (c) godot-rust; Bromeon and contributors. + * 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 proc_macro2::TokenStream; +use quote::quote; + +use crate::context::Context; + +/// Creates `GodotConvert`, `ToGodot` and `FromGodot` impl +/// for SysPointerTypes – various pointer types declared in `gdextension_interface` +/// and used as parameters in exposed Godot APIs. +pub fn make_godotconvert_for_systypes(ctx: &mut Context) -> Vec { + ctx.cached_sys_pointer_types().map(|sys_pointer_type| { + quote! { + impl crate::meta::GodotConvert for #sys_pointer_type { + type Via = i64; + } + + impl crate::meta::ToGodot for #sys_pointer_type { + type Pass = crate::meta::ByValue; + fn to_godot(&self) -> Self::Via { + *self as i64 + } + } + + impl crate::meta::FromGodot for #sys_pointer_type { + fn try_from_godot(via: Self::Via) -> Result { + Ok(via as Self) + } + } + } + }).collect() +} diff --git a/godot-codegen/src/lib.rs b/godot-codegen/src/lib.rs index eb9e4ea75..215525739 100644 --- a/godot-codegen/src/lib.rs +++ b/godot-codegen/src/lib.rs @@ -167,19 +167,6 @@ pub fn generate_core_files(core_gen_path: &Path) { // Deallocate all the JSON models; no longer needed for codegen. // drop(json_api); - generate_core_central_file(&api, &mut ctx, core_gen_path, &mut submit_fn); - watch.record("generate_central_file"); - - generate_utilities_file(&api, core_gen_path, &mut submit_fn); - watch.record("generate_utilities_file"); - - // From 4.4 onward, generate table that maps all virtual methods to their known hashes. - // This allows Godot to fall back to an older compatibility function if one is not supported. - // Also expose tuple signatures of virtual methods. - let code = virtual_definitions::make_virtual_definitions_file(&api, &mut ctx); - submit_fn(core_gen_path.join("virtuals.rs"), code); - watch.record("generate_virtual_definitions"); - // Class files -- currently output in godot-core; could maybe be separated cleaner // Note: deletes entire generated directory! generate_class_files( @@ -207,6 +194,22 @@ pub fn generate_core_files(core_gen_path: &Path) { ); watch.record("generate_native_structures_files"); + // Note – generated at the very end since context could be updated while processing other files. + // For example – SysPointerTypes (ones defined in gdextension_interface) are declared only + // as parameters for various APIs. + generate_core_central_file(&api, &mut ctx, core_gen_path, &mut submit_fn); + watch.record("generate_central_file"); + + generate_utilities_file(&api, core_gen_path, &mut submit_fn); + watch.record("generate_utilities_file"); + + // From 4.4 onward, generate table that maps all virtual methods to their known hashes. + // This allows Godot to fall back to an older compatibility function if one is not supported. + // Also expose tuple signatures of virtual methods. + let code = virtual_definitions::make_virtual_definitions_file(&api, &mut ctx); + submit_fn(core_gen_path.join("virtuals.rs"), code); + watch.record("generate_virtual_definitions"); + #[cfg(feature = "codegen-rustfmt")] { rustfmt_files(); diff --git a/godot-codegen/src/models/domain.rs b/godot-codegen/src/models/domain.rs index 3500cc3ff..c7b1d2f4e 100644 --- a/godot-codegen/src/models/domain.rs +++ b/godot-codegen/src/models/domain.rs @@ -728,7 +728,7 @@ pub enum RustTy { BuiltinIdent { ty: Ident, arg_passing: ArgPassing }, /// Pointers declared in `gdextension_interface` such as `sys::GDExtensionInitializationFunction` /// used as parameters in some APIs. - SysIdent { tokens: TokenStream }, + SysPointerType { tokens: TokenStream }, /// `Array` /// @@ -825,6 +825,13 @@ impl RustTy { "i8" | "i16" | "i32" | "i64" | "u8" | "u16" | "u32" | "u64" | "isize" | "usize" ) } + + pub fn is_sys_pointer(&self) -> bool { + let RustTy::RawPointer { inner, .. } = self else { + return false; + }; + matches!(**inner, RustTy::SysPointerType { .. }) + } } impl ToTokens for RustTy { @@ -845,7 +852,7 @@ impl ToTokens for RustTy { RustTy::EngineClass { tokens: path, .. } => path.to_tokens(tokens), RustTy::ExtenderReceiver { tokens: path } => path.to_tokens(tokens), RustTy::GenericArray => quote! { Array }.to_tokens(tokens), - RustTy::SysIdent { tokens: path } => path.to_tokens(tokens), + RustTy::SysPointerType { tokens: path } => path.to_tokens(tokens), } } } diff --git a/godot-codegen/src/special_cases/codegen_special_cases.rs b/godot-codegen/src/special_cases/codegen_special_cases.rs index f254ace0f..222337fef 100644 --- a/godot-codegen/src/special_cases/codegen_special_cases.rs +++ b/godot-codegen/src/special_cases/codegen_special_cases.rs @@ -50,7 +50,7 @@ fn is_type_excluded(ty: &str, ctx: &mut Context) -> bool { RustTy::BuiltinArray { .. } => false, RustTy::GenericArray => false, RustTy::RawPointer { inner, .. } => is_rust_type_excluded(inner), - RustTy::SysIdent { .. } => true, + RustTy::SysPointerType { .. } => true, RustTy::EngineArray { elem_class, .. } => is_class_excluded(elem_class.as_str()), RustTy::EngineEnum { surrounding_class, .. From b0b6069eed49373c78d0c835fd7644a75111dddc Mon Sep 17 00:00:00 2001 From: Joey Eamigh <55670930+JoeyEamigh@users.noreply.github.com> Date: Tue, 21 Oct 2025 16:57:23 -0400 Subject: [PATCH 15/54] Change reload semantics for macOS --- godot-core/src/meta/class_id.rs | 16 ++++++++++++++-- godot-ffi/src/lib.rs | 10 +++++++++- 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/godot-core/src/meta/class_id.rs b/godot-core/src/meta/class_id.rs index d8df04fab..01108837a 100644 --- a/godot-core/src/meta/class_id.rs +++ b/godot-core/src/meta/class_id.rs @@ -309,8 +309,20 @@ impl ClassIdCache { } fn clear(&mut self) { - self.entries.clear(); + // MACOS-PARTIAL-RELOAD: Previous implementation for when upstream fixes `.gdextension` reload. + // self.entries.clear(); + // self.type_to_index.clear(); + // self.string_to_index.clear(); + + // MACOS-PARTIAL-RELOAD: Preserve existing `ClassId` entries when only the `.gdextension` reloads so indices stay valid. + // There are two types of hot reload: `dylib` reload (`dylib` `mtime` newer) unloads and reloads the library, whereas + // `.gdextension` reload (`.gdextension` `mtime` newer) re-initializes the existing `dylib` without unloading it. To handle + // `.gdextension` reload, keep the backing entries (and thus the `string_to_index` map) but drop cached Godot `StringNames` + // and the `TypeId` lookup so they can be rebuilt. + for entry in &mut self.entries { + entry.godot_str = OnceCell::new(); + } + self.type_to_index.clear(); - self.string_to_index.clear(); } } diff --git a/godot-ffi/src/lib.rs b/godot-ffi/src/lib.rs index 3807b9356..253e40942 100644 --- a/godot-ffi/src/lib.rs +++ b/godot-ffi/src/lib.rs @@ -283,7 +283,15 @@ pub unsafe fn initialize( /// # Safety /// See [`initialize`]. pub unsafe fn deinitialize() { - deinitialize_binding() + deinitialize_binding(); + + // MACOS-PARTIAL-RELOAD: Clear the main thread ID to allow re-initialization during hot reload. + #[cfg(not(wasm_nothreads))] + { + if MAIN_THREAD_ID.is_initialized() { + MAIN_THREAD_ID.clear(); + } + } } fn print_preamble(version: GDExtensionGodotVersion) { From 10eac7ea8cfcb98af14563a90e264f13682ab664 Mon Sep 17 00:00:00 2001 From: Joey Eamigh <55670930+JoeyEamigh@users.noreply.github.com> Date: Tue, 21 Oct 2025 16:57:39 -0400 Subject: [PATCH 16/54] Hot-reload CI: test also change of .gdextension file --- itest/hot-reload/godot/run-test.sh | 60 ++++++++++++++++++++++++------ 1 file changed, 48 insertions(+), 12 deletions(-) diff --git a/itest/hot-reload/godot/run-test.sh b/itest/hot-reload/godot/run-test.sh index 828f8e583..98eda93fe 100755 --- a/itest/hot-reload/godot/run-test.sh +++ b/itest/hot-reload/godot/run-test.sh @@ -37,6 +37,39 @@ cleanup() { set -euo pipefail trap cleanup EXIT +godotAwait() { + if [[ $godotPid -ne 0 ]]; then + echo "[Bash] Error: godotAwait called while Godot (PID $godotPid) is still running." + exit 1 + fi + + $GODOT4_BIN -e --headless --path $rel & + godotPid=$! + echo "[Bash] Wait for Godot ready (PID $godotPid)..." + + $GODOT4_BIN --headless --no-header --script ReloadOrchestrator.gd -- await +} + +godotNotify() { + if [[ $godotPid -eq 0 ]]; then + echo "[Bash] Error: godotNotify called but Godot is not running." + exit 1 + fi + + $GODOT4_BIN --headless --no-header --script ReloadOrchestrator.gd -- notify + + echo "[Bash] Wait for Godot exit..." + local status=0 + wait $godotPid + status=$? + echo "[Bash] Godot (PID $godotPid) has completed with status $status." + godotPid=0 + + if [[ $status -ne 0 ]]; then + exit $status + fi +} + echo "[Bash] Start hot-reload integration test..." # Restore un-reloaded file (for local testing). @@ -54,22 +87,25 @@ cargo build -p hot-reload $cargoArgs # Wait briefly so artifacts are present on file system. sleep 0.5 -$GODOT4_BIN -e --headless --path $rel & -godotPid=$! -echo "[Bash] Wait for Godot ready (PID $godotPid)..." +# ---------------------------------------------------------------- +# Test Case 1: Update Rust source and compile to trigger reload. +# ---------------------------------------------------------------- -$GODOT4_BIN --headless --no-header --script ReloadOrchestrator.gd -- await -$GODOT4_BIN --headless --no-header --script ReloadOrchestrator.gd -- replace +echo "[Bash] Scenario 1: Reload after updating Rust source..." +godotAwait +$GODOT4_BIN --headless --no-header --script ReloadOrchestrator.gd -- replace # Compile updated Rust source. cargo build -p hot-reload $cargoArgs +godotNotify -$GODOT4_BIN --headless --no-header --script ReloadOrchestrator.gd -- notify - -echo "[Bash] Wait for Godot exit..." -wait $godotPid -status=$? -echo "[Bash] Godot (PID $godotPid) has completed with status $status." - +# ---------------------------------------------------------------- +# Test Case 2: Touch the .gdextension file to trigger reload. +# ---------------------------------------------------------------- +echo "[Bash] Scenario 2: Reload after touching rust.gdextension..." +godotAwait +# Update timestamp to trigger reload. +touch "$rel/rust.gdextension" +godotNotify From 26bad2e155fe54dcd32951081888acf690ae8dd2 Mon Sep 17 00:00:00 2001 From: Lukas Kalbertodt Date: Wed, 22 Oct 2025 11:46:15 +0200 Subject: [PATCH 17/54] Update to litrs 1.0 This also avoids some string allocations by using try_from. --- Cargo.toml | 2 +- godot-macros/src/docs/extract_docs.rs | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 60fe25aa1..e93fac674 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -39,7 +39,7 @@ which = "7" # * quote: 1.0.37 allows tokenizing CStr. # * venial: 0.6.1 contains some bugfixes. heck = "0.5" -litrs = "0.4" +litrs = { version = "1.0", features = ["proc-macro2"] } markdown = "=1.0.0-alpha.23" nanoserde = "0.2" proc-macro2 = "1.0.80" diff --git a/godot-macros/src/docs/extract_docs.rs b/godot-macros/src/docs/extract_docs.rs index 44ed4fd62..3ccdbd005 100644 --- a/godot-macros/src/docs/extract_docs.rs +++ b/godot-macros/src/docs/extract_docs.rs @@ -115,9 +115,8 @@ fn extract_docs_from_attributes(doc: &[venial::Attribute]) -> impl Iterator Date: Wed, 22 Oct 2025 13:58:32 +0200 Subject: [PATCH 18/54] Deprecate #[class(no_init)] for editor plugins The Godot editor requires default-constructible plugins. Using `no_init` will cause an error when opened in the editor. --- godot-core/src/deprecated.rs | 6 ++++++ godot-macros/src/class/derive_godot_class.rs | 9 ++++++++- itest/rust/src/object_tests/virtual_methods_test.rs | 2 +- 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/godot-core/src/deprecated.rs b/godot-core/src/deprecated.rs index bde072132..00c88f4f8 100644 --- a/godot-core/src/deprecated.rs +++ b/godot-core/src/deprecated.rs @@ -37,6 +37,12 @@ pub use crate::emit_deprecated_warning; // ---------------------------------------------------------------------------------------------------------------------------------------------- // Library-side deprecations -- see usage description above. +#[deprecated = "\n\ + #[class(no_init, base=EditorPlugin)] will crash when opened in the editor.\n\ + EditorPlugin classes are automatically instantiated by Godot and require a default constructor.\n\ + Use #[class(init)] instead, or provide a custom init() function in the IEditorPlugin impl."] +pub const fn class_no_init_editor_plugin() {} + // ---------------------------------------------------------------------------------------------------------------------------------------------- // Godot-side deprecations (we may mark them deprecated but keep support). diff --git a/godot-macros/src/class/derive_godot_class.rs b/godot-macros/src/class/derive_godot_class.rs index e2b6533b4..cfea9ab46 100644 --- a/godot-macros/src/class/derive_godot_class.rs +++ b/godot-macros/src/class/derive_godot_class.rs @@ -290,7 +290,7 @@ pub fn make_existence_check(ident: &Ident) -> TokenStream { // ---------------------------------------------------------------------------------------------------------------------------------------------- // Implementation -#[derive(Copy, Clone)] +#[derive(Copy, Clone, Eq, PartialEq)] enum InitStrategy { Generated, UserDefined, @@ -568,6 +568,13 @@ fn parse_struct_attributes(class: &venial::Struct) -> ParseResult. From ae6885b1ab594dc2f0493e3811041edd62c88277 Mon Sep 17 00:00:00 2001 From: Jan Haller Date: Wed, 22 Oct 2025 14:07:10 +0200 Subject: [PATCH 19/54] Remove old attribute keys, but keep error message for now Concerns: - #[class(editor_plugin)] - #[class(hidden)] - #[init(default = ...)] Those already caused compile errors, since the deprecation functions no longer existed, so this is not a breaking change. However, the new approach produces more readable errors. --- godot-macros/src/class/derive_godot_class.rs | 48 ++++++++------------ 1 file changed, 20 insertions(+), 28 deletions(-) diff --git a/godot-macros/src/class/derive_godot_class.rs b/godot-macros/src/class/derive_godot_class.rs index cfea9ab46..b1696db12 100644 --- a/godot-macros/src/class/derive_godot_class.rs +++ b/godot-macros/src/class/derive_godot_class.rs @@ -531,11 +531,12 @@ fn parse_struct_attributes(class: &venial::Struct) -> ParseResult - ::godot::__deprecated::emit_deprecated_warning!(class_editor_plugin); - }); + // Removed #[class(editor_plugin)] + if let Some(key) = parser.handle_alone_with_span("editor_plugin")? { + return bail!( + key, + "#[class(editor_plugin)] has been removed in favor of #[class(tool, base=EditorPlugin)]", + ); } // #[class(rename = NewName)] @@ -556,20 +557,19 @@ fn parse_struct_attributes(class: &venial::Struct) -> ParseResult - ::godot::__deprecated::emit_deprecated_warning!(class_hidden); - }); + // Removed #[class(hidden)] + if let Some(key) = parser.handle_alone_with_span("hidden")? { + return bail!( + key, + "#[class(hidden)] has been renamed to #[class(internal)]", + ); } parser.finish()?; } // Deprecated: #[class(no_init)] with base=EditorPlugin - if init_strategy == InitStrategy::Absent && base_ty == "EditorPlugin" { + if matches!(init_strategy, InitStrategy::Absent) && base_ty == ident("EditorPlugin") { deprecations.push(quote! { ::godot::__deprecated::emit_deprecated_warning!(class_no_init_editor_plugin); }); @@ -594,6 +594,7 @@ fn parse_fields( ) -> ParseResult { let mut all_fields = vec![]; let mut base_field = Option::::None; + #[allow(unused_mut)] // Less chore when adding/removing deprecations. let mut deprecations = vec![]; let mut errors = vec![]; @@ -640,21 +641,12 @@ fn parse_fields( }); } - // Deprecated #[init(default = expr)] - if let Some((key, default)) = parser.handle_expr_with_key("default")? { - if field.default_val.is_some() { - return bail!( - key, - "Cannot use both `val` and `default` keys in #[init]; prefer using `val`" - ); - } - field.default_val = Some(FieldDefault { - default_val: default, - span: parser.span(), - }); - deprecations.push(quote_spanned! { parser.span()=> - ::godot::__deprecated::emit_deprecated_warning!(init_default); - }) + // Removed #[init(default = ...)] + if let Some((key, _default)) = parser.handle_expr_with_key("default")? { + return bail!( + key, + "#[init(default = ...)] has been renamed to #[init(val = ...)]", + ); } // #[init(node = "PATH")] From ffcfcabfa56efde6b60256e3b5ac837f7da1e9da Mon Sep 17 00:00:00 2001 From: Jan Haller Date: Wed, 22 Oct 2025 13:30:54 +0200 Subject: [PATCH 20/54] Allow opening itests in editor --- itest/godot/TestRunner.gd | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/itest/godot/TestRunner.gd b/itest/godot/TestRunner.gd index a6f1bc503..21b43dafb 100644 --- a/itest/godot/TestRunner.gd +++ b/itest/godot/TestRunner.gd @@ -9,11 +9,11 @@ extends Node class_name GDScriptTestRunner func _ready(): - # Check that tests are invoked from the command line. Loading the editor may break some parts (e.g. generated test code). - # Both checks are needed (it's possible to invoke `godot -e --headless`). - if Engine.is_editor_hint() || DisplayServer.get_name() != 'headless': - push_error("Integration tests must be run in headless mode (without editor).") - get_tree().quit(2) + # Don't run tests when opened in the editor. + if Engine.is_editor_hint(): + if DisplayServer.get_name() == 'headless': + push_error("Opening itest in editor in headless mode is not supported.") + get_tree().quit(2) return # Ensure physics is initialized, for tests that require it. From fe630c46d120139c5ffa364431723211c82b2906 Mon Sep 17 00:00:00 2001 From: Marko Galevski Date: Wed, 22 Oct 2025 22:39:48 +0200 Subject: [PATCH 21/54] renamed builtin hash -> hash_u32; added tests --- godot-core/src/builtin/callable.rs | 10 +++++++--- godot-core/src/builtin/collections/array.rs | 16 +++++++++------- godot-core/src/builtin/collections/dictionary.rs | 6 +++++- godot-core/src/builtin/macros.rs | 4 +++- godot-core/src/builtin/mod.rs | 10 ++++++++++ godot-core/src/builtin/string/gstring.rs | 6 +++++- godot-core/src/builtin/string/node_path.rs | 6 +++++- godot-core/src/builtin/string/string_name.rs | 6 +++++- godot-core/src/builtin/variant/mod.rs | 10 ++++++++++ .../src/builtin_tests/containers/array_test.rs | 2 +- .../builtin_tests/containers/callable_test.rs | 8 ++++---- .../builtin_tests/containers/dictionary_test.rs | 15 +++++++++++---- .../src/builtin_tests/containers/variant_test.rs | 6 +++--- 13 files changed, 78 insertions(+), 27 deletions(-) diff --git a/godot-core/src/builtin/callable.rs b/godot-core/src/builtin/callable.rs index 272c79271..e472cbdf8 100644 --- a/godot-core/src/builtin/callable.rs +++ b/godot-core/src/builtin/callable.rs @@ -446,9 +446,13 @@ impl Callable { InstanceId::try_from_i64(id) } - /// Returns the 32-bit hash value of this callable's object. - /// - /// _Godot equivalent: `hash`_ + crate::declare_hash_u32_method! { + /// Returns the 32-bit hash value of this callable's object. + /// + /// _Godot equivalent: `hash`_ + } + + #[deprecated = "renamed to hash_u32"] pub fn hash(&self) -> u32 { self.as_inner().hash().try_into().unwrap() } diff --git a/godot-core/src/builtin/collections/array.rs b/godot-core/src/builtin/collections/array.rs index cdbd5ba5d..2f3a5188b 100644 --- a/godot-core/src/builtin/collections/array.rs +++ b/godot-core/src/builtin/collections/array.rs @@ -285,14 +285,16 @@ impl Array { 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. + crate::declare_hash_u32_method! { + /// 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. + } + + #[deprecated = "renamed to hash_u32"] 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() } diff --git a/godot-core/src/builtin/collections/dictionary.rs b/godot-core/src/builtin/collections/dictionary.rs index 830e096e5..ebe00e42d 100644 --- a/godot-core/src/builtin/collections/dictionary.rs +++ b/godot-core/src/builtin/collections/dictionary.rs @@ -285,7 +285,11 @@ impl Dictionary { old_value } - /// Returns a 32-bit integer hash value representing the dictionary and its contents. + crate::declare_hash_u32_method! { + /// Returns a 32-bit integer hash value representing the dictionary and its contents. + } + + #[deprecated = "renamed to hash_u32"] #[must_use] pub fn hash(&self) -> u32 { self.as_inner().hash().try_into().unwrap() diff --git a/godot-core/src/builtin/macros.rs b/godot-core/src/builtin/macros.rs index a55e7d9ff..58938c0ef 100644 --- a/godot-core/src/builtin/macros.rs +++ b/godot-core/src/builtin/macros.rs @@ -103,7 +103,9 @@ macro_rules! impl_builtin_traits_inner { ( Hash for $Type:ty ) => { impl std::hash::Hash for $Type { fn hash(&self, state: &mut H) { - self.hash().hash(state) + // The GDExtension interface only deals in `int64_t`, but the engine's own `hash()` function + // actually returns `uint32_t`. + self.hash_u32().hash(state) } } }; diff --git a/godot-core/src/builtin/mod.rs b/godot-core/src/builtin/mod.rs index be0fa446a..f8d832d72 100644 --- a/godot-core/src/builtin/mod.rs +++ b/godot-core/src/builtin/mod.rs @@ -116,6 +116,16 @@ pub mod inner { pub use crate::gen::builtin_classes::*; } +#[macro_export] +macro_rules! declare_hash_u32_method { + ($ ($docs:tt)+ ) => { + $( $docs )+ + pub fn hash_u32(&self) -> u32 { + self.as_inner().hash().try_into().expect("Godot hashes are uint32_t") + } + } +} + // ---------------------------------------------------------------------------------------------------------------------------------------------- // Conversion functions diff --git a/godot-core/src/builtin/string/gstring.rs b/godot-core/src/builtin/string/gstring.rs index 9bc168716..ca2470d67 100644 --- a/godot-core/src/builtin/string/gstring.rs +++ b/godot-core/src/builtin/string/gstring.rs @@ -156,7 +156,11 @@ impl GString { self.as_inner().length().try_into().unwrap() } - /// Returns a 32-bit integer hash value representing the string. + crate::declare_hash_u32_method! { + /// Returns a 32-bit integer hash value representing the string. + } + + #[deprecated = "renamed to hash_u32"] pub fn hash(&self) -> u32 { self.as_inner() .hash() diff --git a/godot-core/src/builtin/string/node_path.rs b/godot-core/src/builtin/string/node_path.rs index 890bdfe64..e57317235 100644 --- a/godot-core/src/builtin/string/node_path.rs +++ b/godot-core/src/builtin/string/node_path.rs @@ -117,7 +117,11 @@ impl NodePath { self.get_name_count() + self.get_subname_count() } - /// Returns a 32-bit integer hash value representing the string. + crate::declare_hash_u32_method! { + /// Returns a 32-bit integer hash value representing the string. + } + + #[deprecated = "renamed to hash_u32"] pub fn hash(&self) -> u32 { self.as_inner() .hash() diff --git a/godot-core/src/builtin/string/string_name.rs b/godot-core/src/builtin/string/string_name.rs index c15d082fe..fd9fe8644 100644 --- a/godot-core/src/builtin/string/string_name.rs +++ b/godot-core/src/builtin/string/string_name.rs @@ -139,7 +139,11 @@ impl StringName { self.as_inner().length() as usize } - /// Returns a 32-bit integer hash value representing the string. + crate::declare_hash_u32_method! { + /// Returns a 32-bit integer hash value representing the string. + } + + #[deprecated = "renamed to hash_u32"] pub fn hash(&self) -> u32 { self.as_inner() .hash() diff --git a/godot-core/src/builtin/variant/mod.rs b/godot-core/src/builtin/variant/mod.rs index 26500857b..5d189ca2e 100644 --- a/godot-core/src/builtin/variant/mod.rs +++ b/godot-core/src/builtin/variant/mod.rs @@ -310,6 +310,16 @@ impl Variant { /// Return Godot's hash value for the variant. /// /// _Godot equivalent : `@GlobalScope.hash()`_ + pub fn hash_u32(&self) -> u32 { + // @GlobalScope.hash() actually calls the VariantUtilityFunctions::hash(&Variant) function (cpp). + // This function calls the passed reference's `hash` method, which returns a uint32_t. + // Therefore, casting this function to u32 is always fine. + unsafe { interface_fn!(variant_hash)(self.var_sys()) } + .try_into() + .expect("Godot hashes are uint32_t") + } + + #[deprecated = "renamed to hash_u32 and type changed to u32"] pub fn hash(&self) -> i64 { unsafe { interface_fn!(variant_hash)(self.var_sys()) } } diff --git a/itest/rust/src/builtin_tests/containers/array_test.rs b/itest/rust/src/builtin_tests/containers/array_test.rs index ea8e3de98..5a5e4b8b4 100644 --- a/itest/rust/src/builtin_tests/containers/array_test.rs +++ b/itest/rust/src/builtin_tests/containers/array_test.rs @@ -102,7 +102,7 @@ fn array_iter_shared() { fn array_hash() { let array = array![1, 2]; // Just testing that it converts successfully from i64 to u32. - array.hash(); + array.hash_u32(); } #[itest] diff --git a/itest/rust/src/builtin_tests/containers/callable_test.rs b/itest/rust/src/builtin_tests/containers/callable_test.rs index 5d3ad1ef2..e2c055a79 100644 --- a/itest/rust/src/builtin_tests/containers/callable_test.rs +++ b/itest/rust/src/builtin_tests/containers/callable_test.rs @@ -73,14 +73,14 @@ fn callable_validity() { fn callable_hash() { let obj = CallableTestObj::new_gd(); assert_eq!( - obj.callable("assign_int").hash(), - obj.callable("assign_int").hash() + obj.callable("assign_int").hash_u32(), + obj.callable("assign_int").hash_u32() ); // Not guaranteed, but unlikely. assert_ne!( - obj.callable("assign_int").hash(), - obj.callable("stringify_int").hash() + obj.callable("assign_int").hash_u32(), + obj.callable("stringify_int").hash_u32() ); } diff --git a/itest/rust/src/builtin_tests/containers/dictionary_test.rs b/itest/rust/src/builtin_tests/containers/dictionary_test.rs index dc0cb4c8c..6bb5f838a 100644 --- a/itest/rust/src/builtin_tests/containers/dictionary_test.rs +++ b/itest/rust/src/builtin_tests/containers/dictionary_test.rs @@ -126,15 +126,22 @@ fn dictionary_hash() { "bar": true, }; - assert_eq!(a.hash(), b.hash(), "equal dictionaries have same hash"); + assert_eq!( + a.hash_u32(), + b.hash_u32(), + "equal dictionaries have same hash" + ); assert_ne!( - a.hash(), - c.hash(), + a.hash_u32(), + c.hash_u32(), "dictionaries with reordered content have different hash" ); // NaNs are not equal (since Godot 4.2) but share same hash. - assert_eq!(vdict! {772: f32::NAN}.hash(), vdict! {772: f32::NAN}.hash()); + assert_eq!( + vdict! {772: f32::NAN}.hash_u32(), + vdict! {772: f32::NAN}.hash_u32() + ); } #[itest] diff --git a/itest/rust/src/builtin_tests/containers/variant_test.rs b/itest/rust/src/builtin_tests/containers/variant_test.rs index c73b97e43..4eadfbb32 100644 --- a/itest/rust/src/builtin_tests/containers/variant_test.rs +++ b/itest/rust/src/builtin_tests/containers/variant_test.rs @@ -713,13 +713,13 @@ fn variant_hash() { ]; for variant in hash_is_not_0 { - assert_ne!(variant.hash(), 0) + assert_ne!(variant.hash_u32(), 0) } for variant in self_equal { - assert_eq!(variant.hash(), variant.hash()) + assert_eq!(variant.hash_u32(), variant.hash_u32()) } - assert_eq!(Variant::nil().hash(), 0); + assert_eq!(Variant::nil().hash_u32(), 0); // It's not guaranteed that different object will have different hash, but it is // extremely unlikely for a collision to happen. From 25425747c196adee5568d3dbebfaa9dff2cee587 Mon Sep 17 00:00:00 2001 From: Jan Haller Date: Wed, 22 Oct 2025 18:20:21 +0200 Subject: [PATCH 22/54] ExtensionLibrary: on_main_loop_* -> on_stage_[de]init + on_main_loop_frame Add InitStage as a superset of InitLevel, with added MainLoop variant. Integrates into the layered approach of on_level_[de]init, but with a new pair of methods due to backwards compatibility. --- godot-core/src/init/mod.rs | 103 +++++++++++++---- godot-ffi/src/lib.rs | 36 ++++++ godot/src/prelude.rs | 2 +- itest/hot-reload/rust/src/lib.rs | 8 +- itest/rust/src/lib.rs | 16 +-- ...{init_level_test.rs => init_stage_test.rs} | 107 +++++++++++------- itest/rust/src/object_tests/mod.rs | 4 +- 7 files changed, 190 insertions(+), 86 deletions(-) rename itest/rust/src/object_tests/{init_level_test.rs => init_stage_test.rs} (65%) diff --git a/godot-core/src/init/mod.rs b/godot-core/src/init/mod.rs index 8aefef1cb..3377031fa 100644 --- a/godot-core/src/init/mod.rs +++ b/godot-core/src/init/mod.rs @@ -16,7 +16,7 @@ use crate::out; mod reexport_pub { #[cfg(not(wasm_nothreads))] pub use super::sys::main_thread_id; - pub use super::sys::{is_main_thread, GdextBuild}; + pub use super::sys::{is_main_thread, GdextBuild, InitStage}; } pub use reexport_pub::*; @@ -29,7 +29,7 @@ struct InitUserData { #[cfg(since_api = "4.5")] unsafe extern "C" fn startup_func() { - E::on_main_loop_startup(); + E::on_stage_init(InitStage::MainLoop); } #[cfg(since_api = "4.5")] @@ -39,7 +39,7 @@ unsafe extern "C" fn frame_func() { #[cfg(since_api = "4.5")] unsafe extern "C" fn shutdown_func() { - E::on_main_loop_shutdown(); + E::on_stage_deinit(InitStage::MainLoop); } #[doc(hidden)] @@ -146,7 +146,7 @@ unsafe extern "C" fn ffi_initialize_layer( // SAFETY: Godot will call this from the main thread, after `__gdext_load_library` where the library is initialized, // and only once per level. unsafe { gdext_on_level_init(level, userdata) }; - E::on_level_init(level); + E::on_stage_init(level.to_stage()); } // Swallow panics. TODO consider crashing if gdext init fails. @@ -172,7 +172,7 @@ unsafe extern "C" fn ffi_deinitialize_layer( drop(Box::from_raw(userdata.cast::())); } - E::on_level_deinit(level); + E::on_stage_deinit(level.to_stage()); gdext_on_level_deinit(level); }); } @@ -327,43 +327,94 @@ pub unsafe trait ExtensionLibrary { InitLevel::Scene } - /// Custom logic when a certain init-level of Godot is loaded. + /// Custom logic when a certain initialization stage is loaded. /// - /// This will only be invoked for levels >= [`Self::min_level()`], in ascending order. Use `if` or `match` to hook to specific levels. + /// This will be invoked for stages >= [`Self::min_level()`], in ascending order. Use `if` or `match` to hook to specific stages. /// + /// The stages are loaded in order: `Core` → `Servers` → `Scene` → `Editor` (if in editor) → `MainLoop` (4.5+). \ + /// The `MainLoop` stage represents the fully initialized state of Godot, after all initialization levels and classes have been loaded. + /// + /// See also [`on_main_loop_frame()`][Self::on_main_loop_frame] for per-frame processing. + /// + /// # Panics /// If the overridden method panics, an error will be printed, but GDExtension loading is **not** aborted. #[allow(unused_variables)] - fn on_level_init(level: InitLevel) { - // Nothing by default. + fn on_stage_init(stage: InitStage) { + #[expect(deprecated)] // Fall back to older API. + stage + .try_to_level() + .inspect(|&level| Self::on_level_init(level)); } - /// Custom logic when a certain init-level of Godot is unloaded. + /// Custom logic when a certain initialization stage is unloaded. /// - /// This will only be invoked for levels >= [`Self::min_level()`], in descending order. Use `if` or `match` to hook to specific levels. + /// This will be invoked for stages >= [`Self::min_level()`], in descending order. Use `if` or `match` to hook to specific stages. /// + /// The stages are unloaded in reverse order: `MainLoop` (4.5+) → `Editor` (if in editor) → `Scene` → `Servers` → `Core`. \ + /// At the time `MainLoop` is deinitialized, all classes are still available. + /// + /// # Panics /// If the overridden method panics, an error will be printed, but GDExtension unloading is **not** aborted. #[allow(unused_variables)] - fn on_level_deinit(level: InitLevel) { - // Nothing by default. + fn on_stage_deinit(stage: InitStage) { + #[expect(deprecated)] // Fall back to older API. + stage + .try_to_level() + .inspect(|&level| Self::on_level_deinit(level)); } - /// Callback that is called after all initialization levels when Godot is fully initialized. - #[cfg(since_api = "4.5")] - fn on_main_loop_startup() { + /// Old callback before [`on_stage_init()`][Self::on_stage_deinit] was added. Does not support `MainLoop` stage. + #[deprecated = "Use `on_stage_init()` instead, which also includes the MainLoop stage."] + #[allow(unused_variables)] + fn on_level_init(level: InitLevel) { // Nothing by default. } - /// Callback that is called for every process frame. - /// - /// This will run after all `_process()` methods on Node, and before `ScriptServer::frame()`. - #[cfg(since_api = "4.5")] - fn on_main_loop_frame() { + /// Old callback before [`on_stage_deinit()`][Self::on_stage_deinit] was added. Does not support `MainLoop` stage. + #[deprecated = "Use `on_stage_deinit()` instead, which also includes the MainLoop stage."] + #[allow(unused_variables)] + fn on_level_deinit(level: InitLevel) { // Nothing by default. } - /// Callback that is called before Godot is shutdown when it is still fully initialized. + /// Callback invoked for every process frame. + /// + /// This is called during the main loop, after Godot is fully initialized. It runs after all + /// [`process()`][crate::classes::INode::process] methods on Node, and before the Godot-internal `ScriptServer::frame()`. + /// This is intended to be the equivalent of [`IScriptLanguageExtension::frame()`][`crate::classes::IScriptLanguageExtension::frame()`] + /// for GDExtension language bindings that don't use the script API. + /// + /// # Example + /// To hook into startup/shutdown of the main loop, use [`on_stage_init()`][Self::on_stage_init] and + /// [`on_stage_deinit()`][Self::on_stage_deinit] and watch for [`InitStage::MainLoop`]. + /// + /// ```no_run + /// # use godot::init::*; + /// # struct MyExtension; + /// #[gdextension] + /// unsafe impl ExtensionLibrary for MyExtension { + /// fn on_stage_init(stage: InitStage) { + /// if stage == InitStage::MainLoop { + /// // Startup code after fully initialized. + /// } + /// } + /// + /// fn on_main_loop_frame() { + /// // Per-frame logic. + /// } + /// + /// fn on_stage_deinit(stage: InitStage) { + /// if stage == InitStage::MainLoop { + /// // Cleanup code before shutdown. + /// } + /// } + /// } + /// ``` + /// + /// # Panics + /// If the overridden method panics, an error will be printed, but execution continues. #[cfg(since_api = "4.5")] - fn on_main_loop_shutdown() { + fn on_main_loop_frame() { // Nothing by default. } @@ -444,8 +495,10 @@ pub enum EditorRunBehavior { /// a different amount of engine functionality is available. Deinitialization happens in reverse order. /// /// See also: -/// - [`ExtensionLibrary::on_level_init()`] -/// - [`ExtensionLibrary::on_level_deinit()`] +/// - [`InitStage`] - Extended initialization stage that includes the main loop. +/// - [`ExtensionLibrary::on_stage_init()`] +/// - [`ExtensionLibrary::on_stage_deinit()`] +/// - [`ExtensionLibrary::on_main_loop_frame()`] pub type InitLevel = sys::InitLevel; // ---------------------------------------------------------------------------------------------------------------------------------------------- diff --git a/godot-ffi/src/lib.rs b/godot-ffi/src/lib.rs index 253e40942..168d67e5f 100644 --- a/godot-ffi/src/lib.rs +++ b/godot-ffi/src/lib.rs @@ -151,8 +151,44 @@ impl InitLevel { Self::Editor => crate::GDEXTENSION_INITIALIZATION_EDITOR, } } + + /// Convert this initialization level to an initialization stage. + pub fn to_stage(self) -> InitStage { + match self { + Self::Core => InitStage::Core, + Self::Servers => InitStage::Servers, + Self::Scene => InitStage::Scene, + Self::Editor => InitStage::Editor, + } + } } +// ---------------------------------------------------------------------------------------------------------------------------------------------- + +pub enum InitStage { + Core, + Servers, + Scene, + Editor, + #[cfg(since_api = "4.5")] + MainLoop, +} + +impl InitStage { + pub fn try_to_level(self) -> Option { + match self { + Self::Core => Some(InitLevel::Core), + Self::Servers => Some(InitLevel::Servers), + Self::Scene => Some(InitLevel::Scene), + Self::Editor => Some(InitLevel::Editor), + #[cfg(since_api = "4.5")] + Self::MainLoop => None, + } + } +} + +// ---------------------------------------------------------------------------------------------------------------------------------------------- + pub struct GdextRuntimeMetadata { godot_version: GDExtensionGodotVersion, } diff --git a/godot/src/prelude.rs b/godot/src/prelude.rs index 9d479b075..1188b155b 100644 --- a/godot/src/prelude.rs +++ b/godot/src/prelude.rs @@ -13,7 +13,7 @@ pub use super::classes::{ pub use super::global::{ godot_error, godot_print, godot_print_rich, godot_script_error, godot_warn, }; -pub use super::init::{gdextension, ExtensionLibrary, InitLevel}; +pub use super::init::{gdextension, ExtensionLibrary, InitLevel, InitStage}; pub use super::meta::error::{ConvertError, IoError}; pub use super::meta::{FromGodot, GodotConvert, ToGodot}; pub use super::obj::{ diff --git a/itest/hot-reload/rust/src/lib.rs b/itest/hot-reload/rust/src/lib.rs index 4b74b7bfc..69d3c9016 100644 --- a/itest/hot-reload/rust/src/lib.rs +++ b/itest/hot-reload/rust/src/lib.rs @@ -11,12 +11,12 @@ struct HotReload; #[gdextension] unsafe impl ExtensionLibrary for HotReload { - fn on_level_init(level: InitLevel) { - println!("[Rust] Init level {level:?}"); + fn on_stage_init(stage: InitStage) { + println!("[Rust] Init stage {stage:?}"); } - fn on_level_deinit(level: InitLevel) { - println!("[Rust] Deinit level {level:?}"); + fn on_stage_deinit(stage: InitStage) { + println!("[Rust] Deinit stage {stage:?}"); } } diff --git a/itest/rust/src/lib.rs b/itest/rust/src/lib.rs index 201977657..20aab16af 100644 --- a/itest/rust/src/lib.rs +++ b/itest/rust/src/lib.rs @@ -5,7 +5,7 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -use godot::init::{gdextension, ExtensionLibrary, InitLevel}; +use godot::init::{gdextension, ExtensionLibrary, InitLevel, InitStage}; mod benchmarks; mod builtin_tests; @@ -24,22 +24,16 @@ unsafe impl ExtensionLibrary for framework::IntegrationTests { InitLevel::Core } - fn on_level_init(level: InitLevel) { - object_tests::on_level_init(level); + fn on_stage_init(stage: InitStage) { + object_tests::on_stage_init(stage); } - #[cfg(since_api = "4.5")] - fn on_main_loop_startup() { - object_tests::on_main_loop_startup(); + fn on_stage_deinit(stage: InitStage) { + object_tests::on_stage_deinit(stage); } #[cfg(since_api = "4.5")] fn on_main_loop_frame() { object_tests::on_main_loop_frame(); } - - #[cfg(since_api = "4.5")] - fn on_main_loop_shutdown() { - object_tests::on_main_loop_shutdown(); - } } diff --git a/itest/rust/src/object_tests/init_level_test.rs b/itest/rust/src/object_tests/init_stage_test.rs similarity index 65% rename from itest/rust/src/object_tests/init_level_test.rs rename to itest/rust/src/object_tests/init_stage_test.rs index ad4fe397c..f919a74f4 100644 --- a/itest/rust/src/object_tests/init_level_test.rs +++ b/itest/rust/src/object_tests/init_stage_test.rs @@ -5,19 +5,16 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -use std::sync::atomic::{AtomicBool, Ordering}; - -use godot::builtin::Rid; +use godot::builtin::{Rid, Variant}; use godot::classes::{Engine, IObject, RenderingServer}; -use godot::init::InitLevel; +use godot::init::InitStage; use godot::obj::{Base, GodotClass, NewAlloc, Singleton}; use godot::register::{godot_api, GodotClass}; use godot::sys::Global; use crate::framework::{expect_panic, itest, runs_release, suppress_godot_print}; -static OBJECT_CALL_HAS_RUN: AtomicBool = AtomicBool::new(false); -static LEVELS_SEEN: Global> = Global::default(); +static STAGES_SEEN: Global> = Global::default(); #[derive(GodotClass)] #[class(base = Object, init)] @@ -26,57 +23,53 @@ struct SomeObject {} #[godot_api] impl SomeObject { #[func] - pub fn set_has_run_true(&self) { - OBJECT_CALL_HAS_RUN.store(true, Ordering::Release); + pub fn method(&self) -> i32 { + 356 } pub fn test() { - assert!(!OBJECT_CALL_HAS_RUN.load(Ordering::Acquire)); let mut some_object = SomeObject::new_alloc(); // Need to go through Godot here as otherwise we bypass the failure. - some_object.call("set_has_run_true", &[]); + let result = some_object.call("method", &[]); + assert_eq!(result, Variant::from(356)); + some_object.free(); } } -// Ensure that the above function has actually run and succeeded. -#[itest] -fn init_level_all_initialized() { - assert!( - OBJECT_CALL_HAS_RUN.load(Ordering::Relaxed), - "Object call function did not run during Core init level" - ); -} - // Ensure that we saw all the init levels expected. #[itest] fn init_level_observed_all() { - let levels_seen = LEVELS_SEEN.lock().clone(); + let actual_stages = STAGES_SEEN.lock().clone(); - assert_eq!(levels_seen[0], InitLevel::Core); - assert_eq!(levels_seen[1], InitLevel::Servers); - assert_eq!(levels_seen[2], InitLevel::Scene); + let mut expected_stages = vec![InitStage::Core, InitStage::Servers, InitStage::Scene]; // In Debug/Editor builds, Editor level is loaded; otherwise not. - let level_3 = levels_seen.get(3); - if runs_release() { - assert_eq!(level_3, None); - } else { - assert_eq!(level_3, Some(&InitLevel::Editor)); + if !runs_release() { + expected_stages.push(InitStage::Editor); } -} -// ---------------------------------------------------------------------------------------------------------------------------------------------- -// Level-specific callbacks + // From Godot 4.5, MainLoop level is added. + #[cfg(since_api = "4.5")] + expected_stages.push(InitStage::MainLoop); -pub fn on_level_init(level: InitLevel) { - LEVELS_SEEN.lock().push(level); + assert_eq!(actual_stages, expected_stages); +} - match level { - InitLevel::Core => on_init_core(), - InitLevel::Servers => on_init_servers(), - InitLevel::Scene => on_init_scene(), - InitLevel::Editor => on_init_editor(), +// ---------------------------------------------------------------------------------------------------------------------------------------------- +// Stage-specific callbacks + +pub fn on_stage_init(stage: InitStage) { + STAGES_SEEN.lock().push(stage); + + match stage { + InitStage::Core => on_init_core(), + InitStage::Servers => on_init_servers(), + InitStage::Scene => on_init_scene(), + InitStage::Editor => on_init_editor(), + #[cfg(since_api = "4.5")] + InitStage::MainLoop => on_init_main_loop(), + _ => { /* Needed due to #[non_exhaustive] */ } } } @@ -138,8 +131,9 @@ impl IObject for MainLoopCallbackSingleton { } } -pub fn on_main_loop_startup() { - // RenderingServer should be accessible in MainLoop startup and shutdown. +#[cfg(since_api = "4.5")] +fn on_init_main_loop() { + // RenderingServer should be accessible in MainLoop init and deinit. let singleton = MainLoopCallbackSingleton::new_alloc(); assert!(singleton.bind().tex.is_valid()); Engine::singleton().register_singleton( @@ -148,11 +142,23 @@ pub fn on_main_loop_startup() { ); } -pub fn on_main_loop_frame() { - // Nothing yet. +#[cfg(not(since_api = "4.5"))] +fn on_init_main_loop() { + // Nothing on older API versions. } -pub fn on_main_loop_shutdown() { +pub fn on_stage_deinit(stage: InitStage) { + match stage { + #[cfg(since_api = "4.5")] + InitStage::MainLoop => on_deinit_main_loop(), + _ => { + // Nothing for other stages yet. + } + } +} + +#[cfg(since_api = "4.5")] +fn on_deinit_main_loop() { let singleton = Engine::singleton() .get_singleton(&MainLoopCallbackSingleton::class_id().to_string_name()) .unwrap() @@ -164,3 +170,18 @@ pub fn on_main_loop_shutdown() { RenderingServer::singleton().free_rid(tex); singleton.free(); } + +#[cfg(not(since_api = "4.5"))] +fn on_deinit_main_loop() { + // Nothing on older API versions. +} + +#[cfg(since_api = "4.5")] +pub fn on_main_loop_frame() { + // Nothing yet. +} + +#[cfg(not(since_api = "4.5"))] +pub fn on_main_loop_frame() { + // Nothing on older API versions. +} diff --git a/itest/rust/src/object_tests/mod.rs b/itest/rust/src/object_tests/mod.rs index 01270a86c..319d8a8dd 100644 --- a/itest/rust/src/object_tests/mod.rs +++ b/itest/rust/src/object_tests/mod.rs @@ -15,7 +15,7 @@ mod enum_test; // `get_property_list` is only supported in Godot 4.3+ #[cfg(since_api = "4.3")] mod get_property_list_test; -mod init_level_test; +mod init_stage_test; mod object_arg_test; mod object_swap_test; mod object_test; @@ -33,4 +33,4 @@ mod virtual_methods_niche_test; mod virtual_methods_test; // Need to test this in the init level method. -pub use init_level_test::*; +pub use init_stage_test::*; From c5ff2d1d679ba0d89de27fe9d1837926ae7e703b Mon Sep 17 00:00:00 2001 From: Jan Haller Date: Thu, 23 Oct 2025 18:39:56 +0200 Subject: [PATCH 23/54] Fix documentation of `InitLevel` We previously used a type alias because `use` statements don't allow RustDoc in this case. However, this caused many broken links and shows up in a "Type Aliases" section, rather than in "Enums" next to `InitStage`. The new setup requires HTML links because the godot-ffi crate doesn't know the symbols, but it ultimately works, and we can also avoid duplicating docs. --- godot-core/src/init/mod.rs | 14 ++------------ godot-ffi/src/lib.rs | 8 ++++++++ 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/godot-core/src/init/mod.rs b/godot-core/src/init/mod.rs index 3377031fa..9d9d958ee 100644 --- a/godot-core/src/init/mod.rs +++ b/godot-core/src/init/mod.rs @@ -445,7 +445,7 @@ pub unsafe trait ExtensionLibrary { /// #[cfg(feature = "nothreads")] /// return None; /// - /// // Tell gdext we add a custom suffix to the binary with thread support. + /// // Tell godot-rust we add a custom suffix to the binary with thread support. /// // Please note that this is not needed if "mycrate.threads.wasm" is used. /// // (You could return `None` as well in that particular case.) /// #[cfg(not(feature = "nothreads"))] @@ -489,17 +489,7 @@ pub enum EditorRunBehavior { // ---------------------------------------------------------------------------------------------------------------------------------------------- -/// Stage of the Godot initialization process. -/// -/// Godot's initialization and deinitialization processes are split into multiple stages, like a stack. At each level, -/// a different amount of engine functionality is available. Deinitialization happens in reverse order. -/// -/// See also: -/// - [`InitStage`] - Extended initialization stage that includes the main loop. -/// - [`ExtensionLibrary::on_stage_init()`] -/// - [`ExtensionLibrary::on_stage_deinit()`] -/// - [`ExtensionLibrary::on_main_loop_frame()`] -pub type InitLevel = sys::InitLevel; +pub use sys::InitLevel; // ---------------------------------------------------------------------------------------------------------------------------------------------- diff --git a/godot-ffi/src/lib.rs b/godot-ffi/src/lib.rs index 168d67e5f..f26519e97 100644 --- a/godot-ffi/src/lib.rs +++ b/godot-ffi/src/lib.rs @@ -113,6 +113,12 @@ static MAIN_THREAD_ID: ManualInitCell = ManualInitCell::n /// /// Godot's initialization and deinitialization processes are split into multiple stages, like a stack. At each level, /// a different amount of engine functionality is available. Deinitialization happens in reverse order. +/// +/// See also: +// Explicit HTML links because this is re-exported in godot::init, and we can't document a `use` statement. +/// - [`InitStage`](enum.InitStage.html): all levels + main loop. +/// - [`ExtensionLibrary::on_stage_init()`](trait.ExtensionLibrary.html#method.on_stage_init) +/// - [`ExtensionLibrary::on_stage_deinit()`](trait.ExtensionLibrary.html#method.on_stage_deinit) #[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)] pub enum InitLevel { /// First level loaded by Godot. Builtin types are available, classes are not. @@ -165,6 +171,8 @@ impl InitLevel { // ---------------------------------------------------------------------------------------------------------------------------------------------- +#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)] +#[non_exhaustive] pub enum InitStage { Core, Servers, From 025b1fb7478c2a9103d2a81707faf52a70f924de Mon Sep 17 00:00:00 2001 From: Jan Haller Date: Thu, 23 Oct 2025 19:35:11 +0200 Subject: [PATCH 24/54] Extract InitLevel + InitStage to separate file --- godot-ffi/src/init_level.rs | 120 ++++++++++++++++++++++++++++++++++++ godot-ffi/src/lib.rs | 88 +------------------------- 2 files changed, 122 insertions(+), 86 deletions(-) create mode 100644 godot-ffi/src/init_level.rs diff --git a/godot-ffi/src/init_level.rs b/godot-ffi/src/init_level.rs new file mode 100644 index 000000000..47d515f93 --- /dev/null +++ b/godot-ffi/src/init_level.rs @@ -0,0 +1,120 @@ +/* + * Copyright (c) godot-rust; Bromeon and contributors. + * 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/. + */ + +/// Stage of the Godot initialization process. +/// +/// Godot's initialization and deinitialization processes are split into multiple stages, like a stack. At each level, +/// a different amount of engine functionality is available. Deinitialization happens in reverse order. +/// +/// See also: +// Explicit HTML links because this is re-exported in godot::init, and we can't document a `use` statement. +/// - [`InitStage`](enum.InitStage.html): all levels + main loop. +/// - [`ExtensionLibrary::on_stage_init()`](trait.ExtensionLibrary.html#method.on_stage_init) +/// - [`ExtensionLibrary::on_stage_deinit()`](trait.ExtensionLibrary.html#method.on_stage_deinit) +#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)] +pub enum InitLevel { + /// First level loaded by Godot. Builtin types are available, classes are not. + Core, + + /// Second level loaded by Godot. Only server classes and builtins are available. + Servers, + + /// Third level loaded by Godot. Most classes are available. + Scene, + + /// Fourth level loaded by Godot, only in the editor. All classes are available. + Editor, +} + +impl InitLevel { + #[doc(hidden)] + pub fn from_sys(level: crate::GDExtensionInitializationLevel) -> Self { + match level { + crate::GDEXTENSION_INITIALIZATION_CORE => Self::Core, + crate::GDEXTENSION_INITIALIZATION_SERVERS => Self::Servers, + crate::GDEXTENSION_INITIALIZATION_SCENE => Self::Scene, + crate::GDEXTENSION_INITIALIZATION_EDITOR => Self::Editor, + _ => { + eprintln!("WARNING: unknown initialization level {level}"); + Self::Scene + } + } + } + + #[doc(hidden)] + pub fn to_sys(self) -> crate::GDExtensionInitializationLevel { + match self { + Self::Core => crate::GDEXTENSION_INITIALIZATION_CORE, + Self::Servers => crate::GDEXTENSION_INITIALIZATION_SERVERS, + Self::Scene => crate::GDEXTENSION_INITIALIZATION_SCENE, + Self::Editor => crate::GDEXTENSION_INITIALIZATION_EDITOR, + } + } + + /// Convert this initialization level to an initialization stage. + pub fn to_stage(self) -> InitStage { + match self { + Self::Core => InitStage::Core, + Self::Servers => InitStage::Servers, + Self::Scene => InitStage::Scene, + Self::Editor => InitStage::Editor, + } + } +} + +// ---------------------------------------------------------------------------------------------------------------------------------------------- + +/// Extended initialization stage that includes both initialization levels and the main loop. +/// +/// This enum extends [`InitLevel`] with a `MainLoop` variant, representing the fully initialized state of Godot +/// after all initialization levels have been loaded and before any deinitialization begins. +/// +/// During initialization, stages are loaded in order: `Core` → `Servers` → `Scene` → `Editor` (if in editor) → `MainLoop`. \ +/// During deinitialization, stages are unloaded in reverse order. +/// +/// See also: +/// - [`InitLevel`](enum.InitLevel.html): only levels, without `MainLoop`. +/// - [`ExtensionLibrary::on_stage_init()`](trait.ExtensionLibrary.html#method.on_stage_init) +/// - [`ExtensionLibrary::on_stage_deinit()`](trait.ExtensionLibrary.html#method.on_stage_deinit) +/// - [`ExtensionLibrary::on_main_loop_frame()`](trait.ExtensionLibrary.html#method.on_main_loop_frame) +#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)] +#[non_exhaustive] +pub enum InitStage { + /// First level loaded by Godot. Builtin types are available, classes are not. + Core, + + /// Second level loaded by Godot. Only server classes and builtins are available. + Servers, + + /// Third level loaded by Godot. Most classes are available. + Scene, + + /// Fourth level loaded by Godot, only in the editor. All classes are available. + Editor, + + /// The main loop stage, representing the fully initialized state of Godot. + /// + /// This variant is only available in Godot 4.5+. In earlier versions, it will never be passed to callbacks. + #[cfg(since_api = "4.5")] + MainLoop, +} + +impl InitStage { + /// Try to convert this initialization stage to an initialization level. + /// + /// Returns `None` for [`InitStage::MainLoop`], as it doesn't correspond to a Godot initialization level. + pub fn try_to_level(self) -> Option { + match self { + Self::Core => Some(InitLevel::Core), + Self::Servers => Some(InitLevel::Servers), + Self::Scene => Some(InitLevel::Scene), + Self::Editor => Some(InitLevel::Editor), + #[cfg(since_api = "4.5")] + Self::MainLoop => None, + } + } +} diff --git a/godot-ffi/src/lib.rs b/godot-ffi/src/lib.rs index f26519e97..ad7e6655e 100644 --- a/godot-ffi/src/lib.rs +++ b/godot-ffi/src/lib.rs @@ -87,6 +87,7 @@ pub use gen::table_scene_classes::*; pub use gen::table_servers_classes::*; pub use gen::table_utilities::*; pub use global::*; +pub use init_level::*; pub use string_cache::StringCache; pub use toolbox::*; @@ -98,6 +99,7 @@ pub use crate::godot_ffi::{ // API to access Godot via FFI mod binding; +mod init_level; pub use binding::*; use binding::{ @@ -109,92 +111,6 @@ use binding::{ #[cfg(not(wasm_nothreads))] static MAIN_THREAD_ID: ManualInitCell = ManualInitCell::new(); -/// Stage of the Godot initialization process. -/// -/// Godot's initialization and deinitialization processes are split into multiple stages, like a stack. At each level, -/// a different amount of engine functionality is available. Deinitialization happens in reverse order. -/// -/// See also: -// Explicit HTML links because this is re-exported in godot::init, and we can't document a `use` statement. -/// - [`InitStage`](enum.InitStage.html): all levels + main loop. -/// - [`ExtensionLibrary::on_stage_init()`](trait.ExtensionLibrary.html#method.on_stage_init) -/// - [`ExtensionLibrary::on_stage_deinit()`](trait.ExtensionLibrary.html#method.on_stage_deinit) -#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)] -pub enum InitLevel { - /// First level loaded by Godot. Builtin types are available, classes are not. - Core, - - /// Second level loaded by Godot. Only server classes and builtins are available. - Servers, - - /// Third level loaded by Godot. Most classes are available. - Scene, - - /// Fourth level loaded by Godot, only in the editor. All classes are available. - Editor, -} - -impl InitLevel { - #[doc(hidden)] - pub fn from_sys(level: crate::GDExtensionInitializationLevel) -> Self { - match level { - crate::GDEXTENSION_INITIALIZATION_CORE => Self::Core, - crate::GDEXTENSION_INITIALIZATION_SERVERS => Self::Servers, - crate::GDEXTENSION_INITIALIZATION_SCENE => Self::Scene, - crate::GDEXTENSION_INITIALIZATION_EDITOR => Self::Editor, - _ => { - eprintln!("WARNING: unknown initialization level {level}"); - Self::Scene - } - } - } - #[doc(hidden)] - pub fn to_sys(self) -> crate::GDExtensionInitializationLevel { - match self { - Self::Core => crate::GDEXTENSION_INITIALIZATION_CORE, - Self::Servers => crate::GDEXTENSION_INITIALIZATION_SERVERS, - Self::Scene => crate::GDEXTENSION_INITIALIZATION_SCENE, - Self::Editor => crate::GDEXTENSION_INITIALIZATION_EDITOR, - } - } - - /// Convert this initialization level to an initialization stage. - pub fn to_stage(self) -> InitStage { - match self { - Self::Core => InitStage::Core, - Self::Servers => InitStage::Servers, - Self::Scene => InitStage::Scene, - Self::Editor => InitStage::Editor, - } - } -} - -// ---------------------------------------------------------------------------------------------------------------------------------------------- - -#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)] -#[non_exhaustive] -pub enum InitStage { - Core, - Servers, - Scene, - Editor, - #[cfg(since_api = "4.5")] - MainLoop, -} - -impl InitStage { - pub fn try_to_level(self) -> Option { - match self { - Self::Core => Some(InitLevel::Core), - Self::Servers => Some(InitLevel::Servers), - Self::Scene => Some(InitLevel::Scene), - Self::Editor => Some(InitLevel::Editor), - #[cfg(since_api = "4.5")] - Self::MainLoop => None, - } - } -} - // ---------------------------------------------------------------------------------------------------------------------------------------------- pub struct GdextRuntimeMetadata { From 959c85b82d65b57a6b9ce1e956c7ccc8ef3254ed Mon Sep 17 00:00:00 2001 From: Jan Haller Date: Thu, 23 Oct 2025 22:15:14 +0200 Subject: [PATCH 25/54] Update crate version: 0.4.0 -> 0.4.1 --- Changelog.md | 36 +++++++++++++++++++++++++++++++++++- godot-bindings/Cargo.toml | 2 +- godot-cell/Cargo.toml | 2 +- godot-codegen/Cargo.toml | 6 +++--- godot-core/Cargo.toml | 10 +++++----- godot-ffi/Cargo.toml | 8 ++++---- godot-macros/Cargo.toml | 4 ++-- godot/Cargo.toml | 8 ++++---- 8 files changed, 55 insertions(+), 21 deletions(-) diff --git a/Changelog.md b/Changelog.md index c99f09692..f49add90b 100644 --- a/Changelog.md +++ b/Changelog.md @@ -10,12 +10,46 @@ Cutting-edge API docs of the `master` branch are available [here](https://godot- ## Quick navigation -- [v0.4.0](#v040) +- [v0.4.0](#v040), [v0.4.1](#v041) - [v0.3.0](#v030), [v0.3.1](#v031), [v0.3.2](#v032), [v0.3.3](#v033), [v0.3.4](#v034), [v0.3.5](#v035) - [v0.2.0](#v020), [v0.2.1](#v021), [v0.2.2](#v022), [v0.2.3](#v023), [v0.2.4](#v024) - [v0.1.1](#v011), [v0.1.2](#v012), [v0.1.3](#v013) +## [v0.4.1](https://docs.rs/godot/0.4.1) + +_23 October 2025_ + +### 🌻 Features + +- Add main loop callbacks to `ExtensionLibrary` ([#1313](https://github.com/godot-rust/gdext/pull/1313), [#1380](https://github.com/godot-rust/gdext/pull/1380)) +- Class Docs – register docs in `#[godot_api(secondary)]`, simplify docs registration logic ([#1355](https://github.com/godot-rust/gdext/pull/1355)) +- Codegen: support sys types in engine APIs ([#1363](https://github.com/godot-rust/gdext/pull/1363), [#1365](https://github.com/godot-rust/gdext/pull/1365)) + +### 📈 Performance + +- Use Rust `str` instead of `CStr` in `ClassIdSource` ([#1334](https://github.com/godot-rust/gdext/pull/1334)) + +### 🧹 Quality of life + +- Preserve doc comments for signal ([#1353](https://github.com/godot-rust/gdext/pull/1353)) +- Provide error context for typed array clone check ([#1348](https://github.com/godot-rust/gdext/pull/1348)) +- Improve spans; use tuple type for virtual signatures ([#1370](https://github.com/godot-rust/gdext/pull/1370)) +- Preserve span of arguments for better compile errors ([#1373](https://github.com/godot-rust/gdext/pull/1373)) +- Update to litrs 1.0 ([#1377](https://github.com/godot-rust/gdext/pull/1377)) +- Allow opening itests in editor ([#1379](https://github.com/godot-rust/gdext/pull/1379)) + +### 🛠️ Bugfixes + +- Ease `AsArg>>` bounds to make it usable with signals ([#1371](https://github.com/godot-rust/gdext/pull/1371)) +- Handle panic in OnReady `auto_init` ([#1351](https://github.com/godot-rust/gdext/pull/1351)) +- Update `GFile::read_as_gstring_entire()` after Godot removes `skip_cr` parameter ([#1349](https://github.com/godot-rust/gdext/pull/1349)) +- Fix `Callable::from_sync_fn` doc example using deprecated `Result` return ([#1347](https://github.com/godot-rust/gdext/pull/1347)) +- Deprecate `#[class(no_init)]` for editor plugins ([#1378](https://github.com/godot-rust/gdext/pull/1378)) +- Initialize and cache proper return value for generic, typed array ([#1357](https://github.com/godot-rust/gdext/pull/1357)) +- Fix hot-reload crashes on macOS when the `.gdextension` file changes ([#1367](https://github.com/godot-rust/gdext/pull/1367)) + + ## [v0.4.0](https://docs.rs/godot/0.4.0) _29 September 2025_ diff --git a/godot-bindings/Cargo.toml b/godot-bindings/Cargo.toml index 8f5980866..9305c9b2f 100644 --- a/godot-bindings/Cargo.toml +++ b/godot-bindings/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "godot-bindings" -version = "0.4.0" +version = "0.4.1" edition = "2021" rust-version = "1.87" license = "MPL-2.0" diff --git a/godot-cell/Cargo.toml b/godot-cell/Cargo.toml index 35d4e2697..e2a5c7746 100644 --- a/godot-cell/Cargo.toml +++ b/godot-cell/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "godot-cell" -version = "0.4.0" +version = "0.4.1" edition = "2021" rust-version = "1.87" license = "MPL-2.0" diff --git a/godot-codegen/Cargo.toml b/godot-codegen/Cargo.toml index f8a2f6137..60032a960 100644 --- a/godot-codegen/Cargo.toml +++ b/godot-codegen/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "godot-codegen" -version = "0.4.0" +version = "0.4.1" edition = "2021" rust-version = "1.87" license = "MPL-2.0" @@ -22,7 +22,7 @@ experimental-godot-api = [] experimental-threads = [] [dependencies] -godot-bindings = { path = "../godot-bindings", version = "=0.4.0" } +godot-bindings = { path = "../godot-bindings", version = "=0.4.1" } heck = { workspace = true } nanoserde = { workspace = true } @@ -31,7 +31,7 @@ quote = { workspace = true } regex = { workspace = true } [build-dependencies] -godot-bindings = { path = "../godot-bindings", version = "=0.4.0" } # emit_godot_version_cfg +godot-bindings = { path = "../godot-bindings", version = "=0.4.1" } # emit_godot_version_cfg # https://docs.rs/about/metadata [package.metadata.docs.rs] diff --git a/godot-core/Cargo.toml b/godot-core/Cargo.toml index f45ccd38a..a31cd61a1 100644 --- a/godot-core/Cargo.toml +++ b/godot-core/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "godot-core" -version = "0.4.0" +version = "0.4.1" edition = "2021" rust-version = "1.87" license = "MPL-2.0" @@ -39,16 +39,16 @@ api-4-5 = ["godot-ffi/api-4-5"] # ]] [dependencies] -godot-ffi = { path = "../godot-ffi", version = "=0.4.0" } +godot-ffi = { path = "../godot-ffi", version = "=0.4.1" } # See https://docs.rs/glam/latest/glam/index.html#feature-gates glam = { workspace = true } serde = { workspace = true, optional = true } -godot-cell = { path = "../godot-cell", version = "=0.4.0" } +godot-cell = { path = "../godot-cell", version = "=0.4.1" } [build-dependencies] -godot-bindings = { path = "../godot-bindings", version = "=0.4.0" } -godot-codegen = { path = "../godot-codegen", version = "=0.4.0" } +godot-bindings = { path = "../godot-bindings", version = "=0.4.1" } +godot-codegen = { path = "../godot-codegen", version = "=0.4.1" } # Reverse dev dependencies so doctests can use `godot::` prefix. [dev-dependencies] diff --git a/godot-ffi/Cargo.toml b/godot-ffi/Cargo.toml index d5d21f48a..f99655d0a 100644 --- a/godot-ffi/Cargo.toml +++ b/godot-ffi/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "godot-ffi" -version = "0.4.0" +version = "0.4.1" edition = "2021" rust-version = "1.87" license = "MPL-2.0" @@ -37,11 +37,11 @@ libc = { workspace = true } [target.'cfg(target_family = "wasm")'.dependencies] # Only needed for WASM identifier generation. -godot-macros = { path = "../godot-macros", version = "=0.4.0", features = ["experimental-wasm"] } +godot-macros = { path = "../godot-macros", version = "=0.4.1", features = ["experimental-wasm"] } [build-dependencies] -godot-bindings = { path = "../godot-bindings", version = "=0.4.0" } -godot-codegen = { path = "../godot-codegen", version = "=0.4.0" } +godot-bindings = { path = "../godot-bindings", version = "=0.4.1" } +godot-codegen = { path = "../godot-codegen", version = "=0.4.1" } # https://docs.rs/about/metadata [package.metadata.docs.rs] diff --git a/godot-macros/Cargo.toml b/godot-macros/Cargo.toml index 2110bde96..fb6f3a80f 100644 --- a/godot-macros/Cargo.toml +++ b/godot-macros/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "godot-macros" -version = "0.4.0" +version = "0.4.1" edition = "2021" rust-version = "1.87" license = "MPL-2.0" @@ -29,7 +29,7 @@ litrs = { workspace = true, optional = true } venial = { workspace = true } [build-dependencies] -godot-bindings = { path = "../godot-bindings", version = "=0.4.0" } # emit_godot_version_cfg +godot-bindings = { path = "../godot-bindings", version = "=0.4.1" } # emit_godot_version_cfg # Reverse dev dependencies so doctests can use `godot::` prefix. [dev-dependencies] diff --git a/godot/Cargo.toml b/godot/Cargo.toml index 4fb405634..7a373841a 100644 --- a/godot/Cargo.toml +++ b/godot/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "godot" -version = "0.4.0" +version = "0.4.1" edition = "2021" rust-version = "1.87" license = "MPL-2.0" @@ -10,7 +10,7 @@ description = "Rust bindings for Godot 4" authors = ["Bromeon", "godot-rust contributors"] repository = "https://github.com/godot-rust/gdext" homepage = "https://godot-rust.github.io" -documentation = "https://docs.rs/godot/0.4.0" +documentation = "https://docs.rs/godot/0.4.1" readme = "crate-readme.md" [features] @@ -47,8 +47,8 @@ __debug-log = ["godot-core/debug-log"] __trace = ["godot-core/trace"] [dependencies] -godot-core = { path = "../godot-core", version = "=0.4.0" } -godot-macros = { path = "../godot-macros", version = "=0.4.0" } +godot-core = { path = "../godot-core", version = "=0.4.1" } +godot-macros = { path = "../godot-macros", version = "=0.4.1" } # https://docs.rs/about/metadata [package.metadata.docs.rs] From 3d44fb683a37269d37d97629069da2c1b526394f Mon Sep 17 00:00:00 2001 From: Jan Haller Date: Thu, 23 Oct 2025 22:48:46 +0200 Subject: [PATCH 26/54] Compat layer for old main-loop API --- godot-core/src/init/mod.rs | 28 ++++++++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/godot-core/src/init/mod.rs b/godot-core/src/init/mod.rs index 9d9d958ee..842d6870b 100644 --- a/godot-core/src/init/mod.rs +++ b/godot-core/src/init/mod.rs @@ -339,11 +339,16 @@ pub unsafe trait ExtensionLibrary { /// # Panics /// If the overridden method panics, an error will be printed, but GDExtension loading is **not** aborted. #[allow(unused_variables)] + #[expect(deprecated)] // Fall back to older API. fn on_stage_init(stage: InitStage) { - #[expect(deprecated)] // Fall back to older API. stage .try_to_level() .inspect(|&level| Self::on_level_init(level)); + + #[cfg(since_api = "4.5")] // Compat layer. + if stage == InitStage::MainLoop { + Self::on_main_loop_startup(); + } } /// Custom logic when a certain initialization stage is unloaded. @@ -356,8 +361,13 @@ pub unsafe trait ExtensionLibrary { /// # Panics /// If the overridden method panics, an error will be printed, but GDExtension unloading is **not** aborted. #[allow(unused_variables)] + #[expect(deprecated)] // Fall back to older API. fn on_stage_deinit(stage: InitStage) { - #[expect(deprecated)] // Fall back to older API. + #[cfg(since_api = "4.5")] // Compat layer. + if stage == InitStage::MainLoop { + Self::on_main_loop_shutdown(); + } + stage .try_to_level() .inspect(|&level| Self::on_level_deinit(level)); @@ -377,6 +387,20 @@ pub unsafe trait ExtensionLibrary { // Nothing by default. } + #[cfg(since_api = "4.5")] + #[deprecated = "Use `on_stage_init(InitStage::MainLoop)` instead."] + #[doc(hidden)] // Added by mistake -- works but don't advertise. + fn on_main_loop_startup() { + // Nothing by default. + } + + #[cfg(since_api = "4.5")] + #[deprecated = "Use `on_stage_deinit(InitStage::MainLoop)` instead."] + #[doc(hidden)] // Added by mistake -- works but don't advertise. + fn on_main_loop_shutdown() { + // Nothing by default. + } + /// Callback invoked for every process frame. /// /// This is called during the main loop, after Godot is fully initialized. It runs after all From 21a85e2833e461d329d5aeb5d71fd5f6020d9fe5 Mon Sep 17 00:00:00 2001 From: Jan Haller Date: Fri, 24 Oct 2025 18:11:46 +0200 Subject: [PATCH 27/54] Fix ConvertError Display formatting for custom errors Previously, custom errors were formatted with Debug impl, which wrapped the error message in Some("..."). Now properly unwraps the Option and uses Display impl. --- godot-core/src/meta/error/convert_error.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/godot-core/src/meta/error/convert_error.rs b/godot-core/src/meta/error/convert_error.rs index 110cb3aa8..12fa2f296 100644 --- a/godot-core/src/meta/error/convert_error.rs +++ b/godot-core/src/meta/error/convert_error.rs @@ -171,7 +171,10 @@ impl fmt::Display for ErrorKind { Self::FromGodot(from_godot) => write!(f, "{from_godot}"), Self::FromVariant(from_variant) => write!(f, "{from_variant}"), Self::FromFfi(from_ffi) => write!(f, "{from_ffi}"), - Self::Custom(cause) => write!(f, "{cause:?}"), + Self::Custom(cause) => match cause { + Some(c) => write!(f, "{c}"), + None => write!(f, "custom error"), + }, } } } From c5f30423920cb850097d7d8878500af4c2f246b5 Mon Sep 17 00:00:00 2001 From: Jan Haller Date: Fri, 17 Oct 2025 03:51:46 +0200 Subject: [PATCH 28/54] Backport Godot fix for incorrect `Glyph` native-struct --- godot-codegen/src/models/domain_mapping.rs | 7 ++++++- godot-codegen/src/special_cases/special_cases.rs | 16 ++++++++++++++++ .../src/engine_tests/native_structures_test.rs | 2 ++ 3 files changed, 24 insertions(+), 1 deletion(-) diff --git a/godot-codegen/src/models/domain_mapping.rs b/godot-codegen/src/models/domain_mapping.rs index 477075028..2f90391e3 100644 --- a/godot-codegen/src/models/domain_mapping.rs +++ b/godot-codegen/src/models/domain_mapping.rs @@ -803,9 +803,14 @@ fn validate_enum_replacements( impl NativeStructure { pub fn from_json(json: &JsonNativeStructure) -> Self { + // Some native-struct definitions are incorrect in earlier Godot versions; this backports corrections. + let format = special_cases::get_native_struct_definition(&json.name) + .map(|s| s.to_string()) + .unwrap_or_else(|| json.format.clone()); + Self { name: json.name.clone(), - format: json.format.clone(), + format, } } } diff --git a/godot-codegen/src/special_cases/special_cases.rs b/godot-codegen/src/special_cases/special_cases.rs index afa444f21..afe1f86a7 100644 --- a/godot-codegen/src/special_cases/special_cases.rs +++ b/godot-codegen/src/special_cases/special_cases.rs @@ -97,6 +97,22 @@ pub fn is_native_struct_excluded(ty: &str) -> bool { codegen_special_cases::is_native_struct_excluded(ty) } +/// Overrides the definition string for native structures, if they have incorrect definitions in the JSON. +#[rustfmt::skip] +pub fn get_native_struct_definition(struct_name: &str) -> Option<&'static str> { + match struct_name { + // Glyph struct definition was corrected in Godot 4.6 to include missing `span_index` field. + // See https://github.com/godotengine/godot/pull/108369. + // #[cfg(before_api = "4.6")] // TODO(v0.5): enable this once upstream PR is merged. + "Glyph" => Some( + "int start = -1;int end = -1;uint8_t count = 0;uint8_t repeat = 1;uint16_t flags = 0;float x_off = 0.f;float y_off = 0.f;\ + float advance = 0.f;RID font_rid;int font_size = 0;int32_t index = 0;int span_index = -1" + ), + + _ => None, + } +} + #[rustfmt::skip] pub fn is_godot_type_deleted(godot_ty: &str) -> bool { // Note: parameter can be a class or builtin name, but also something like "enum::AESContext.Mode". diff --git a/itest/rust/src/engine_tests/native_structures_test.rs b/itest/rust/src/engine_tests/native_structures_test.rs index cf2e36e71..10aec961b 100644 --- a/itest/rust/src/engine_tests/native_structures_test.rs +++ b/itest/rust/src/engine_tests/native_structures_test.rs @@ -26,6 +26,7 @@ pub fn sample_glyph(start: i32) -> Glyph { font_rid: Rid::new(1024), font_size: 1025, index: 1026, + span_index: -1, } } @@ -48,6 +49,7 @@ fn native_structure_codegen() { font_rid: Rid::new(0), font_size: 0, index: 0, + span_index: -1, }; } From c98a0c2978c0737efa9b63bd252943b4e82507db Mon Sep 17 00:00:00 2001 From: Lyon Beckers Date: Fri, 24 Oct 2025 22:00:48 -0230 Subject: [PATCH 29/54] Only provide function name to CallContext in debug --- godot-core/src/builtin/callable.rs | 31 +++++++++++++++++++++++++----- itest/rust/src/benchmarks/mod.rs | 28 +++++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 5 deletions(-) diff --git a/godot-core/src/builtin/callable.rs b/godot-core/src/builtin/callable.rs index e472cbdf8..2071ceb1a 100644 --- a/godot-core/src/builtin/callable.rs +++ b/godot-core/src/builtin/callable.rs @@ -162,10 +162,12 @@ impl Callable { F: 'static + FnMut(&[&Variant]) -> R, S: meta::AsArg, { + #[cfg(debug_assertions)] meta::arg_into_owned!(name); Self::from_fn_wrapper::(FnWrapper { rust_function, + #[cfg(debug_assertions)] name, thread_id: Some(std::thread::current().id()), linked_obj_id: None, @@ -187,10 +189,12 @@ impl Callable { F: 'static + FnMut(&[&Variant]) -> R, S: meta::AsArg, { + #[cfg(debug_assertions)] meta::arg_into_owned!(name); Self::from_fn_wrapper::(FnWrapper { rust_function, + #[cfg(debug_assertions)] name, thread_id: Some(std::thread::current().id()), linked_obj_id: Some(linked_object.instance_id()), @@ -254,10 +258,12 @@ impl Callable { F: FnMut(&[&Variant]) -> Variant, Fc: FnOnce(&Callable) -> R, { + #[cfg(debug_assertions)] meta::arg_into_owned!(name); let callable = Self::from_fn_wrapper::(FnWrapper { rust_function, + #[cfg(debug_assertions)] name, thread_id: Some(std::thread::current().id()), linked_obj_id: None, @@ -292,10 +298,12 @@ impl Callable { F: 'static + Send + Sync + FnMut(&[&Variant]) -> R, S: meta::AsArg, { + #[cfg(debug_assertions)] meta::arg_into_owned!(name); Self::from_fn_wrapper::(FnWrapper { rust_function, + #[cfg(debug_assertions)] name, thread_id: None, linked_obj_id: None, @@ -340,6 +348,7 @@ impl Callable { callable_userdata: Box::into_raw(Box::new(userdata)) as *mut std::ffi::c_void, call_func: Some(rust_callable_call_fn::), free_func: Some(rust_callable_destroy::>), + #[cfg(debug_assertions)] to_string_func: Some(rust_callable_to_string_named::), is_valid_func: Some(rust_callable_is_valid), ..Self::default_callable_custom_info() @@ -608,6 +617,7 @@ mod custom_callable { pub(crate) struct FnWrapper { pub(super) rust_function: F, + #[cfg(debug_assertions)] pub(super) name: GString, /// `None` if the callable is multi-threaded ([`Callable::from_sync_fn`]). @@ -658,11 +668,14 @@ mod custom_callable { ) { let arg_refs: &[&Variant] = Variant::borrow_ref_slice(p_args, p_argument_count as usize); - let name = { + #[cfg(debug_assertions)] + let name = &{ let c: &C = CallableUserdata::inner_from_raw(callable_userdata); c.to_string() }; - let ctx = meta::CallContext::custom_callable(name.as_str()); + #[cfg(not(debug_assertions))] + let name = ""; + let ctx = meta::CallContext::custom_callable(name); crate::private::handle_varcall_panic(&ctx, &mut *r_error, move || { // Get the RustCallable again inside closure so it doesn't have to be UnwindSafe. @@ -685,11 +698,14 @@ mod custom_callable { { let arg_refs: &[&Variant] = Variant::borrow_ref_slice(p_args, p_argument_count as usize); - let name = { + #[cfg(debug_assertions)] + let name = &{ let w: &FnWrapper = CallableUserdata::inner_from_raw(callable_userdata); w.name.to_string() }; - let ctx = meta::CallContext::custom_callable(name.as_str()); + #[cfg(not(debug_assertions))] + let name = ""; + let ctx = meta::CallContext::custom_callable(name); crate::private::handle_varcall_panic(&ctx, &mut *r_error, move || { // Get the FnWrapper again inside closure so the FnMut doesn't have to be UnwindSafe. @@ -698,12 +714,16 @@ mod custom_callable { if w.thread_id .is_some_and(|tid| tid != std::thread::current().id()) { + #[cfg(debug_assertions)] + let name = &w.name; + #[cfg(not(debug_assertions))] + let name = ""; // NOTE: this panic is currently not propagated to the caller, but results in an error message and Nil return. // See comments in itest callable_call() for details. panic!( "Callable '{}' created with from_fn() must be called from the same thread it was created in.\n\ If you need to call it from any thread, use from_sync_fn() instead (requires `experimental-threads` feature).", - w.name + name ); } @@ -749,6 +769,7 @@ mod custom_callable { *r_is_valid = sys::conv::SYS_TRUE; } + #[cfg(debug_assertions)] pub unsafe extern "C" fn rust_callable_to_string_named( callable_userdata: *mut std::ffi::c_void, r_is_valid: *mut sys::GDExtensionBool, diff --git a/itest/rust/src/benchmarks/mod.rs b/itest/rust/src/benchmarks/mod.rs index 77312090b..6d41bec56 100644 --- a/itest/rust/src/benchmarks/mod.rs +++ b/itest/rust/src/benchmarks/mod.rs @@ -13,6 +13,7 @@ use godot::builtin::inner::InnerRect2i; use godot::builtin::{GString, PackedInt32Array, Rect2i, StringName, Vector2i}; use godot::classes::{Node3D, Os, RefCounted}; use godot::obj::{Gd, InstanceId, NewAlloc, NewGd, Singleton}; +use godot::prelude::{varray, Callable, RustCallable, Variant}; use godot::register::GodotClass; use crate::framework::bench; @@ -113,9 +114,36 @@ fn packed_array_from_iter_unknown_size() -> PackedInt32Array { })) } +#[bench(repeat = 25)] +fn call_callv_rust_fn() -> Variant { + let callable = Callable::from_fn("RustFunction", |_| ()); + callable.callv(&varray![]) +} + +#[bench(repeat = 25)] +fn call_callv_custom() -> Variant { + let callable = Callable::from_custom(MyRustCallable {}); + callable.callv(&varray![]) +} + // ---------------------------------------------------------------------------------------------------------------------------------------------- // Helpers for benchmarks above #[derive(GodotClass)] #[class(init)] struct MyBenchType {} + +#[derive(PartialEq, Hash)] +struct MyRustCallable {} + +impl RustCallable for MyRustCallable { + fn invoke(&mut self, _args: &[&Variant]) -> Variant { + Variant::nil() + } +} + +impl std::fmt::Display for MyRustCallable { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "MyRustCallable") + } +} From bbc95a6c8764143460dee3e07d3c4b529650e170 Mon Sep 17 00:00:00 2001 From: Jan Haller Date: Sat, 25 Oct 2025 09:30:03 +0200 Subject: [PATCH 30/54] Add API to fetch autoloads by name --- godot-core/src/tools/autoload.rs | 99 +++++++++++++++++++ godot-core/src/tools/mod.rs | 2 + itest/godot/SpecialTests.gd | 14 +++ itest/godot/TestRunner.tscn | 2 +- itest/godot/gdscript_tests/AutoloadScene.tscn | 3 + itest/godot/project.godot | 4 + itest/rust/src/engine_tests/autoload_test.rs | 67 +++++++++++++ itest/rust/src/engine_tests/mod.rs | 1 + 8 files changed, 191 insertions(+), 1 deletion(-) create mode 100644 godot-core/src/tools/autoload.rs create mode 100644 itest/godot/gdscript_tests/AutoloadScene.tscn create mode 100644 itest/rust/src/engine_tests/autoload_test.rs diff --git a/godot-core/src/tools/autoload.rs b/godot-core/src/tools/autoload.rs new file mode 100644 index 000000000..66eb47ed9 --- /dev/null +++ b/godot-core/src/tools/autoload.rs @@ -0,0 +1,99 @@ +/* + * Copyright (c) godot-rust; Bromeon and contributors. + * 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::builtin::NodePath; +use crate::classes::{Engine, Node, SceneTree}; +use crate::meta::error::ConvertError; +use crate::obj::{Gd, Inherits, Singleton}; + +/// Retrieves an autoload by its name. +/// +/// See [Godot docs] for an explanation of the autoload concept. Godot sometimes uses the term "autoload" interchangeably with "singleton"; +/// we strictly refer to the former to separate from [`Singleton`][crate::obj::Singleton] objects. +/// +/// [Godot docs]: https://docs.godotengine.org/en/stable/tutorials/scripting/singletons_autoload.html +/// +/// # Panics +/// This is a convenience function that calls [`try_get_autoload_by_name()`]. Panics if that fails, e.g. not found or wrong type. +/// +/// # Example +/// ```no_run +/// use godot::prelude::*; +/// use godot::tools::get_autoload_by_name; +/// +/// #[derive(GodotClass)] +/// #[class(init, base=Node)] +/// struct GlobalStats { +/// base: Base, +/// } +/// +/// // Assuming "Statistics" is registered as an autoload in `project.godot`, +/// // this returns the one instance of type Gd. +/// let stats = get_autoload_by_name::("Statistics"); +/// ``` +pub fn get_autoload_by_name(autoload_name: &str) -> Gd +where + T: Inherits, +{ + try_get_autoload_by_name::(autoload_name) + .unwrap_or_else(|err| panic!("Failed to get autoload `{autoload_name}`: {err}")) +} + +/// Retrieves an autoload by name (fallible). +/// +/// Autoloads are accessed via the `/root/{name}` path in the scene tree. The name is the one you used to register the autoload in +/// `project.godot`. By convention, it often corresponds to the class name, but does not have to. +/// +/// See also [`get_autoload_by_name()`] for simpler function expecting the class name and non-fallible invocation. +/// +/// This function returns `Err` if: +/// - No autoload is registered under `name`. +/// - The autoload cannot be cast to type `T`. +/// - There is an error fetching the scene tree. +/// +/// # Example +/// ```no_run +/// use godot::prelude::*; +/// use godot::tools::try_get_autoload_by_name; +/// +/// #[derive(GodotClass)] +/// #[class(init, base=Node)] +/// struct GlobalStats { +/// base: Base, +/// } +/// +/// let result = try_get_autoload_by_name::("Statistics"); +/// match result { +/// Ok(autoload) => { /* Use the Gd. */ } +/// Err(err) => eprintln!("Failed to get autoload: {err}"), +/// } +/// ``` +pub fn try_get_autoload_by_name(autoload_name: &str) -> Result, ConvertError> +where + T: Inherits, +{ + let main_loop = Engine::singleton() + .get_main_loop() + .ok_or_else(|| ConvertError::new("main loop not available"))?; + + let scene_tree = main_loop + .try_cast::() + .map_err(|_| ConvertError::new("main loop is not a SceneTree"))?; + + let autoload_path = NodePath::from(&format!("/root/{autoload_name}")); + + let root = scene_tree + .get_root() + .ok_or_else(|| ConvertError::new("scene tree root not available"))?; + + root.try_get_node_as::(&autoload_path).ok_or_else(|| { + let class = T::class_id(); + ConvertError::new(format!( + "autoload `{autoload_name}` not found or has wrong type (expected {class})", + )) + }) +} diff --git a/godot-core/src/tools/mod.rs b/godot-core/src/tools/mod.rs index 0b27ec75a..7817ed400 100644 --- a/godot-core/src/tools/mod.rs +++ b/godot-core/src/tools/mod.rs @@ -10,10 +10,12 @@ //! Contains functionality that extends existing Godot classes and functions, to make them more versatile //! or better integrated with Rust. +mod autoload; mod gfile; mod save_load; mod translate; +pub use autoload::*; pub use gfile::*; pub use save_load::*; pub use translate::*; diff --git a/itest/godot/SpecialTests.gd b/itest/godot/SpecialTests.gd index e787a45b0..0f752b24a 100644 --- a/itest/godot/SpecialTests.gd +++ b/itest/godot/SpecialTests.gd @@ -57,3 +57,17 @@ func test_collision_object_2d_input_event(): window.queue_free() +func test_autoload(): + var fetched = Engine.get_main_loop().get_root().get_node_or_null("/root/MyAutoload") + assert_that(fetched != null, "MyAutoload should be loaded") + + var by_class: AutoloadClass = fetched + assert_eq(by_class.verify_works(), 787, "Autoload typed by class") + + var by_class_symbol: AutoloadClass = MyAutoload + assert_eq(by_class_symbol.verify_works(), 787, "Autoload typed by class") + + # Autoload in GDScript can be referenced by class name or autoload name, however autoload as a type is only available in Godot 4.3+. + # See https://github.com/godot-rust/gdext/pull/1381#issuecomment-3446111511. + # var by_name: MyAutoload = fetched + # assert_eq(by_name.verify_works(), 787, "Autoload typed by name") diff --git a/itest/godot/TestRunner.tscn b/itest/godot/TestRunner.tscn index dbd5cd740..cf2dc9b53 100644 --- a/itest/godot/TestRunner.tscn +++ b/itest/godot/TestRunner.tscn @@ -1,6 +1,6 @@ [gd_scene load_steps=2 format=3 uid="uid://dgcj68l8n6wpb"] -[ext_resource type="Script" path="res://TestRunner.gd" id="1_wdbrf"] +[ext_resource type="Script" uid="uid://dcsm6ho05dipr" path="res://TestRunner.gd" id="1_wdbrf"] [node name="TestRunner" type="Node"] script = ExtResource("1_wdbrf") diff --git a/itest/godot/gdscript_tests/AutoloadScene.tscn b/itest/godot/gdscript_tests/AutoloadScene.tscn new file mode 100644 index 000000000..f1404c188 --- /dev/null +++ b/itest/godot/gdscript_tests/AutoloadScene.tscn @@ -0,0 +1,3 @@ +[gd_scene format=3 uid="uid://csf04mj3dj8bn"] + +[node name="AutoloadNode" type="AutoloadClass"] diff --git a/itest/godot/project.godot b/itest/godot/project.godot index 049e2ba0a..fdccb81bb 100644 --- a/itest/godot/project.godot +++ b/itest/godot/project.godot @@ -15,6 +15,10 @@ run/main_scene="res://TestRunner.tscn" config/features=PackedStringArray("4.2") run/flush_stdout_on_print=true +[autoload] + +MyAutoload="*res://gdscript_tests/AutoloadScene.tscn" + [debug] gdscript/warnings/shadowed_variable=0 diff --git a/itest/rust/src/engine_tests/autoload_test.rs b/itest/rust/src/engine_tests/autoload_test.rs new file mode 100644 index 000000000..3d226ec77 --- /dev/null +++ b/itest/rust/src/engine_tests/autoload_test.rs @@ -0,0 +1,67 @@ +/* + * Copyright (c) godot-rust; Bromeon and contributors. + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +use godot::classes::Node; +use godot::prelude::*; +use godot::tools::{get_autoload_by_name, try_get_autoload_by_name}; + +use crate::framework::itest; + +#[derive(GodotClass)] +#[class(init, base=Node)] +struct AutoloadClass { + base: Base, + #[var] + property: i32, +} + +#[godot_api] +impl AutoloadClass { + #[func] + fn verify_works(&self) -> i32 { + 787 + } +} + +#[itest] +fn autoload_get() { + let mut autoload = get_autoload_by_name::("MyAutoload"); + { + let mut guard = autoload.bind_mut(); + assert_eq!(guard.verify_works(), 787); + assert_eq!(guard.property, 0, "still has default value"); + + guard.property = 42; + } + + // Fetch same autoload anew. + let autoload2 = get_autoload_by_name::("MyAutoload"); + assert_eq!(autoload2.bind().property, 42); + + // Reset for other tests. + autoload.bind_mut().property = 0; +} + +#[itest] +fn autoload_try_get_named() { + let autoload = try_get_autoload_by_name::("MyAutoload").expect("fetch autoload"); + + assert_eq!(autoload.bind().verify_works(), 787); + assert_eq!(autoload.bind().property, 0, "still has default value"); +} + +#[itest] +fn autoload_try_get_named_inexistent() { + let result = try_get_autoload_by_name::("InexistentAutoload"); + result.expect_err("non-existent autoload"); +} + +#[itest] +fn autoload_try_get_named_bad_type() { + let result = try_get_autoload_by_name::("MyAutoload"); + result.expect_err("autoload of incompatible node type"); +} diff --git a/itest/rust/src/engine_tests/mod.rs b/itest/rust/src/engine_tests/mod.rs index 7a7c5503d..b5edaefd7 100644 --- a/itest/rust/src/engine_tests/mod.rs +++ b/itest/rust/src/engine_tests/mod.rs @@ -6,6 +6,7 @@ */ mod async_test; +mod autoload_test; mod codegen_enums_test; mod codegen_test; mod engine_enum_test; From 81dfc9a30377923ef5996c35c6dd1517fc6f98cb Mon Sep 17 00:00:00 2001 From: Yarvin Date: Sat, 25 Oct 2025 08:12:34 +0200 Subject: [PATCH 31/54] Validate call params for `gd_self` virtual methods. --- godot-macros/src/class/data_models/func.rs | 28 +++++++++++++++++----- 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/godot-macros/src/class/data_models/func.rs b/godot-macros/src/class/data_models/func.rs index 62b5ec3f8..744707403 100644 --- a/godot-macros/src/class/data_models/func.rs +++ b/godot-macros/src/class/data_models/func.rs @@ -302,11 +302,7 @@ fn make_forwarding_closure( _ => unreachable!("unexpected receiver type"), // checked above. }; - let rust_sig_name = format_ident!("Sig_{method_name}"); - - sig_tuple_annotation = quote! { - : ::godot::private::virtuals::#trait_base_class::#rust_sig_name - }; + sig_tuple_annotation = make_sig_tuple_annotation(trait_base_class, method_name); let method_invocation = TokenStream::from_iter( quote! {<#class_name as #interface_trait>::#method_name} @@ -346,10 +342,17 @@ fn make_forwarding_closure( ReceiverType::GdSelf => { // Method call is always present, since GdSelf implies that the user declares the method. // (Absent method is only used in the case of a generated default virtual method, e.g. for ready()). + + let sig_tuple_annotation = if interface_trait.is_some() { + make_sig_tuple_annotation(trait_base_class, method_name) + } else { + TokenStream::new() + }; + quote! { |instance_ptr, params| { // Not using `virtual_sig`, since virtual methods with `#[func(gd_self)]` are being moved out of the trait to inherent impl. - let #params_tuple = #param_ident; + let #params_tuple #sig_tuple_annotation = #param_ident; let storage = unsafe { ::godot::private::as_storage::<#class_name>(instance_ptr) }; @@ -637,6 +640,19 @@ fn make_call_context(class_name_str: &str, method_name_str: &str) -> TokenStream } } +/// Returns a type annotation for the tuple corresponding to the signature declared on given ITrait method, +/// allowing to validate params for a generated method call at compile time. +/// +/// For example `::godot::private::virtuals::Node::Sig_physics_process` is `(f64, )`, +/// thus `let params: ::godot::private::virtuals::Node::Sig_physics_process = ();` +/// will not compile. +fn make_sig_tuple_annotation(trait_base_class: &Ident, method_name: &Ident) -> TokenStream { + let rust_sig_name = format_ident!("Sig_{method_name}"); + quote! { + : ::godot::private::virtuals::#trait_base_class::#rust_sig_name + } +} + pub fn bail_attr(attr_name: &Ident, msg: &str, method_name: &Ident) -> ParseResult { bail!(method_name, "#[{attr_name}]: {msg}") } From a2d23de1c3c6360e228c6be25f215f7a0538c83e Mon Sep 17 00:00:00 2001 From: Jan Haller Date: Sat, 25 Oct 2025 11:06:06 +0200 Subject: [PATCH 32/54] Cache autoloads once resolved --- godot-core/src/init/mod.rs | 1 + godot-core/src/tools/autoload.rs | 86 +++++++++++++++++++- godot-core/src/tools/mod.rs | 6 ++ itest/rust/src/engine_tests/autoload_test.rs | 27 +++++- 4 files changed, 115 insertions(+), 5 deletions(-) diff --git a/godot-core/src/init/mod.rs b/godot-core/src/init/mod.rs index 842d6870b..a1eee5ada 100644 --- a/godot-core/src/init/mod.rs +++ b/godot-core/src/init/mod.rs @@ -232,6 +232,7 @@ fn gdext_on_level_deinit(level: InitLevel) { // No business logic by itself, but ensures consistency if re-initialization (hot-reload on Linux) occurs. crate::task::cleanup(); + crate::tools::cleanup(); // Garbage-collect various statics. // SAFETY: this is the last time meta APIs are used. diff --git a/godot-core/src/tools/autoload.rs b/godot-core/src/tools/autoload.rs index 66eb47ed9..c22e3d8ce 100644 --- a/godot-core/src/tools/autoload.rs +++ b/godot-core/src/tools/autoload.rs @@ -5,16 +5,24 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ +use std::cell::RefCell; +use std::collections::HashMap; + +use sys::is_main_thread; + use crate::builtin::NodePath; use crate::classes::{Engine, Node, SceneTree}; use crate::meta::error::ConvertError; use crate::obj::{Gd, Inherits, Singleton}; +use crate::sys; -/// Retrieves an autoload by its name. +/// Retrieves an autoload by name. /// /// See [Godot docs] for an explanation of the autoload concept. Godot sometimes uses the term "autoload" interchangeably with "singleton"; /// we strictly refer to the former to separate from [`Singleton`][crate::obj::Singleton] objects. /// +/// If the autoload can be resolved, it will be cached and returned very quickly the second time. +/// /// [Godot docs]: https://docs.godotengine.org/en/stable/tutorials/scripting/singletons_autoload.html /// /// # Panics @@ -48,6 +56,8 @@ where /// Autoloads are accessed via the `/root/{name}` path in the scene tree. The name is the one you used to register the autoload in /// `project.godot`. By convention, it often corresponds to the class name, but does not have to. /// +/// If the autoload can be resolved, it will be cached and returned very quickly the second time. +/// /// See also [`get_autoload_by_name()`] for simpler function expecting the class name and non-fallible invocation. /// /// This function returns `Err` if: @@ -76,6 +86,16 @@ pub fn try_get_autoload_by_name(autoload_name: &str) -> Result, Convert where T: Inherits, { + ensure_main_thread()?; + + // Check cache first. + let cached = AUTOLOAD_CACHE.with(|cache| cache.borrow().get(autoload_name).cloned()); + + if let Some(cached_node) = cached { + return cast_autoload(cached_node, autoload_name); + } + + // Cache miss - fetch from scene tree. let main_loop = Engine::singleton() .get_main_loop() .ok_or_else(|| ConvertError::new("main loop not available"))?; @@ -90,10 +110,68 @@ where .get_root() .ok_or_else(|| ConvertError::new("scene tree root not available"))?; - root.try_get_node_as::(&autoload_path).ok_or_else(|| { - let class = T::class_id(); + let autoload_node = root + .try_get_node_as::(&autoload_path) + .ok_or_else(|| ConvertError::new(format!("autoload `{autoload_name}` not found")))?; + + // Store in cache as Gd. + AUTOLOAD_CACHE.with(|cache| { + cache + .borrow_mut() + .insert(autoload_name.to_string(), autoload_node.clone()); + }); + + // Cast to requested type. + cast_autoload(autoload_node, autoload_name) +} + +// ---------------------------------------------------------------------------------------------------------------------------------------------- +// Cache implementation + +thread_local! { + /// Cache for autoloads. Maps autoload name to `Gd`. + /// + /// Uses `thread_local!` because `Gd` is not `Send`/`Sync`. Since all Godot objects must be accessed + /// from the main thread, this is safe. We enforce main-thread access via `ensure_main_thread()`. + static AUTOLOAD_CACHE: RefCell>> = RefCell::new(HashMap::new()); +} + +/// Verifies that the current thread is the main thread. +/// +/// Returns an error if called from a thread other than the main thread. This is necessary because `Gd` is not thread-safe. +fn ensure_main_thread() -> Result<(), ConvertError> { + if is_main_thread() { + Ok(()) + } else { + Err(ConvertError::new( + "Autoloads must be fetched from main thread, as Gd is not thread-safe", + )) + } +} + +/// Casts an autoload node to the requested type, with descriptive error message on failure. +fn cast_autoload(node: Gd, autoload_name: &str) -> Result, ConvertError> +where + T: Inherits, +{ + node.try_cast::().map_err(|node| { + let expected = T::class_id(); + let actual = node.get_class(); + ConvertError::new(format!( - "autoload `{autoload_name}` not found or has wrong type (expected {class})", + "autoload `{autoload_name}` has wrong type (expected {expected}, got {actual})", )) }) } + +/// Clears the autoload cache (called during shutdown). +/// +/// # Panics +/// Panics if called from a thread other than the main thread. +pub(crate) fn clear_autoload_cache() { + ensure_main_thread().expect("clear_autoload_cache() must be called from the main thread"); + + AUTOLOAD_CACHE.with(|cache| { + cache.borrow_mut().clear(); + }); +} diff --git a/godot-core/src/tools/mod.rs b/godot-core/src/tools/mod.rs index 7817ed400..04aafa1cb 100644 --- a/godot-core/src/tools/mod.rs +++ b/godot-core/src/tools/mod.rs @@ -19,3 +19,9 @@ pub use autoload::*; pub use gfile::*; pub use save_load::*; pub use translate::*; + +// ---------------------------------------------------------------------------------------------------------------------------------------------- + +pub(crate) fn cleanup() { + clear_autoload_cache(); +} diff --git a/itest/rust/src/engine_tests/autoload_test.rs b/itest/rust/src/engine_tests/autoload_test.rs index 3d226ec77..cc3a7c7c2 100644 --- a/itest/rust/src/engine_tests/autoload_test.rs +++ b/itest/rust/src/engine_tests/autoload_test.rs @@ -9,7 +9,7 @@ use godot::classes::Node; use godot::prelude::*; use godot::tools::{get_autoload_by_name, try_get_autoload_by_name}; -use crate::framework::itest; +use crate::framework::{itest, quick_thread}; #[derive(GodotClass)] #[class(init, base=Node)] @@ -65,3 +65,28 @@ fn autoload_try_get_named_bad_type() { let result = try_get_autoload_by_name::("MyAutoload"); result.expect_err("autoload of incompatible node type"); } + +#[itest] +fn autoload_from_other_thread() { + use std::sync::{Arc, Mutex}; + + // We can't return the Result from the thread because Gd is not Send, so we extract the error message instead. + let outer_error = Arc::new(Mutex::new(String::new())); + let inner_error = Arc::clone(&outer_error); + + quick_thread(move || { + let result = try_get_autoload_by_name::("MyAutoload"); + match result { + Ok(_) => panic!("autoload access from non-main thread should fail"), + Err(err) => { + *inner_error.lock().unwrap() = err.to_string(); + } + } + }); + + let msg = outer_error.lock().unwrap(); + assert_eq!( + *msg, + "Autoloads must be fetched from main thread, as Gd is not thread-safe" + ); +} From fcf8b0f781d3da9e3afb9ea70a070d8c760cea26 Mon Sep 17 00:00:00 2001 From: Luo Zhihao Date: Sat, 11 Oct 2025 15:45:43 +0800 Subject: [PATCH 33/54] Add cargo features to configure safety checks --- godot-bindings/Cargo.toml | 3 ++ godot-bindings/build.rs | 2 +- godot-bindings/src/lib.rs | 20 +++++++++++ godot-codegen/src/generator/classes.rs | 5 +-- .../src/generator/native_structures.rs | 1 + godot-core/Cargo.toml | 3 ++ godot-core/build.rs | 1 + godot-core/src/builtin/collections/array.rs | 11 +++--- godot-core/src/classes/class_runtime.rs | 11 +++--- godot-core/src/meta/error/convert_error.rs | 4 +-- godot-core/src/meta/signature.rs | 4 +++ godot-core/src/obj/base.rs | 36 ++++++++++--------- godot-core/src/obj/casts.rs | 7 ++-- godot-core/src/obj/gd.rs | 5 ++- godot-core/src/obj/raw_gd.rs | 22 +++++++----- godot-core/src/obj/rtti.rs | 14 ++++---- godot-core/src/storage/mod.rs | 8 ++--- godot-ffi/Cargo.toml | 4 +++ godot-ffi/build.rs | 1 + godot-ffi/src/lib.rs | 16 +++++++-- godot/Cargo.toml | 4 +++ 21 files changed, 124 insertions(+), 58 deletions(-) diff --git a/godot-bindings/Cargo.toml b/godot-bindings/Cargo.toml index 9305c9b2f..6f7d60328 100644 --- a/godot-bindings/Cargo.toml +++ b/godot-bindings/Cargo.toml @@ -33,6 +33,9 @@ api-custom = ["dep:bindgen", "dep:regex", "dep:which"] api-custom-json = ["dep:nanoserde", "dep:bindgen", "dep:regex", "dep:which"] api-custom-extheader = [] +debug-checks-balanced = [] +release-checks-fast-unsafe = [] + [dependencies] gdextension-api = { workspace = true } diff --git a/godot-bindings/build.rs b/godot-bindings/build.rs index edceaa9f3..dbd915cf1 100644 --- a/godot-bindings/build.rs +++ b/godot-bindings/build.rs @@ -10,7 +10,7 @@ // It's the only purpose of this build.rs file. If a better solution is found, this file can be removed. #[rustfmt::skip] -fn main() { +fn main() { let mut count = 0; if cfg!(feature = "api-custom") { count += 1; } if cfg!(feature = "api-custom-json") { count += 1; } diff --git a/godot-bindings/src/lib.rs b/godot-bindings/src/lib.rs index 5e3346010..f522b23f2 100644 --- a/godot-bindings/src/lib.rs +++ b/godot-bindings/src/lib.rs @@ -267,3 +267,23 @@ pub fn before_api(major_minor: &str) -> bool { pub fn since_api(major_minor: &str) -> bool { !before_api(major_minor) } + +pub fn emit_checks_mode() { + let check_modes = ["fast-unsafe", "balanced", "paranoid"]; + let mut checks_level = if cfg!(debug_assertions) { 2 } else { 1 }; + #[cfg(debug_assertions)] + if cfg!(feature = "debug-checks-balanced") { + checks_level = 1; + } + #[cfg(not(debug_assertions))] + if cfg!(feature = "release-checks-fast-unsafe") { + checks_level = 0; + } + + for mode in check_modes.iter() { + println!(r#"cargo:rustc-check-cfg=cfg(checks_at_least, values("{mode}"))"#); + } + for mode in check_modes.iter().take(checks_level + 1) { + println!(r#"cargo:rustc-cfg=checks_at_least="{mode}""#); + } +} diff --git a/godot-codegen/src/generator/classes.rs b/godot-codegen/src/generator/classes.rs index 621946ef0..a8c48aa74 100644 --- a/godot-codegen/src/generator/classes.rs +++ b/godot-codegen/src/generator/classes.rs @@ -195,8 +195,9 @@ fn make_class(class: &Class, ctx: &mut Context, view: &ApiView) -> GeneratedClas fn __checked_id(&self) -> Option { // SAFETY: only Option due to layout-compatibility with RawGd; it is always Some because stored in Gd which is non-null. let rtti = unsafe { self.rtti.as_ref().unwrap_unchecked() }; - let instance_id = rtti.check_type::(); - Some(instance_id) + #[cfg(checks_at_least = "paranoid")] + rtti.check_type::(); + Some(rtti.instance_id()) } #[doc(hidden)] diff --git a/godot-codegen/src/generator/native_structures.rs b/godot-codegen/src/generator/native_structures.rs index 435b373b8..6c324051a 100644 --- a/godot-codegen/src/generator/native_structures.rs +++ b/godot-codegen/src/generator/native_structures.rs @@ -219,6 +219,7 @@ fn make_native_structure_field_and_accessor( let obj = #snake_field_name.upcast(); + #[cfg(checks_at_least = "balanced")] assert!(obj.is_instance_valid(), "provided node is dead"); let id = obj.instance_id().to_u64(); diff --git a/godot-core/Cargo.toml b/godot-core/Cargo.toml index a31cd61a1..486729b37 100644 --- a/godot-core/Cargo.toml +++ b/godot-core/Cargo.toml @@ -38,6 +38,9 @@ api-4-4 = ["godot-ffi/api-4-4"] api-4-5 = ["godot-ffi/api-4-5"] # ]] +debug-checks-balanced = ["godot-ffi/debug-checks-balanced"] +release-checks-fast-unsafe = ["godot-ffi/release-checks-fast-unsafe"] + [dependencies] godot-ffi = { path = "../godot-ffi", version = "=0.4.1" } diff --git a/godot-core/build.rs b/godot-core/build.rs index 624c73ebf..c7fb9707b 100644 --- a/godot-core/build.rs +++ b/godot-core/build.rs @@ -18,4 +18,5 @@ fn main() { godot_bindings::emit_godot_version_cfg(); godot_bindings::emit_wasm_nothreads_cfg(); + godot_bindings::emit_checks_mode(); } diff --git a/godot-core/src/builtin/collections/array.rs b/godot-core/src/builtin/collections/array.rs index a3beccd86..a1c6ee328 100644 --- a/godot-core/src/builtin/collections/array.rs +++ b/godot-core/src/builtin/collections/array.rs @@ -968,7 +968,7 @@ impl Array { } /// Validates that all elements in this array can be converted to integers of type `T`. - #[cfg(debug_assertions)] + #[cfg(checks_at_least = "paranoid")] pub(crate) fn debug_validate_int_elements(&self) -> Result<(), ConvertError> { // SAFETY: every element is internally represented as Variant. let canonical_array = unsafe { self.assume_type_ref::() }; @@ -990,7 +990,7 @@ impl Array { } // No-op in Release. Avoids O(n) conversion checks, but still panics on access. - #[cfg(not(debug_assertions))] + #[cfg(not(checks_at_least = "paranoid"))] pub(crate) fn debug_validate_int_elements(&self) -> Result<(), ConvertError> { Ok(()) } @@ -1233,10 +1233,9 @@ impl Clone for Array { let copy = unsafe { self.clone_unchecked() }; // Double-check copy's runtime type in Debug mode. - if cfg!(debug_assertions) { - copy.with_checked_type().unwrap_or_else(|e| { - panic!("copied array should have same type as original array: {e}") - }) + if cfg!(checks_at_least = "paranoid") { + copy.with_checked_type() + .expect("copied array should have same type as original array") } else { copy } diff --git a/godot-core/src/classes/class_runtime.rs b/godot-core/src/classes/class_runtime.rs index f73ecada4..8573710bd 100644 --- a/godot-core/src/classes/class_runtime.rs +++ b/godot-core/src/classes/class_runtime.rs @@ -8,10 +8,10 @@ //! Runtime checks and inspection of Godot classes. use crate::builtin::{GString, StringName, Variant, VariantType}; -#[cfg(debug_assertions)] +#[cfg(checks_at_least = "paranoid")] use crate::classes::{ClassDb, Object}; use crate::meta::CallContext; -#[cfg(debug_assertions)] +#[cfg(checks_at_least = "paranoid")] use crate::meta::ClassId; use crate::obj::{bounds, Bounds, Gd, GodotClass, InstanceId, RawGd, Singleton}; use crate::sys; @@ -191,6 +191,7 @@ where Gd::::from_obj_sys(object_ptr) } +#[cfg(checks_at_least = "balanced")] pub(crate) fn ensure_object_alive( instance_id: InstanceId, old_object_ptr: sys::GDExtensionObjectPtr, @@ -211,7 +212,7 @@ pub(crate) fn ensure_object_alive( ); } -#[cfg(debug_assertions)] +#[cfg(checks_at_least = "paranoid")] pub(crate) fn ensure_object_inherits(derived: ClassId, base: ClassId, instance_id: InstanceId) { if derived == base || base == Object::class_id() // for Object base, anything inherits by definition @@ -226,7 +227,7 @@ pub(crate) fn ensure_object_inherits(derived: ClassId, base: ClassId, instance_i ) } -#[cfg(debug_assertions)] +#[cfg(checks_at_least = "paranoid")] pub(crate) fn ensure_binding_not_null(binding: sys::GDExtensionClassInstancePtr) where T: GodotClass + Bounds, @@ -254,7 +255,7 @@ where // Implementation of this file /// Checks if `derived` inherits from `base`, using a cache for _successful_ queries. -#[cfg(debug_assertions)] +#[cfg(checks_at_least = "paranoid")] fn is_derived_base_cached(derived: ClassId, base: ClassId) -> bool { use std::collections::HashSet; diff --git a/godot-core/src/meta/error/convert_error.rs b/godot-core/src/meta/error/convert_error.rs index 12fa2f296..4e019d9e8 100644 --- a/godot-core/src/meta/error/convert_error.rs +++ b/godot-core/src/meta/error/convert_error.rs @@ -189,7 +189,7 @@ pub(crate) enum FromGodotError { }, /// Special case of `BadArrayType` where a custom int type such as `i8` cannot hold a dynamic `i64` value. - #[cfg(debug_assertions)] + #[cfg(checks_at_least = "paranoid")] BadArrayTypeInt { expected_int_type: &'static str, value: i64, @@ -234,7 +234,7 @@ impl fmt::Display for FromGodotError { write!(f, "expected array of type {exp_class}, got {act_class}") } - #[cfg(debug_assertions)] + #[cfg(checks_at_least = "paranoid")] Self::BadArrayTypeInt { expected_int_type, value, diff --git a/godot-core/src/meta/signature.rs b/godot-core/src/meta/signature.rs index 0af81e3eb..dadb4b02b 100644 --- a/godot-core/src/meta/signature.rs +++ b/godot-core/src/meta/signature.rs @@ -144,6 +144,8 @@ impl Signature { //$crate::out!("out_class_varcall: {call_ctx}"); // Note: varcalls are not safe from failing, if they happen through an object pointer -> validity check necessary. + // paranoid since we already check the validity in check_rtti, this is unlikely to happen. + #[cfg(checks_at_least = "paranoid")] if let Some(instance_id) = maybe_instance_id { crate::classes::ensure_object_alive(instance_id, object_ptr, &call_ctx); } @@ -304,6 +306,8 @@ impl Signature { let call_ctx = CallContext::outbound(class_name, method_name); // $crate::out!("out_class_ptrcall: {call_ctx}"); + // paranoid since we already check the validity in check_rtti, this is unlikely to happen. + #[cfg(checks_at_least = "paranoid")] if let Some(instance_id) = maybe_instance_id { crate::classes::ensure_object_alive(instance_id, object_ptr, &call_ctx); } diff --git a/godot-core/src/obj/base.rs b/godot-core/src/obj/base.rs index 1e02e038c..adae2e9e9 100644 --- a/godot-core/src/obj/base.rs +++ b/godot-core/src/obj/base.rs @@ -5,7 +5,7 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -#[cfg(debug_assertions)] +#[cfg(checks_at_least = "paranoid")] use std::cell::Cell; use std::cell::RefCell; use std::collections::hash_map::Entry; @@ -27,7 +27,7 @@ thread_local! { } /// Represents the initialization state of a `Base` object. -#[cfg(debug_assertions)] +#[cfg(checks_at_least = "paranoid")] #[derive(Debug, Copy, Clone, PartialEq, Eq)] enum InitState { /// Object is being constructed (inside `I*::init()` or `Gd::from_init_fn()`). @@ -38,14 +38,14 @@ enum InitState { Script, } -#[cfg(debug_assertions)] +#[cfg(checks_at_least = "paranoid")] macro_rules! base_from_obj { ($obj:expr, $state:expr) => { Base::from_obj($obj, $state) }; } -#[cfg(not(debug_assertions))] +#[cfg(not(checks_at_least = "paranoid"))] macro_rules! base_from_obj { ($obj:expr, $state:expr) => { Base::from_obj($obj) @@ -82,7 +82,7 @@ pub struct Base { /// Tracks the initialization state of this `Base` in Debug mode. /// /// Rc allows to "copy-construct" the base from an existing one, while still affecting the user-instance through the original `Base`. - #[cfg(debug_assertions)] + #[cfg(checks_at_least = "paranoid")] init_state: Rc>, } @@ -95,13 +95,14 @@ impl Base { /// `base` must be alive at the time of invocation, i.e. user `init()` (which could technically destroy it) must not have run yet. /// If `base` is destroyed while the returned `Base` is in use, that constitutes a logic error, not a safety issue. pub(crate) unsafe fn from_base(base: &Base) -> Base { - debug_assert!(base.obj.is_instance_valid()); + #[cfg(checks_at_least = "paranoid")] + assert!(base.obj.is_instance_valid()); let obj = Gd::from_obj_sys_weak(base.obj.obj_sys()); Self { obj: ManuallyDrop::new(obj), - #[cfg(debug_assertions)] + #[cfg(checks_at_least = "paranoid")] init_state: Rc::clone(&base.init_state), } } @@ -114,7 +115,8 @@ impl Base { /// `gd` must be alive at the time of invocation. If it is destroyed while the returned `Base` is in use, that constitutes a logic /// error, not a safety issue. pub(crate) unsafe fn from_script_gd(gd: &Gd) -> Self { - debug_assert!(gd.is_instance_valid()); + #[cfg(checks_at_least = "paranoid")] + assert!(gd.is_instance_valid()); let obj = Gd::from_obj_sys_weak(gd.obj_sys()); base_from_obj!(obj, InitState::Script) @@ -141,7 +143,7 @@ impl Base { base_from_obj!(obj, InitState::ObjectConstructing) } - #[cfg(debug_assertions)] + #[cfg(checks_at_least = "paranoid")] fn from_obj(obj: Gd, init_state: InitState) -> Self { Self { obj: ManuallyDrop::new(obj), @@ -149,7 +151,7 @@ impl Base { } } - #[cfg(not(debug_assertions))] + #[cfg(not(checks_at_least = "paranoid"))] fn from_obj(obj: Gd) -> Self { Self { obj: ManuallyDrop::new(obj), @@ -176,7 +178,7 @@ impl Base { /// # Panics (Debug) /// If called outside an initialization function, or for ref-counted objects on a non-main thread. pub fn to_init_gd(&self) -> Gd { - #[cfg(debug_assertions)] // debug_assert! still checks existence of symbols. + #[cfg(checks_at_least = "paranoid")] // debug_assert! still checks existence of symbols. assert!( self.is_initializing(), "Base::to_init_gd() can only be called during object initialization, inside I*::init() or Gd::from_init_fn()" @@ -248,7 +250,7 @@ impl Base { /// Finalizes the initialization of this `Base` and returns whether pub(crate) fn mark_initialized(&mut self) { - #[cfg(debug_assertions)] + #[cfg(checks_at_least = "paranoid")] { assert_eq!( self.init_state.get(), @@ -265,7 +267,7 @@ impl Base { /// Returns a [`Gd`] referencing the base object, assuming the derived object is fully constructed. #[doc(hidden)] pub fn __fully_constructed_gd(&self) -> Gd { - #[cfg(debug_assertions)] // debug_assert! still checks existence of symbols. + #[cfg(checks_at_least = "paranoid")] // debug_assert! still checks existence of symbols. assert!( !self.is_initializing(), "WithBaseField::to_gd(), base(), base_mut() can only be called on fully-constructed objects, after I*::init() or Gd::from_init_fn()" @@ -296,7 +298,7 @@ impl Base { /// Returns a passive reference to the base object, for use in script contexts only. pub(crate) fn to_script_passive(&self) -> PassiveGd { - #[cfg(debug_assertions)] + #[cfg(checks_at_least = "paranoid")] assert_eq!( self.init_state.get(), InitState::Script, @@ -308,7 +310,7 @@ impl Base { } /// Returns `true` if this `Base` is currently in the initializing state. - #[cfg(debug_assertions)] + #[cfg(checks_at_least = "paranoid")] fn is_initializing(&self) -> bool { self.init_state.get() == InitState::ObjectConstructing } @@ -316,7 +318,7 @@ impl Base { /// Returns a [`Gd`] referencing the base object, assuming the derived object is fully constructed. #[doc(hidden)] pub fn __constructed_gd(&self) -> Gd { - #[cfg(debug_assertions)] // debug_assert! still checks existence of symbols. + #[cfg(checks_at_least = "paranoid")] // debug_assert! still checks existence of symbols. assert!( !self.is_initializing(), "WithBaseField::to_gd(), base(), base_mut() can only be called on fully-constructed objects, after I*::init() or Gd::from_init_fn()" @@ -333,7 +335,7 @@ impl Base { /// # Safety /// Caller must ensure that the underlying object remains valid for the entire lifetime of the returned `PassiveGd`. pub(crate) unsafe fn constructed_passive(&self) -> PassiveGd { - #[cfg(debug_assertions)] // debug_assert! still checks existence of symbols. + #[cfg(checks_at_least = "paranoid")] // debug_assert! still checks existence of symbols. assert!( !self.is_initializing(), "WithBaseField::base(), base_mut() can only be called on fully-constructed objects, after I*::init() or Gd::from_init_fn()" diff --git a/godot-core/src/obj/casts.rs b/godot-core/src/obj/casts.rs index b0504c68b..820c6c475 100644 --- a/godot-core/src/obj/casts.rs +++ b/godot-core/src/obj/casts.rs @@ -48,7 +48,7 @@ impl CastSuccess { } /// Access shared reference to destination, without consuming object. - #[cfg(debug_assertions)] + #[cfg(checks_at_least = "paranoid")] pub fn as_dest_ref(&self) -> &RawGd { self.check_validity(); &self.dest @@ -56,6 +56,7 @@ impl CastSuccess { /// Access exclusive reference to destination, without consuming object. pub fn as_dest_mut(&mut self) -> &mut RawGd { + #[cfg(checks_at_least = "paranoid")] self.check_validity(); &mut self.dest } @@ -70,13 +71,15 @@ impl CastSuccess { self.dest.instance_id_unchecked(), "traded_source must point to the same object as the destination" ); + #[cfg(checks_at_least = "paranoid")] self.check_validity(); std::mem::forget(traded_source); ManuallyDrop::into_inner(self.dest) } + #[cfg(checks_at_least = "paranoid")] fn check_validity(&self) { - debug_assert!(self.dest.is_null() || self.dest.is_instance_valid()); + assert!(self.dest.is_null() || self.dest.is_instance_valid()); } } diff --git a/godot-core/src/obj/gd.rs b/godot-core/src/obj/gd.rs index 474511f86..86714e796 100644 --- a/godot-core/src/obj/gd.rs +++ b/godot-core/src/obj/gd.rs @@ -783,7 +783,9 @@ where } // If ref_counted returned None, that means the instance was destroyed - if ref_counted != Some(false) || !self.is_instance_valid() { + if ref_counted != Some(false) + || (cfg!(checks_at_least = "balanced") && !self.is_instance_valid()) + { return error_or_panic("called free() on already destroyed object".to_string()); } @@ -791,6 +793,7 @@ where // static type information to be correct. This is a no-op in Release mode. // Skip check during panic unwind; would need to rewrite whole thing to use Result instead. Having BOTH panic-in-panic and bad type is // a very unlikely corner case. + #[cfg(checks_at_least = "paranoid")] if !is_panic_unwind { self.raw.check_dynamic_type(&CallContext::gd::("free")); } diff --git a/godot-core/src/obj/raw_gd.rs b/godot-core/src/obj/raw_gd.rs index ef7d6a913..743a066f8 100644 --- a/godot-core/src/obj/raw_gd.rs +++ b/godot-core/src/obj/raw_gd.rs @@ -360,7 +360,7 @@ impl RawGd { debug_assert!(!self.is_null(), "cannot upcast null object refs"); // In Debug builds, go the long path via Godot FFI to verify the results are the same. - #[cfg(debug_assertions)] + #[cfg(checks_at_least = "paranoid")] { // SAFETY: we forget the object below and do not leave the function before. let ffi_dest = self.ffi_cast::().expect("failed FFI upcast"); @@ -381,16 +381,22 @@ impl RawGd { } } - /// Verify that the object is non-null and alive. In Debug mode, additionally verify that it is of type `T` or derived. + /// Verify that the object is non-null and alive. In paranoid mode, additionally verify that it is of type `T` or derived. pub(crate) fn check_rtti(&self, method_name: &'static str) { - let call_ctx = CallContext::gd::(method_name); + #[cfg(checks_at_least = "balanced")] + { + let call_ctx = CallContext::gd::(method_name); + #[cfg(checks_at_least = "paranoid")] + self.check_dynamic_type(&call_ctx); + let instance_id = unsafe { self.instance_id_unchecked().unwrap_unchecked() }; - let instance_id = self.check_dynamic_type(&call_ctx); - classes::ensure_object_alive(instance_id, self.obj_sys(), &call_ctx); + classes::ensure_object_alive(instance_id, self.obj_sys(), &call_ctx); + } } /// Checks only type, not alive-ness. Used in Gd in case of `free()`. - pub(crate) fn check_dynamic_type(&self, call_ctx: &CallContext<'static>) -> InstanceId { + #[cfg(checks_at_least = "paranoid")] + pub(crate) fn check_dynamic_type(&self, call_ctx: &CallContext<'static>) { debug_assert!( !self.is_null(), "{call_ctx}: cannot call method on null object", @@ -400,7 +406,7 @@ impl RawGd { // SAFETY: code surrounding RawGd ensures that `self` is non-null; above is just a sanity check against internal bugs. let rtti = unsafe { rtti.unwrap_unchecked() }; - rtti.check_type::() + rtti.check_type::(); } // Not pub(super) because used by godot::meta::args::ObjectArg. @@ -509,7 +515,7 @@ where let ptr: sys::GDExtensionClassInstancePtr = binding.cast(); - #[cfg(debug_assertions)] + #[cfg(checks_at_least = "paranoid")] crate::classes::ensure_binding_not_null::(ptr); self.cached_storage_ptr.set(ptr); diff --git a/godot-core/src/obj/rtti.rs b/godot-core/src/obj/rtti.rs index 114cf8861..c26d9e228 100644 --- a/godot-core/src/obj/rtti.rs +++ b/godot-core/src/obj/rtti.rs @@ -21,7 +21,7 @@ pub struct ObjectRtti { instance_id: InstanceId, /// Only in Debug mode: dynamic class. - #[cfg(debug_assertions)] + #[cfg(checks_at_least = "paranoid")] class_name: crate::meta::ClassId, // // TODO(bromeon): class_id is not always most-derived class; ObjectRtti is sometimes constructed from a base class, via RawGd::from_obj_sys_weak(). @@ -36,21 +36,19 @@ impl ObjectRtti { Self { instance_id, - #[cfg(debug_assertions)] + #[cfg(checks_at_least = "paranoid")] class_name: T::class_id(), } } - /// Checks that the object is of type `T` or derived. Returns instance ID. + /// Checks that the object is of type `T` or derived. /// /// # Panics - /// In Debug mode, if the object is not of type `T` or derived. + /// In paranoid mode, if the object is not of type `T` or derived. + #[cfg(checks_at_least = "paranoid")] #[inline] - pub fn check_type(&self) -> InstanceId { - #[cfg(debug_assertions)] + pub fn check_type(&self) { crate::classes::ensure_object_inherits(self.class_name, T::class_id(), self.instance_id); - - self.instance_id } #[inline] diff --git a/godot-core/src/storage/mod.rs b/godot-core/src/storage/mod.rs index 3061b6b5d..3f454b167 100644 --- a/godot-core/src/storage/mod.rs +++ b/godot-core/src/storage/mod.rs @@ -123,14 +123,14 @@ mod log_inactive { // ---------------------------------------------------------------------------------------------------------------------------------------------- // Tracking borrows in Debug mode -#[cfg(debug_assertions)] +#[cfg(checks_at_least = "paranoid")] use borrow_info::DebugBorrowTracker; -#[cfg(not(debug_assertions))] +#[cfg(not(checks_at_least = "paranoid"))] use borrow_info_noop::DebugBorrowTracker; use crate::obj::{Base, GodotClass}; -#[cfg(debug_assertions)] +#[cfg(checks_at_least = "paranoid")] mod borrow_info { use std::backtrace::Backtrace; use std::fmt; @@ -195,7 +195,7 @@ mod borrow_info { } } -#[cfg(not(debug_assertions))] +#[cfg(not(checks_at_least = "paranoid"))] mod borrow_info_noop { use std::fmt; diff --git a/godot-ffi/Cargo.toml b/godot-ffi/Cargo.toml index f99655d0a..b612b81aa 100644 --- a/godot-ffi/Cargo.toml +++ b/godot-ffi/Cargo.toml @@ -30,6 +30,10 @@ api-4-4 = ["godot-bindings/api-4-4"] api-4-5 = ["godot-bindings/api-4-5"] # ]] + +debug-checks-balanced = ["godot-bindings/debug-checks-balanced"] +release-checks-fast-unsafe = ["godot-bindings/release-checks-fast-unsafe"] + [dependencies] [target.'cfg(target_os = "linux")'.dependencies] diff --git a/godot-ffi/build.rs b/godot-ffi/build.rs index aa08ea282..8b327ee8a 100644 --- a/godot-ffi/build.rs +++ b/godot-ffi/build.rs @@ -29,4 +29,5 @@ fn main() { godot_bindings::emit_godot_version_cfg(); godot_bindings::emit_wasm_nothreads_cfg(); + godot_bindings::emit_checks_mode(); } diff --git a/godot-ffi/src/lib.rs b/godot-ffi/src/lib.rs index ad7e6655e..47a4a48bc 100644 --- a/godot-ffi/src/lib.rs +++ b/godot-ffi/src/lib.rs @@ -254,11 +254,23 @@ pub unsafe fn deinitialize() { } } +fn safety_checks_string() -> &'static str { + if cfg!(checks_at_least = "paranoid") { + "paranoid" + } else if cfg!(checks_at_least = "balanced") { + "balanced" + } else if cfg!(checks_at_least = "fast-unsafe") { + "fast-unsafe" + } else { + unreachable!(); + } +} + fn print_preamble(version: GDExtensionGodotVersion) { let api_version: &'static str = GdextBuild::godot_static_version_string(); let runtime_version = read_version_string(&version); - - println!("Initialize godot-rust (API {api_version}, runtime {runtime_version})"); + let checks_mode = safety_checks_string(); + println!("Initialize godot-rust (API {api_version}, runtime {runtime_version}, safety checks {checks_mode})"); } /// # Safety diff --git a/godot/Cargo.toml b/godot/Cargo.toml index 7a373841a..72d897d2d 100644 --- a/godot/Cargo.toml +++ b/godot/Cargo.toml @@ -39,6 +39,10 @@ api-4-4 = ["godot-core/api-4-4"] api-4-5 = ["godot-core/api-4-5"] # ]] + +debug-checks-balanced = ["godot-core/debug-checks-balanced"] +release-checks-fast-unsafe = ["godot-core/release-checks-fast-unsafe"] + default = ["__codegen-full"] # Private features, they are under no stability guarantee From 3bffdf60e7b13d683ceaa6af379f1281ff983b98 Mon Sep 17 00:00:00 2001 From: Jan Haller Date: Sat, 25 Oct 2025 17:56:31 +0200 Subject: [PATCH 34/54] Terminology around safety/safeguard levels Renames: - "checks" -> "safeguards" - "debug" -> "dev" (matches Cargo profile) - "paranoid" -> "strict" - "fast-unsafe" -> "disengaged" --- godot-bindings/Cargo.toml | 5 +- godot-bindings/src/lib.rs | 22 +++---- godot-codegen/src/generator/classes.rs | 2 +- .../src/generator/native_structures.rs | 2 +- godot-core/Cargo.toml | 5 +- godot-core/build.rs | 2 +- godot-core/src/builtin/collections/array.rs | 6 +- godot-core/src/classes/class_runtime.rs | 12 ++-- godot-core/src/init/mod.rs | 16 +++++ godot-core/src/meta/error/convert_error.rs | 4 +- godot-core/src/meta/signature.rs | 4 +- godot-core/src/obj/base.rs | 34 +++++------ godot-core/src/obj/casts.rs | 8 +-- godot-core/src/obj/gd.rs | 4 +- godot-core/src/obj/raw_gd.rs | 10 ++-- godot-core/src/obj/rtti.rs | 6 +- godot-core/src/storage/mod.rs | 8 +-- godot-ffi/Cargo.toml | 6 +- godot-ffi/build.rs | 2 +- godot-ffi/src/lib.rs | 16 ++--- godot/Cargo.toml | 6 +- godot/src/lib.rs | 59 ++++++++++++++++++- 22 files changed, 155 insertions(+), 84 deletions(-) diff --git a/godot-bindings/Cargo.toml b/godot-bindings/Cargo.toml index 6f7d60328..d257389fc 100644 --- a/godot-bindings/Cargo.toml +++ b/godot-bindings/Cargo.toml @@ -33,8 +33,9 @@ api-custom = ["dep:bindgen", "dep:regex", "dep:which"] api-custom-json = ["dep:nanoserde", "dep:bindgen", "dep:regex", "dep:which"] api-custom-extheader = [] -debug-checks-balanced = [] -release-checks-fast-unsafe = [] +# Safeguard levels (see godot/lib.rs for detailed documentation). +safeguards-dev-balanced = [] +safeguards-release-disengaged = [] [dependencies] gdextension-api = { workspace = true } diff --git a/godot-bindings/src/lib.rs b/godot-bindings/src/lib.rs index f522b23f2..ce7642c8a 100644 --- a/godot-bindings/src/lib.rs +++ b/godot-bindings/src/lib.rs @@ -268,22 +268,22 @@ pub fn since_api(major_minor: &str) -> bool { !before_api(major_minor) } -pub fn emit_checks_mode() { - let check_modes = ["fast-unsafe", "balanced", "paranoid"]; - let mut checks_level = if cfg!(debug_assertions) { 2 } else { 1 }; +pub fn emit_safeguard_levels() { + let safeguard_modes = ["disengaged", "balanced", "strict"]; + let mut safeguards_level = if cfg!(debug_assertions) { 2 } else { 1 }; #[cfg(debug_assertions)] - if cfg!(feature = "debug-checks-balanced") { - checks_level = 1; + if cfg!(feature = "safeguards-dev-balanced") { + safeguards_level = 1; } #[cfg(not(debug_assertions))] - if cfg!(feature = "release-checks-fast-unsafe") { - checks_level = 0; + if cfg!(feature = "safeguards-release-disengaged") { + safeguards_level = 0; } - for mode in check_modes.iter() { - println!(r#"cargo:rustc-check-cfg=cfg(checks_at_least, values("{mode}"))"#); + for mode in safeguard_modes.iter() { + println!(r#"cargo:rustc-check-cfg=cfg(safeguards_at_least, values("{mode}"))"#); } - for mode in check_modes.iter().take(checks_level + 1) { - println!(r#"cargo:rustc-cfg=checks_at_least="{mode}""#); + for mode in safeguard_modes.iter().take(safeguards_level + 1) { + println!(r#"cargo:rustc-cfg=safeguards_at_least="{mode}""#); } } diff --git a/godot-codegen/src/generator/classes.rs b/godot-codegen/src/generator/classes.rs index a8c48aa74..9b35b05a3 100644 --- a/godot-codegen/src/generator/classes.rs +++ b/godot-codegen/src/generator/classes.rs @@ -195,7 +195,7 @@ fn make_class(class: &Class, ctx: &mut Context, view: &ApiView) -> GeneratedClas fn __checked_id(&self) -> Option { // SAFETY: only Option due to layout-compatibility with RawGd; it is always Some because stored in Gd which is non-null. let rtti = unsafe { self.rtti.as_ref().unwrap_unchecked() }; - #[cfg(checks_at_least = "paranoid")] + #[cfg(safeguards_at_least = "strict")] rtti.check_type::(); Some(rtti.instance_id()) } diff --git a/godot-codegen/src/generator/native_structures.rs b/godot-codegen/src/generator/native_structures.rs index 6c324051a..3b6a7bb3b 100644 --- a/godot-codegen/src/generator/native_structures.rs +++ b/godot-codegen/src/generator/native_structures.rs @@ -219,7 +219,7 @@ fn make_native_structure_field_and_accessor( let obj = #snake_field_name.upcast(); - #[cfg(checks_at_least = "balanced")] + #[cfg(safeguards_at_least = "balanced")] assert!(obj.is_instance_valid(), "provided node is dead"); let id = obj.instance_id().to_u64(); diff --git a/godot-core/Cargo.toml b/godot-core/Cargo.toml index 486729b37..18f4b4124 100644 --- a/godot-core/Cargo.toml +++ b/godot-core/Cargo.toml @@ -38,8 +38,9 @@ api-4-4 = ["godot-ffi/api-4-4"] api-4-5 = ["godot-ffi/api-4-5"] # ]] -debug-checks-balanced = ["godot-ffi/debug-checks-balanced"] -release-checks-fast-unsafe = ["godot-ffi/release-checks-fast-unsafe"] +# Safeguard levels (see godot/lib.rs for detailed documentation). +safeguards-dev-balanced = ["godot-ffi/safeguards-dev-balanced"] +safeguards-release-disengaged = ["godot-ffi/safeguards-release-disengaged"] [dependencies] godot-ffi = { path = "../godot-ffi", version = "=0.4.1" } diff --git a/godot-core/build.rs b/godot-core/build.rs index c7fb9707b..1cb9dd503 100644 --- a/godot-core/build.rs +++ b/godot-core/build.rs @@ -18,5 +18,5 @@ fn main() { godot_bindings::emit_godot_version_cfg(); godot_bindings::emit_wasm_nothreads_cfg(); - godot_bindings::emit_checks_mode(); + godot_bindings::emit_safeguard_levels(); } diff --git a/godot-core/src/builtin/collections/array.rs b/godot-core/src/builtin/collections/array.rs index a1c6ee328..0cf63b47e 100644 --- a/godot-core/src/builtin/collections/array.rs +++ b/godot-core/src/builtin/collections/array.rs @@ -968,7 +968,7 @@ impl Array { } /// Validates that all elements in this array can be converted to integers of type `T`. - #[cfg(checks_at_least = "paranoid")] + #[cfg(safeguards_at_least = "strict")] pub(crate) fn debug_validate_int_elements(&self) -> Result<(), ConvertError> { // SAFETY: every element is internally represented as Variant. let canonical_array = unsafe { self.assume_type_ref::() }; @@ -990,7 +990,7 @@ impl Array { } // No-op in Release. Avoids O(n) conversion checks, but still panics on access. - #[cfg(not(checks_at_least = "paranoid"))] + #[cfg(not(safeguards_at_least = "strict"))] pub(crate) fn debug_validate_int_elements(&self) -> Result<(), ConvertError> { Ok(()) } @@ -1233,7 +1233,7 @@ impl Clone for Array { let copy = unsafe { self.clone_unchecked() }; // Double-check copy's runtime type in Debug mode. - if cfg!(checks_at_least = "paranoid") { + if cfg!(safeguards_at_least = "strict") { copy.with_checked_type() .expect("copied array should have same type as original array") } else { diff --git a/godot-core/src/classes/class_runtime.rs b/godot-core/src/classes/class_runtime.rs index 8573710bd..318bdaa1e 100644 --- a/godot-core/src/classes/class_runtime.rs +++ b/godot-core/src/classes/class_runtime.rs @@ -8,10 +8,10 @@ //! Runtime checks and inspection of Godot classes. use crate::builtin::{GString, StringName, Variant, VariantType}; -#[cfg(checks_at_least = "paranoid")] +#[cfg(safeguards_at_least = "strict")] use crate::classes::{ClassDb, Object}; use crate::meta::CallContext; -#[cfg(checks_at_least = "paranoid")] +#[cfg(safeguards_at_least = "strict")] use crate::meta::ClassId; use crate::obj::{bounds, Bounds, Gd, GodotClass, InstanceId, RawGd, Singleton}; use crate::sys; @@ -191,7 +191,7 @@ where Gd::::from_obj_sys(object_ptr) } -#[cfg(checks_at_least = "balanced")] +#[cfg(safeguards_at_least = "balanced")] pub(crate) fn ensure_object_alive( instance_id: InstanceId, old_object_ptr: sys::GDExtensionObjectPtr, @@ -212,7 +212,7 @@ pub(crate) fn ensure_object_alive( ); } -#[cfg(checks_at_least = "paranoid")] +#[cfg(safeguards_at_least = "strict")] pub(crate) fn ensure_object_inherits(derived: ClassId, base: ClassId, instance_id: InstanceId) { if derived == base || base == Object::class_id() // for Object base, anything inherits by definition @@ -227,7 +227,7 @@ pub(crate) fn ensure_object_inherits(derived: ClassId, base: ClassId, instance_i ) } -#[cfg(checks_at_least = "paranoid")] +#[cfg(safeguards_at_least = "strict")] pub(crate) fn ensure_binding_not_null(binding: sys::GDExtensionClassInstancePtr) where T: GodotClass + Bounds, @@ -255,7 +255,7 @@ where // Implementation of this file /// Checks if `derived` inherits from `base`, using a cache for _successful_ queries. -#[cfg(checks_at_least = "paranoid")] +#[cfg(safeguards_at_least = "strict")] fn is_derived_base_cached(derived: ClassId, base: ClassId) -> bool { use std::collections::HashSet; diff --git a/godot-core/src/init/mod.rs b/godot-core/src/init/mod.rs index a1eee5ada..87e7283af 100644 --- a/godot-core/src/init/mod.rs +++ b/godot-core/src/init/mod.rs @@ -311,8 +311,12 @@ fn gdext_on_level_deinit(level: InitLevel) { /// 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. /// +/// If you use the `disengaged` [safeguard level], you accept that UB becomes possible even **in safe Rust APIs**, if you use them wrong +/// (e.g. accessing a destroyed object). +/// /// [gdextension]: attr.gdextension.html /// [safety]: https://godot-rust.github.io/book/gdext/advanced/safety.html +/// [safeguard level]: ../index.html#safeguard-levels // FIXME intra-doc link #[doc(alias = "entry_symbol", alias = "entry_point")] pub unsafe trait ExtensionLibrary { @@ -530,6 +534,18 @@ unsafe fn ensure_godot_features_compatible() { out!("Check Godot precision setting..."); + #[cfg(feature = "debug-log")] // Display safeguards level in debug log. + let safeguards_level = if cfg!(safeguards_at_least = "strict") { + "strict" + } else if cfg!(safeguards_at_least = "balanced") { + "balanced" + } else if cfg!(safeguards_at_least = "disengaged") { + "disengaged" + } else { + "unknown" + }; + out!("Safeguards: {safeguards_level}"); + let os_class = StringName::from("OS"); let single = GString::from("single"); let double = GString::from("double"); diff --git a/godot-core/src/meta/error/convert_error.rs b/godot-core/src/meta/error/convert_error.rs index 4e019d9e8..53eaf74b0 100644 --- a/godot-core/src/meta/error/convert_error.rs +++ b/godot-core/src/meta/error/convert_error.rs @@ -189,7 +189,7 @@ pub(crate) enum FromGodotError { }, /// Special case of `BadArrayType` where a custom int type such as `i8` cannot hold a dynamic `i64` value. - #[cfg(checks_at_least = "paranoid")] + #[cfg(safeguards_at_least = "strict")] BadArrayTypeInt { expected_int_type: &'static str, value: i64, @@ -234,7 +234,7 @@ impl fmt::Display for FromGodotError { write!(f, "expected array of type {exp_class}, got {act_class}") } - #[cfg(checks_at_least = "paranoid")] + #[cfg(safeguards_at_least = "strict")] Self::BadArrayTypeInt { expected_int_type, value, diff --git a/godot-core/src/meta/signature.rs b/godot-core/src/meta/signature.rs index dadb4b02b..b75717602 100644 --- a/godot-core/src/meta/signature.rs +++ b/godot-core/src/meta/signature.rs @@ -145,7 +145,7 @@ impl Signature { // Note: varcalls are not safe from failing, if they happen through an object pointer -> validity check necessary. // paranoid since we already check the validity in check_rtti, this is unlikely to happen. - #[cfg(checks_at_least = "paranoid")] + #[cfg(safeguards_at_least = "strict")] if let Some(instance_id) = maybe_instance_id { crate::classes::ensure_object_alive(instance_id, object_ptr, &call_ctx); } @@ -307,7 +307,7 @@ impl Signature { // $crate::out!("out_class_ptrcall: {call_ctx}"); // paranoid since we already check the validity in check_rtti, this is unlikely to happen. - #[cfg(checks_at_least = "paranoid")] + #[cfg(safeguards_at_least = "strict")] if let Some(instance_id) = maybe_instance_id { crate::classes::ensure_object_alive(instance_id, object_ptr, &call_ctx); } diff --git a/godot-core/src/obj/base.rs b/godot-core/src/obj/base.rs index adae2e9e9..f1201e316 100644 --- a/godot-core/src/obj/base.rs +++ b/godot-core/src/obj/base.rs @@ -5,7 +5,7 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -#[cfg(checks_at_least = "paranoid")] +#[cfg(safeguards_at_least = "strict")] use std::cell::Cell; use std::cell::RefCell; use std::collections::hash_map::Entry; @@ -27,7 +27,7 @@ thread_local! { } /// Represents the initialization state of a `Base` object. -#[cfg(checks_at_least = "paranoid")] +#[cfg(safeguards_at_least = "strict")] #[derive(Debug, Copy, Clone, PartialEq, Eq)] enum InitState { /// Object is being constructed (inside `I*::init()` or `Gd::from_init_fn()`). @@ -38,14 +38,14 @@ enum InitState { Script, } -#[cfg(checks_at_least = "paranoid")] +#[cfg(safeguards_at_least = "strict")] macro_rules! base_from_obj { ($obj:expr, $state:expr) => { Base::from_obj($obj, $state) }; } -#[cfg(not(checks_at_least = "paranoid"))] +#[cfg(not(safeguards_at_least = "strict"))] macro_rules! base_from_obj { ($obj:expr, $state:expr) => { Base::from_obj($obj) @@ -82,7 +82,7 @@ pub struct Base { /// Tracks the initialization state of this `Base` in Debug mode. /// /// Rc allows to "copy-construct" the base from an existing one, while still affecting the user-instance through the original `Base`. - #[cfg(checks_at_least = "paranoid")] + #[cfg(safeguards_at_least = "strict")] init_state: Rc>, } @@ -95,14 +95,14 @@ impl Base { /// `base` must be alive at the time of invocation, i.e. user `init()` (which could technically destroy it) must not have run yet. /// If `base` is destroyed while the returned `Base` is in use, that constitutes a logic error, not a safety issue. pub(crate) unsafe fn from_base(base: &Base) -> Base { - #[cfg(checks_at_least = "paranoid")] + #[cfg(safeguards_at_least = "strict")] assert!(base.obj.is_instance_valid()); let obj = Gd::from_obj_sys_weak(base.obj.obj_sys()); Self { obj: ManuallyDrop::new(obj), - #[cfg(checks_at_least = "paranoid")] + #[cfg(safeguards_at_least = "strict")] init_state: Rc::clone(&base.init_state), } } @@ -115,7 +115,7 @@ impl Base { /// `gd` must be alive at the time of invocation. If it is destroyed while the returned `Base` is in use, that constitutes a logic /// error, not a safety issue. pub(crate) unsafe fn from_script_gd(gd: &Gd) -> Self { - #[cfg(checks_at_least = "paranoid")] + #[cfg(safeguards_at_least = "strict")] assert!(gd.is_instance_valid()); let obj = Gd::from_obj_sys_weak(gd.obj_sys()); @@ -143,7 +143,7 @@ impl Base { base_from_obj!(obj, InitState::ObjectConstructing) } - #[cfg(checks_at_least = "paranoid")] + #[cfg(safeguards_at_least = "strict")] fn from_obj(obj: Gd, init_state: InitState) -> Self { Self { obj: ManuallyDrop::new(obj), @@ -151,7 +151,7 @@ impl Base { } } - #[cfg(not(checks_at_least = "paranoid"))] + #[cfg(not(safeguards_at_least = "strict"))] fn from_obj(obj: Gd) -> Self { Self { obj: ManuallyDrop::new(obj), @@ -178,7 +178,7 @@ impl Base { /// # Panics (Debug) /// If called outside an initialization function, or for ref-counted objects on a non-main thread. pub fn to_init_gd(&self) -> Gd { - #[cfg(checks_at_least = "paranoid")] // debug_assert! still checks existence of symbols. + #[cfg(safeguards_at_least = "strict")] // debug_assert! still checks existence of symbols. assert!( self.is_initializing(), "Base::to_init_gd() can only be called during object initialization, inside I*::init() or Gd::from_init_fn()" @@ -250,7 +250,7 @@ impl Base { /// Finalizes the initialization of this `Base` and returns whether pub(crate) fn mark_initialized(&mut self) { - #[cfg(checks_at_least = "paranoid")] + #[cfg(safeguards_at_least = "strict")] { assert_eq!( self.init_state.get(), @@ -267,7 +267,7 @@ impl Base { /// Returns a [`Gd`] referencing the base object, assuming the derived object is fully constructed. #[doc(hidden)] pub fn __fully_constructed_gd(&self) -> Gd { - #[cfg(checks_at_least = "paranoid")] // debug_assert! still checks existence of symbols. + #[cfg(safeguards_at_least = "strict")] // debug_assert! still checks existence of symbols. assert!( !self.is_initializing(), "WithBaseField::to_gd(), base(), base_mut() can only be called on fully-constructed objects, after I*::init() or Gd::from_init_fn()" @@ -298,7 +298,7 @@ impl Base { /// Returns a passive reference to the base object, for use in script contexts only. pub(crate) fn to_script_passive(&self) -> PassiveGd { - #[cfg(checks_at_least = "paranoid")] + #[cfg(safeguards_at_least = "strict")] assert_eq!( self.init_state.get(), InitState::Script, @@ -310,7 +310,7 @@ impl Base { } /// Returns `true` if this `Base` is currently in the initializing state. - #[cfg(checks_at_least = "paranoid")] + #[cfg(safeguards_at_least = "strict")] fn is_initializing(&self) -> bool { self.init_state.get() == InitState::ObjectConstructing } @@ -318,7 +318,7 @@ impl Base { /// Returns a [`Gd`] referencing the base object, assuming the derived object is fully constructed. #[doc(hidden)] pub fn __constructed_gd(&self) -> Gd { - #[cfg(checks_at_least = "paranoid")] // debug_assert! still checks existence of symbols. + #[cfg(safeguards_at_least = "strict")] // debug_assert! still checks existence of symbols. assert!( !self.is_initializing(), "WithBaseField::to_gd(), base(), base_mut() can only be called on fully-constructed objects, after I*::init() or Gd::from_init_fn()" @@ -335,7 +335,7 @@ impl Base { /// # Safety /// Caller must ensure that the underlying object remains valid for the entire lifetime of the returned `PassiveGd`. pub(crate) unsafe fn constructed_passive(&self) -> PassiveGd { - #[cfg(checks_at_least = "paranoid")] // debug_assert! still checks existence of symbols. + #[cfg(safeguards_at_least = "strict")] // debug_assert! still checks existence of symbols. assert!( !self.is_initializing(), "WithBaseField::base(), base_mut() can only be called on fully-constructed objects, after I*::init() or Gd::from_init_fn()" diff --git a/godot-core/src/obj/casts.rs b/godot-core/src/obj/casts.rs index 820c6c475..f748027e7 100644 --- a/godot-core/src/obj/casts.rs +++ b/godot-core/src/obj/casts.rs @@ -48,7 +48,7 @@ impl CastSuccess { } /// Access shared reference to destination, without consuming object. - #[cfg(checks_at_least = "paranoid")] + #[cfg(safeguards_at_least = "strict")] pub fn as_dest_ref(&self) -> &RawGd { self.check_validity(); &self.dest @@ -56,7 +56,7 @@ impl CastSuccess { /// Access exclusive reference to destination, without consuming object. pub fn as_dest_mut(&mut self) -> &mut RawGd { - #[cfg(checks_at_least = "paranoid")] + #[cfg(safeguards_at_least = "strict")] self.check_validity(); &mut self.dest } @@ -71,14 +71,14 @@ impl CastSuccess { self.dest.instance_id_unchecked(), "traded_source must point to the same object as the destination" ); - #[cfg(checks_at_least = "paranoid")] + #[cfg(safeguards_at_least = "strict")] self.check_validity(); std::mem::forget(traded_source); ManuallyDrop::into_inner(self.dest) } - #[cfg(checks_at_least = "paranoid")] + #[cfg(safeguards_at_least = "strict")] fn check_validity(&self) { assert!(self.dest.is_null() || self.dest.is_instance_valid()); } diff --git a/godot-core/src/obj/gd.rs b/godot-core/src/obj/gd.rs index 86714e796..5ede7b6ba 100644 --- a/godot-core/src/obj/gd.rs +++ b/godot-core/src/obj/gd.rs @@ -784,7 +784,7 @@ where // If ref_counted returned None, that means the instance was destroyed if ref_counted != Some(false) - || (cfg!(checks_at_least = "balanced") && !self.is_instance_valid()) + || (cfg!(safeguards_at_least = "balanced") && !self.is_instance_valid()) { return error_or_panic("called free() on already destroyed object".to_string()); } @@ -793,7 +793,7 @@ where // static type information to be correct. This is a no-op in Release mode. // Skip check during panic unwind; would need to rewrite whole thing to use Result instead. Having BOTH panic-in-panic and bad type is // a very unlikely corner case. - #[cfg(checks_at_least = "paranoid")] + #[cfg(safeguards_at_least = "strict")] if !is_panic_unwind { self.raw.check_dynamic_type(&CallContext::gd::("free")); } diff --git a/godot-core/src/obj/raw_gd.rs b/godot-core/src/obj/raw_gd.rs index 743a066f8..61dddc637 100644 --- a/godot-core/src/obj/raw_gd.rs +++ b/godot-core/src/obj/raw_gd.rs @@ -360,7 +360,7 @@ impl RawGd { debug_assert!(!self.is_null(), "cannot upcast null object refs"); // In Debug builds, go the long path via Godot FFI to verify the results are the same. - #[cfg(checks_at_least = "paranoid")] + #[cfg(safeguards_at_least = "strict")] { // SAFETY: we forget the object below and do not leave the function before. let ffi_dest = self.ffi_cast::().expect("failed FFI upcast"); @@ -383,10 +383,10 @@ impl RawGd { /// Verify that the object is non-null and alive. In paranoid mode, additionally verify that it is of type `T` or derived. pub(crate) fn check_rtti(&self, method_name: &'static str) { - #[cfg(checks_at_least = "balanced")] + #[cfg(safeguards_at_least = "balanced")] { let call_ctx = CallContext::gd::(method_name); - #[cfg(checks_at_least = "paranoid")] + #[cfg(safeguards_at_least = "strict")] self.check_dynamic_type(&call_ctx); let instance_id = unsafe { self.instance_id_unchecked().unwrap_unchecked() }; @@ -395,7 +395,7 @@ impl RawGd { } /// Checks only type, not alive-ness. Used in Gd in case of `free()`. - #[cfg(checks_at_least = "paranoid")] + #[cfg(safeguards_at_least = "strict")] pub(crate) fn check_dynamic_type(&self, call_ctx: &CallContext<'static>) { debug_assert!( !self.is_null(), @@ -515,7 +515,7 @@ where let ptr: sys::GDExtensionClassInstancePtr = binding.cast(); - #[cfg(checks_at_least = "paranoid")] + #[cfg(safeguards_at_least = "strict")] crate::classes::ensure_binding_not_null::(ptr); self.cached_storage_ptr.set(ptr); diff --git a/godot-core/src/obj/rtti.rs b/godot-core/src/obj/rtti.rs index c26d9e228..e97e27182 100644 --- a/godot-core/src/obj/rtti.rs +++ b/godot-core/src/obj/rtti.rs @@ -21,7 +21,7 @@ pub struct ObjectRtti { instance_id: InstanceId, /// Only in Debug mode: dynamic class. - #[cfg(checks_at_least = "paranoid")] + #[cfg(safeguards_at_least = "strict")] class_name: crate::meta::ClassId, // // TODO(bromeon): class_id is not always most-derived class; ObjectRtti is sometimes constructed from a base class, via RawGd::from_obj_sys_weak(). @@ -36,7 +36,7 @@ impl ObjectRtti { Self { instance_id, - #[cfg(checks_at_least = "paranoid")] + #[cfg(safeguards_at_least = "strict")] class_name: T::class_id(), } } @@ -45,7 +45,7 @@ impl ObjectRtti { /// /// # Panics /// In paranoid mode, if the object is not of type `T` or derived. - #[cfg(checks_at_least = "paranoid")] + #[cfg(safeguards_at_least = "strict")] #[inline] pub fn check_type(&self) { crate::classes::ensure_object_inherits(self.class_name, T::class_id(), self.instance_id); diff --git a/godot-core/src/storage/mod.rs b/godot-core/src/storage/mod.rs index 3f454b167..a3db331e5 100644 --- a/godot-core/src/storage/mod.rs +++ b/godot-core/src/storage/mod.rs @@ -123,14 +123,14 @@ mod log_inactive { // ---------------------------------------------------------------------------------------------------------------------------------------------- // Tracking borrows in Debug mode -#[cfg(checks_at_least = "paranoid")] +#[cfg(safeguards_at_least = "strict")] use borrow_info::DebugBorrowTracker; -#[cfg(not(checks_at_least = "paranoid"))] +#[cfg(not(safeguards_at_least = "strict"))] use borrow_info_noop::DebugBorrowTracker; use crate::obj::{Base, GodotClass}; -#[cfg(checks_at_least = "paranoid")] +#[cfg(safeguards_at_least = "strict")] mod borrow_info { use std::backtrace::Backtrace; use std::fmt; @@ -195,7 +195,7 @@ mod borrow_info { } } -#[cfg(not(checks_at_least = "paranoid"))] +#[cfg(not(safeguards_at_least = "strict"))] mod borrow_info_noop { use std::fmt; diff --git a/godot-ffi/Cargo.toml b/godot-ffi/Cargo.toml index b612b81aa..641f36240 100644 --- a/godot-ffi/Cargo.toml +++ b/godot-ffi/Cargo.toml @@ -30,9 +30,9 @@ api-4-4 = ["godot-bindings/api-4-4"] api-4-5 = ["godot-bindings/api-4-5"] # ]] - -debug-checks-balanced = ["godot-bindings/debug-checks-balanced"] -release-checks-fast-unsafe = ["godot-bindings/release-checks-fast-unsafe"] +# Safeguard levels (see godot/lib.rs for detailed documentation). +safeguards-dev-balanced = ["godot-bindings/safeguards-dev-balanced"] +safeguards-release-disengaged = ["godot-bindings/safeguards-release-disengaged"] [dependencies] diff --git a/godot-ffi/build.rs b/godot-ffi/build.rs index 8b327ee8a..7dfed4d3c 100644 --- a/godot-ffi/build.rs +++ b/godot-ffi/build.rs @@ -29,5 +29,5 @@ fn main() { godot_bindings::emit_godot_version_cfg(); godot_bindings::emit_wasm_nothreads_cfg(); - godot_bindings::emit_checks_mode(); + godot_bindings::emit_safeguard_levels(); } diff --git a/godot-ffi/src/lib.rs b/godot-ffi/src/lib.rs index 47a4a48bc..d9edd930e 100644 --- a/godot-ffi/src/lib.rs +++ b/godot-ffi/src/lib.rs @@ -254,13 +254,13 @@ pub unsafe fn deinitialize() { } } -fn safety_checks_string() -> &'static str { - if cfg!(checks_at_least = "paranoid") { - "paranoid" - } else if cfg!(checks_at_least = "balanced") { +fn safeguards_level_string() -> &'static str { + if cfg!(safeguards_at_least = "strict") { + "strict" + } else if cfg!(safeguards_at_least = "balanced") { "balanced" - } else if cfg!(checks_at_least = "fast-unsafe") { - "fast-unsafe" + } else if cfg!(safeguards_at_least = "disengaged") { + "disengaged" } else { unreachable!(); } @@ -269,8 +269,8 @@ fn safety_checks_string() -> &'static str { fn print_preamble(version: GDExtensionGodotVersion) { let api_version: &'static str = GdextBuild::godot_static_version_string(); let runtime_version = read_version_string(&version); - let checks_mode = safety_checks_string(); - println!("Initialize godot-rust (API {api_version}, runtime {runtime_version}, safety checks {checks_mode})"); + let safeguards_level = safeguards_level_string(); + println!("Initialize godot-rust (API {api_version}, runtime {runtime_version}, safeguards {safeguards_level})"); } /// # Safety diff --git a/godot/Cargo.toml b/godot/Cargo.toml index 72d897d2d..ddd1026d4 100644 --- a/godot/Cargo.toml +++ b/godot/Cargo.toml @@ -39,9 +39,9 @@ api-4-4 = ["godot-core/api-4-4"] api-4-5 = ["godot-core/api-4-5"] # ]] - -debug-checks-balanced = ["godot-core/debug-checks-balanced"] -release-checks-fast-unsafe = ["godot-core/release-checks-fast-unsafe"] +# Safeguard levels (see godot/lib.rs for detailed documentation). +safeguards-dev-balanced = ["godot-core/safeguards-dev-balanced"] +safeguards-release-disengaged = ["godot-core/safeguards-release-disengaged"] default = ["__codegen-full"] diff --git a/godot/src/lib.rs b/godot/src/lib.rs index ab149d85c..dcc16a133 100644 --- a/godot/src/lib.rs +++ b/godot/src/lib.rs @@ -58,6 +58,47 @@ //!

//! //! +//! ## Safeguard levels +//! +//! godot-rust uses three tiers that differ in the amount of runtime checks and validations that are performed. \ +//! They can be configured via [Cargo features](#cargo-features). +//! +//! - 🛡️ **Strict** (default for dev builds) +//! +//! Lots of additional, sometimes expensive checks. Detects many bugs during development. +//! - `Gd::bind/bind_mut()` provides extensive diagnostics to locate runtime borrow errors. +//! - `Array` safe conversion checks (for types like `Array`). +//! - RTTI checks on object access (protect against type mismatch edge cases). +//! - Geometric invariants (e.g. normalized quaternions). +//! - Access to engine APIs outside valid scope.

+//! +//! - ⚖️ **Balanced** (default for release builds) +//! +//! Basic validity and invariant checks, reasonably fast. Within this level, you should not be able to encounter undefined behavior (UB) +//! in safe Rust code. Invariant violations may however cause panics and logic errors. +//! - Object liveness checks. +//! - `Gd::bind/bind_mut()` cause panics on borrow errors.

+//! +//! - ☣️ **Disengaged** +//! +//! Most checks disabled, sacrifices safety for raw speed. This renders a large part of the godot-rust API `unsafe` without polluting the +//! code; you opt in via `unsafe impl ExtensionLibrary`. +//! +//! Before using this, measure to ensure you truly need the last bit of performance (balanced should be fast enough for most cases; if not, +//! consider bringing it up). Also test your code thoroughly using the other levels first. Undefined behavior and crashes arising +//! from using this level are your full responsibility. When reporting a bug, make sure you can reproduce it under the balanced level. +//! - Unchecked object access -> instant UB if an object is dead. +//! - `Gd::bind/bind_mut()` are unchecked -> UB if mutable aliasing occurs. +//! +//!
+//!

Safeguards are a recent addition to godot-rust and need calibrating over time. If you are unhappy with how the balanced level +//! performs in basic operations, consider bringing it up for discussion. We'd like to offer the disengaged level for power users who +//! really need it, but it shouldn't be the only choice for decent runtime performance, as it comes with heavy trade-offs.

+//! +//!

As of v0.4, the above checks are not fully implemented yet. Neither are they guarantees; categorization may change over time.

+//!
+//! +//! //! ## Cargo features //! //! The following features can be enabled for this crate. All of them are off by default. @@ -114,9 +155,9 @@ //! //! By default, Wasm threads are enabled and require the flag `"-C", "link-args=-pthread"` in the `wasm32-unknown-unknown` target. //! This must be kept in sync with Godot's Web export settings (threading support enabled). To disable it, use **additionally* the feature -//! `experimental-wasm-nothreads`.

+//! `experimental-wasm-nothreads`. //! -//! It is recommended to use this feature in combination with `lazy-function-tables` to reduce the size of the generated Wasm binary. +//! It is recommended to use this feature in combination with `lazy-function-tables` to reduce the size of the generated Wasm binary.

//! //! * **`experimental-wasm-nothreads`** //! @@ -136,7 +177,19 @@ //! This feature requires at least Godot 4.3. //! See also: [`#[derive(GodotClass)]`](register/derive.GodotClass.html#documentation) //! -//! _Integrations:_ +//! _Safeguards:_ +//! +//! See [Safeguard levels](#safeguard-levels). Levels can only be downgraded by 1 at the moment. +//! +//! * **`safeguards-dev-balanced`** +//! +//! For the `dev` Cargo profile, use the **balanced** safeguard level instead of the default strict level.

+//! +//! * **`safeguards-release-disengaged`** +//! +//! For the `release` Cargo profile, use the **disengaged** safeguard level instead of the default balanced level. +//! +//! _Third-party integrations:_ //! //! * **`serde`** //! From 096af2e8cb08792a8a8af968c205979fc2a83e19 Mon Sep 17 00:00:00 2001 From: Jan Haller Date: Sat, 25 Oct 2025 19:14:55 +0200 Subject: [PATCH 35/54] Experimental support for required parameters/returns in Godot APIs --- godot-codegen/Cargo.toml | 1 + godot-codegen/src/conv/type_conversions.rs | 33 +++++++++-- .../src/generator/functions_common.rs | 30 +++++----- godot-codegen/src/generator/signals.rs | 5 +- godot-codegen/src/lib.rs | 11 ++++ godot-codegen/src/models/domain.rs | 55 +++++++++++++------ godot-codegen/src/models/json.rs | 2 + .../src/special_cases/special_cases.rs | 4 ++ godot-core/Cargo.toml | 1 + godot/Cargo.toml | 1 + godot/src/lib.rs | 24 +++++++- itest/rust/Cargo.toml | 2 +- itest/rust/src/engine_tests/node_test.rs | 24 ++++++++ 13 files changed, 152 insertions(+), 41 deletions(-) diff --git a/godot-codegen/Cargo.toml b/godot-codegen/Cargo.toml index 60032a960..b97445101 100644 --- a/godot-codegen/Cargo.toml +++ b/godot-codegen/Cargo.toml @@ -20,6 +20,7 @@ api-custom = ["godot-bindings/api-custom"] api-custom-json = ["godot-bindings/api-custom-json"] experimental-godot-api = [] experimental-threads = [] +experimental-required-objs = [] [dependencies] godot-bindings = { path = "../godot-bindings", version = "=0.4.1" } diff --git a/godot-codegen/src/conv/type_conversions.rs b/godot-codegen/src/conv/type_conversions.rs index 443154290..d63e7698e 100644 --- a/godot-codegen/src/conv/type_conversions.rs +++ b/godot-codegen/src/conv/type_conversions.rs @@ -67,7 +67,11 @@ fn to_hardcoded_rust_ident(full_ty: &GodotTy) -> Option<&str> { ("real_t", None) => "real", ("void", None) => "c_void", - (ty, Some(meta)) => panic!("unhandled type {ty:?} with meta {meta:?}"), + // meta="required" is a special case of non-null object parameters/return types. + // Other metas are unrecognized. + (ty, Some(meta)) if meta != "required" => { + panic!("unhandled type {ty:?} with meta {meta:?}") + } _ => return None, }; @@ -244,13 +248,30 @@ fn to_rust_type_uncached(full_ty: &GodotTy, ctx: &mut Context) -> RustTy { arg_passing: ctx.get_builtin_arg_passing(full_ty), } } else { - let ty = rustify_ty(ty); - let qualified_class = quote! { crate::classes::#ty }; + let is_nullable = if cfg!(feature = "experimental-required-objs") { + full_ty.meta.as_ref().is_none_or(|m| m != "required") + } else { + true + }; + + let inner_class = rustify_ty(ty); + let qualified_class = quote! { crate::classes::#inner_class }; + + // Stores unwrapped Gd directly in `gd_tokens`. + let gd_tokens = quote! { Gd<#qualified_class> }; + + // Use Option for `impl_as_object_arg` if nullable. + let impl_as_object_arg = if is_nullable { + quote! { impl AsArg>> } + } else { + quote! { impl AsArg> } + }; RustTy::EngineClass { - tokens: quote! { Gd<#qualified_class> }, - impl_as_object_arg: quote! { impl AsArg>> }, - inner_class: ty, + gd_tokens, + impl_as_object_arg, + inner_class, + is_nullable, } } } diff --git a/godot-codegen/src/generator/functions_common.rs b/godot-codegen/src/generator/functions_common.rs index 7381bc9ff..5708bfddf 100644 --- a/godot-codegen/src/generator/functions_common.rs +++ b/godot-codegen/src/generator/functions_common.rs @@ -446,23 +446,23 @@ pub(crate) fn make_param_or_field_type( let mut special_ty = None; let param_ty = match ty { - // Objects: impl AsArg> + // Objects: impl AsArg> or impl AsArg>>. RustTy::EngineClass { - impl_as_object_arg, - inner_class, - .. + impl_as_object_arg, .. } => { let lft = lifetimes.next(); - special_ty = Some(quote! { CowArg<#lft, Option>> }); + + // #ty is already Gd<...> or Option> depending on nullability. + special_ty = Some(quote! { CowArg<#lft, #ty> }); match decl { FnParamDecl::FnPublic => quote! { #impl_as_object_arg }, FnParamDecl::FnPublicLifetime => quote! { #impl_as_object_arg + 'a }, FnParamDecl::FnInternal => { - quote! { CowArg>> } + quote! { CowArg<#ty> } } FnParamDecl::Field => { - quote! { CowArg<'a, Option>> } + quote! { CowArg<'a, #ty> } } } } @@ -615,16 +615,20 @@ pub(crate) fn make_virtual_param_type( function_sig: &dyn Function, ) -> TokenStream { match param_ty { - // Virtual methods accept Option>, since we don't know whether objects are nullable or required. - RustTy::EngineClass { .. } - if !special_cases::is_class_method_param_required( + RustTy::EngineClass { gd_tokens, .. } => { + if special_cases::is_class_method_param_required( function_sig.surrounding_class().unwrap(), function_sig.godot_name(), param_name, - ) => - { - quote! { Option<#param_ty> } + ) { + // For special-cased EngineClass params, use Gd without Option. + gd_tokens.clone() + } else { + // In general, virtual methods accept Option>, since we don't know whether objects are nullable or required. + quote! { Option<#gd_tokens> } + } } + _ => quote! { #param_ty }, } } diff --git a/godot-codegen/src/generator/signals.rs b/godot-codegen/src/generator/signals.rs index ca099390f..c74f382f3 100644 --- a/godot-codegen/src/generator/signals.rs +++ b/godot-codegen/src/generator/signals.rs @@ -291,9 +291,10 @@ impl SignalParams { for param in params.iter() { let param_name = safe_ident(¶m.name.to_string()); let param_ty = ¶m.type_; + let param_ty_tokens = param_ty.tokens_non_null(); - param_list.extend(quote! { #param_name: #param_ty, }); - type_list.extend(quote! { #param_ty, }); + param_list.extend(quote! { #param_name: #param_ty_tokens, }); + type_list.extend(quote! { #param_ty_tokens, }); name_list.extend(quote! { #param_name, }); let formatted_ty = match param_ty { diff --git a/godot-codegen/src/lib.rs b/godot-codegen/src/lib.rs index 215525739..71d5ce128 100644 --- a/godot-codegen/src/lib.rs +++ b/godot-codegen/src/lib.rs @@ -51,6 +51,17 @@ pub const IS_CODEGEN_FULL: bool = false; #[cfg(feature = "codegen-full")] pub const IS_CODEGEN_FULL: bool = true; +#[cfg(all(feature = "experimental-required-objs", before_api = "4.6"))] +fn __feature_warning() { + // Not a hard error, it's experimental anyway and allows more flexibility like this. + #[must_use = "The `experimental-required-objs` feature needs at least Godot 4.6-dev version"] + fn feature_has_no_effect() -> i32 { + 1 + } + + feature_has_no_effect(); +} + fn write_file(path: &Path, contents: String) { let dir = path.parent().unwrap(); let _ = std::fs::create_dir_all(dir); diff --git a/godot-codegen/src/models/domain.rs b/godot-codegen/src/models/domain.rs index c7b1d2f4e..a95a515a2 100644 --- a/godot-codegen/src/models/domain.rs +++ b/godot-codegen/src/models/domain.rs @@ -676,15 +676,8 @@ impl FnReturn { pub fn type_tokens(&self) -> TokenStream { match &self.type_ { - Some(RustTy::EngineClass { tokens, .. }) => { - quote! { Option<#tokens> } - } - Some(ty) => { - quote! { #ty } - } - _ => { - quote! { () } - } + Some(ty) => ty.to_token_stream(), + _ => quote! { () }, } } @@ -726,6 +719,7 @@ pub struct GodotTy { pub enum RustTy { /// `bool`, `Vector3i`, `Array`, `GString` BuiltinIdent { ty: Ident, arg_passing: ArgPassing }, + /// Pointers declared in `gdextension_interface` such as `sys::GDExtensionInitializationFunction` /// used as parameters in some APIs. SysPointerType { tokens: TokenStream }, @@ -761,16 +755,19 @@ pub enum RustTy { /// `Gd` EngineClass { - /// Tokens with full `Gd` (e.g. used in return type position). - tokens: TokenStream, + /// Tokens with full `Gd`, never `Option>`. + gd_tokens: TokenStream, - /// Signature declaration with `impl AsArg>`. + /// Signature declaration with `impl AsArg>` or `impl AsArg>>`. impl_as_object_arg: TokenStream, - /// only inner `T` - #[allow(dead_code)] - // only read in minimal config + RustTy::default_extender_field_decl() + /// Only inner `Node`. inner_class: Ident, + + /// Whether this object parameter/return is nullable in the GDExtension API. + /// + /// Defaults to true (nullable). Only false when meta="required". + is_nullable: bool, }, /// Receiver type of default parameters extender constructor. @@ -789,9 +786,20 @@ impl RustTy { pub fn return_decl(&self) -> TokenStream { match self { - Self::EngineClass { tokens, .. } => quote! { -> Option<#tokens> }, Self::GenericArray => quote! { -> Array }, - other => quote! { -> #other }, + _ => quote! { -> #self }, + } + } + + /// Returns tokens without `Option` wrapper, even for nullable engine classes. + /// + /// For `EngineClass`, always returns `Gd` regardless of nullability. For other types, behaves the same as `ToTokens`. + // TODO(v0.5): only used for signal params, which is a bug. Those should conservatively be Option> as well. + // Might also be useful to directly extract inner `gd_tokens` field. + pub fn tokens_non_null(&self) -> TokenStream { + match self { + Self::EngineClass { gd_tokens, .. } => gd_tokens.clone(), + other => other.to_token_stream(), } } @@ -849,7 +857,18 @@ impl ToTokens for RustTy { } => quote! { *mut #inner }.to_tokens(tokens), RustTy::EngineArray { tokens: path, .. } => path.to_tokens(tokens), RustTy::EngineEnum { tokens: path, .. } => path.to_tokens(tokens), - RustTy::EngineClass { tokens: path, .. } => path.to_tokens(tokens), + RustTy::EngineClass { + is_nullable, + gd_tokens: path, + .. + } => { + // Return nullable-aware type: Option> if nullable, else Gd. + if *is_nullable { + quote! { Option<#path> }.to_tokens(tokens) + } else { + path.to_tokens(tokens) + } + } RustTy::ExtenderReceiver { tokens: path } => path.to_tokens(tokens), RustTy::GenericArray => quote! { Array }.to_tokens(tokens), RustTy::SysPointerType { tokens: path } => path.to_tokens(tokens), diff --git a/godot-codegen/src/models/json.rs b/godot-codegen/src/models/json.rs index 335f29146..6e3b4ad0c 100644 --- a/godot-codegen/src/models/json.rs +++ b/godot-codegen/src/models/json.rs @@ -238,6 +238,7 @@ pub struct JsonMethodArg { pub name: String, #[nserde(rename = "type")] pub type_: String, + /// Extra information about the type (e.g. which integer). Value "required" indicates non-nullable class types (Godot 4.6+). pub meta: Option, pub default_value: Option, } @@ -247,6 +248,7 @@ pub struct JsonMethodArg { pub struct JsonMethodReturn { #[nserde(rename = "type")] pub type_: String, + /// Extra information about the type (e.g. which integer). Value "required" indicates non-nullable class types (Godot 4.6+). pub meta: Option, } diff --git a/godot-codegen/src/special_cases/special_cases.rs b/godot-codegen/src/special_cases/special_cases.rs index afe1f86a7..1d515177d 100644 --- a/godot-codegen/src/special_cases/special_cases.rs +++ b/godot-codegen/src/special_cases/special_cases.rs @@ -758,6 +758,10 @@ pub fn is_class_method_param_required( godot_method_name: &str, param: &Ident, // Don't use `&str` to avoid to_string() allocations for each check on call-site. ) -> bool { + // TODO(v0.5): this overlaps now slightly with Godot's own "required" meta in extension_api.json. + // Having an override list can always be useful, but possibly the two inputs (here + JSON) should be evaluated at the same time, + // during JSON->Domain mapping. + // Note: magically, it's enough if a base class method is declared here; it will be picked up by derived classes. match (class_name.godot_ty.as_str(), godot_method_name) { diff --git a/godot-core/Cargo.toml b/godot-core/Cargo.toml index a31cd61a1..d11425f30 100644 --- a/godot-core/Cargo.toml +++ b/godot-core/Cargo.toml @@ -22,6 +22,7 @@ codegen-lazy-fptrs = [ double-precision = ["godot-codegen/double-precision"] experimental-godot-api = ["godot-codegen/experimental-godot-api"] experimental-threads = ["godot-ffi/experimental-threads", "godot-codegen/experimental-threads"] +experimental-required-objs = ["godot-codegen/experimental-required-objs"] experimental-wasm-nothreads = ["godot-ffi/experimental-wasm-nothreads"] debug-log = ["godot-ffi/debug-log"] trace = [] diff --git a/godot/Cargo.toml b/godot/Cargo.toml index 7a373841a..35b53e0f2 100644 --- a/godot/Cargo.toml +++ b/godot/Cargo.toml @@ -19,6 +19,7 @@ custom-json = ["api-custom-json"] double-precision = ["godot-core/double-precision"] experimental-godot-api = ["godot-core/experimental-godot-api"] experimental-threads = ["godot-core/experimental-threads"] +experimental-required-objs = ["godot-core/experimental-required-objs"] experimental-wasm = [] experimental-wasm-nothreads = ["godot-core/experimental-wasm-nothreads"] codegen-rustfmt = ["godot-core/codegen-rustfmt"] diff --git a/godot/src/lib.rs b/godot/src/lib.rs index ab149d85c..d68cf434c 100644 --- a/godot/src/lib.rs +++ b/godot/src/lib.rs @@ -88,7 +88,13 @@ //! * **`experimental-godot-api`** //! //! Access to `godot::classes` APIs that Godot marks "experimental". These are under heavy development and may change at any time. -//! If you opt in to this feature, expect breaking changes at compile and runtime. +//! If you opt in to this feature, expect breaking changes at compile and runtime.

+//! +//! * **`experimental-required-objs`** +//! +//! Enables _required_ objects in Godot function signatures. When GDExtension advertises parameters or return value as required (non-null), the +//! generated code will use `Gd` instead of `Option>` for type safety. This will undergo many breaking changes as the API evolves; +//! we are explicitly excluding this from any SemVer guarantees. Needs Godot 4.6-dev. See . //! //! _Rust functionality toggles:_ //! @@ -220,3 +226,19 @@ pub use godot_core::private; /// Often-imported symbols. pub mod prelude; + +/// Tests for code that must not compile. +// Do not add #[cfg(test)], it seems to break `cargo test -p godot --features godot/api-custom,godot/experimental-required-objs`. +mod no_compile_tests { + /// ```compile_fail + /// use godot::prelude::*; + /// let mut node: Gd = todo!(); + /// let option = Some(node.clone()); + /// let option: Option<&Gd> = option.as_ref(); + /// + /// // Following must not compile since `add_child` accepts only required (non-null) arguments. Comment-out for sanity check. + /// node.add_child(option); + /// ``` + #[cfg(feature = "experimental-required-objs")] + fn __test_invalid_patterns() {} +} diff --git a/itest/rust/Cargo.toml b/itest/rust/Cargo.toml index 0694779fb..cd7c79ebc 100644 --- a/itest/rust/Cargo.toml +++ b/itest/rust/Cargo.toml @@ -13,7 +13,7 @@ crate-type = ["cdylib"] # Default feature MUST be empty for workflow reasons, even if it differs from the default feature set in upstream `godot` crate. default = [] codegen-full = ["godot/__codegen-full"] -codegen-full-experimental = ["codegen-full", "godot/experimental-godot-api"] +codegen-full-experimental = ["codegen-full", "godot/experimental-godot-api", "godot/experimental-required-objs"] experimental-threads = ["godot/experimental-threads"] register-docs = ["godot/register-docs"] serde = ["dep:serde", "dep:serde_json", "godot/serde"] diff --git a/itest/rust/src/engine_tests/node_test.rs b/itest/rust/src/engine_tests/node_test.rs index badabfe6c..d4fc4f721 100644 --- a/itest/rust/src/engine_tests/node_test.rs +++ b/itest/rust/src/engine_tests/node_test.rs @@ -74,3 +74,27 @@ fn node_call_group(ctx: &TestContext) { tree.call_group("group", "remove_meta", vslice!["something"]); assert!(!node.has_meta("something")); } + +// Experimental required parameter/return value. +/* TODO(v0.5): enable once https://github.com/godot-rust/gdext/pull/1383 is merged. +#[cfg(all(feature = "codegen-full-experimental", since_api = "4.6"))] +#[itest] +fn node_required_param_return() { + use godot::classes::Tween; + use godot::obj::Gd; + + let mut parent = Node::new_alloc(); + let child = Node::new_alloc(); + + // add_child() takes required arg, so this still works. + // (Test for Option *not* working anymore is in godot > no_compile_tests.) + parent.add_child(&child); + + // create_tween() returns now non-null instance. + let tween: Gd = parent.create_tween(); + assert!(tween.is_instance_valid()); + assert!(tween.to_string().contains("Tween")); + + parent.free(); +} +*/ From bde2c1ab4814edd728b1e594010fb23aa061d96d Mon Sep 17 00:00:00 2001 From: Jan Haller Date: Sat, 25 Oct 2025 19:17:10 +0200 Subject: [PATCH 36/54] Make #[cfg] model for safeguards easier to understand Use simple #[cfg]s: `safeguards_strict` + `safeguards_balanced`. This implies "at least this safety level". No more indirect `*_at_least = "string"` conditions and extra negation. --- godot-bindings/src/lib.rs | 16 ++++++--- godot-codegen/src/generator/classes.rs | 2 +- .../src/generator/native_structures.rs | 2 +- godot-core/src/builtin/collections/array.rs | 6 ++-- godot-core/src/classes/class_runtime.rs | 12 +++---- godot-core/src/init/mod.rs | 8 ++--- godot-core/src/meta/error/convert_error.rs | 4 +-- godot-core/src/meta/signature.rs | 4 +-- godot-core/src/obj/base.rs | 35 ++++++++++--------- godot-core/src/obj/casts.rs | 8 ++--- godot-core/src/obj/gd.rs | 13 ++++--- godot-core/src/obj/raw_gd.rs | 10 +++--- godot-core/src/obj/rtti.rs | 6 ++-- godot-core/src/storage/mod.rs | 8 ++--- godot-ffi/src/lib.rs | 8 ++--- godot/src/lib.rs | 2 +- 16 files changed, 73 insertions(+), 71 deletions(-) diff --git a/godot-bindings/src/lib.rs b/godot-bindings/src/lib.rs index ce7642c8a..464fd3983 100644 --- a/godot-bindings/src/lib.rs +++ b/godot-bindings/src/lib.rs @@ -269,8 +269,10 @@ pub fn since_api(major_minor: &str) -> bool { } pub fn emit_safeguard_levels() { - let safeguard_modes = ["disengaged", "balanced", "strict"]; + // Levels: disengaged (0), balanced (1), strict (2) let mut safeguards_level = if cfg!(debug_assertions) { 2 } else { 1 }; + + // Override default level with Cargo feature, in dev/release profiles. #[cfg(debug_assertions)] if cfg!(feature = "safeguards-dev-balanced") { safeguards_level = 1; @@ -280,10 +282,14 @@ pub fn emit_safeguard_levels() { safeguards_level = 0; } - for mode in safeguard_modes.iter() { - println!(r#"cargo:rustc-check-cfg=cfg(safeguards_at_least, values("{mode}"))"#); + println!(r#"cargo:rustc-check-cfg=cfg(safeguards_balanced)"#); + println!(r#"cargo:rustc-check-cfg=cfg(safeguards_strict)"#); + + // Emit #[cfg]s cumulatively: strict builds get both balanced and strict. + if safeguards_level >= 1 { + println!(r#"cargo:rustc-cfg=safeguards_balanced"#); } - for mode in safeguard_modes.iter().take(safeguards_level + 1) { - println!(r#"cargo:rustc-cfg=safeguards_at_least="{mode}""#); + if safeguards_level >= 2 { + println!(r#"cargo:rustc-cfg=safeguards_strict"#); } } diff --git a/godot-codegen/src/generator/classes.rs b/godot-codegen/src/generator/classes.rs index 9b35b05a3..8b6a031e0 100644 --- a/godot-codegen/src/generator/classes.rs +++ b/godot-codegen/src/generator/classes.rs @@ -195,7 +195,7 @@ fn make_class(class: &Class, ctx: &mut Context, view: &ApiView) -> GeneratedClas fn __checked_id(&self) -> Option { // SAFETY: only Option due to layout-compatibility with RawGd; it is always Some because stored in Gd which is non-null. let rtti = unsafe { self.rtti.as_ref().unwrap_unchecked() }; - #[cfg(safeguards_at_least = "strict")] + #[cfg(safeguards_strict)] rtti.check_type::(); Some(rtti.instance_id()) } diff --git a/godot-codegen/src/generator/native_structures.rs b/godot-codegen/src/generator/native_structures.rs index 3b6a7bb3b..01f7bfd50 100644 --- a/godot-codegen/src/generator/native_structures.rs +++ b/godot-codegen/src/generator/native_structures.rs @@ -219,7 +219,7 @@ fn make_native_structure_field_and_accessor( let obj = #snake_field_name.upcast(); - #[cfg(safeguards_at_least = "balanced")] + #[cfg(safeguards_balanced)] assert!(obj.is_instance_valid(), "provided node is dead"); let id = obj.instance_id().to_u64(); diff --git a/godot-core/src/builtin/collections/array.rs b/godot-core/src/builtin/collections/array.rs index 0cf63b47e..4079b7bdc 100644 --- a/godot-core/src/builtin/collections/array.rs +++ b/godot-core/src/builtin/collections/array.rs @@ -968,7 +968,7 @@ impl Array { } /// Validates that all elements in this array can be converted to integers of type `T`. - #[cfg(safeguards_at_least = "strict")] + #[cfg(safeguards_strict)] pub(crate) fn debug_validate_int_elements(&self) -> Result<(), ConvertError> { // SAFETY: every element is internally represented as Variant. let canonical_array = unsafe { self.assume_type_ref::() }; @@ -990,7 +990,7 @@ impl Array { } // No-op in Release. Avoids O(n) conversion checks, but still panics on access. - #[cfg(not(safeguards_at_least = "strict"))] + #[cfg(not(safeguards_strict))] pub(crate) fn debug_validate_int_elements(&self) -> Result<(), ConvertError> { Ok(()) } @@ -1233,7 +1233,7 @@ impl Clone for Array { let copy = unsafe { self.clone_unchecked() }; // Double-check copy's runtime type in Debug mode. - if cfg!(safeguards_at_least = "strict") { + if cfg!(safeguards_strict) { copy.with_checked_type() .expect("copied array should have same type as original array") } else { diff --git a/godot-core/src/classes/class_runtime.rs b/godot-core/src/classes/class_runtime.rs index 318bdaa1e..7e82063f3 100644 --- a/godot-core/src/classes/class_runtime.rs +++ b/godot-core/src/classes/class_runtime.rs @@ -8,10 +8,10 @@ //! Runtime checks and inspection of Godot classes. use crate::builtin::{GString, StringName, Variant, VariantType}; -#[cfg(safeguards_at_least = "strict")] +#[cfg(safeguards_strict)] use crate::classes::{ClassDb, Object}; use crate::meta::CallContext; -#[cfg(safeguards_at_least = "strict")] +#[cfg(safeguards_strict)] use crate::meta::ClassId; use crate::obj::{bounds, Bounds, Gd, GodotClass, InstanceId, RawGd, Singleton}; use crate::sys; @@ -191,7 +191,7 @@ where Gd::::from_obj_sys(object_ptr) } -#[cfg(safeguards_at_least = "balanced")] +#[cfg(safeguards_balanced)] pub(crate) fn ensure_object_alive( instance_id: InstanceId, old_object_ptr: sys::GDExtensionObjectPtr, @@ -212,7 +212,7 @@ pub(crate) fn ensure_object_alive( ); } -#[cfg(safeguards_at_least = "strict")] +#[cfg(safeguards_strict)] pub(crate) fn ensure_object_inherits(derived: ClassId, base: ClassId, instance_id: InstanceId) { if derived == base || base == Object::class_id() // for Object base, anything inherits by definition @@ -227,7 +227,7 @@ pub(crate) fn ensure_object_inherits(derived: ClassId, base: ClassId, instance_i ) } -#[cfg(safeguards_at_least = "strict")] +#[cfg(safeguards_strict)] pub(crate) fn ensure_binding_not_null(binding: sys::GDExtensionClassInstancePtr) where T: GodotClass + Bounds, @@ -255,7 +255,7 @@ where // Implementation of this file /// Checks if `derived` inherits from `base`, using a cache for _successful_ queries. -#[cfg(safeguards_at_least = "strict")] +#[cfg(safeguards_strict)] fn is_derived_base_cached(derived: ClassId, base: ClassId) -> bool { use std::collections::HashSet; diff --git a/godot-core/src/init/mod.rs b/godot-core/src/init/mod.rs index 87e7283af..e35b65bae 100644 --- a/godot-core/src/init/mod.rs +++ b/godot-core/src/init/mod.rs @@ -535,14 +535,12 @@ unsafe fn ensure_godot_features_compatible() { out!("Check Godot precision setting..."); #[cfg(feature = "debug-log")] // Display safeguards level in debug log. - let safeguards_level = if cfg!(safeguards_at_least = "strict") { + let safeguards_level = if cfg!(safeguards_strict) { "strict" - } else if cfg!(safeguards_at_least = "balanced") { + } else if cfg!(safeguards_balanced) { "balanced" - } else if cfg!(safeguards_at_least = "disengaged") { - "disengaged" } else { - "unknown" + "disengaged" }; out!("Safeguards: {safeguards_level}"); diff --git a/godot-core/src/meta/error/convert_error.rs b/godot-core/src/meta/error/convert_error.rs index 53eaf74b0..ea5996d5f 100644 --- a/godot-core/src/meta/error/convert_error.rs +++ b/godot-core/src/meta/error/convert_error.rs @@ -189,7 +189,7 @@ pub(crate) enum FromGodotError { }, /// Special case of `BadArrayType` where a custom int type such as `i8` cannot hold a dynamic `i64` value. - #[cfg(safeguards_at_least = "strict")] + #[cfg(safeguards_strict)] BadArrayTypeInt { expected_int_type: &'static str, value: i64, @@ -234,7 +234,7 @@ impl fmt::Display for FromGodotError { write!(f, "expected array of type {exp_class}, got {act_class}") } - #[cfg(safeguards_at_least = "strict")] + #[cfg(safeguards_strict)] Self::BadArrayTypeInt { expected_int_type, value, diff --git a/godot-core/src/meta/signature.rs b/godot-core/src/meta/signature.rs index b75717602..470add4b8 100644 --- a/godot-core/src/meta/signature.rs +++ b/godot-core/src/meta/signature.rs @@ -145,7 +145,7 @@ impl Signature { // Note: varcalls are not safe from failing, if they happen through an object pointer -> validity check necessary. // paranoid since we already check the validity in check_rtti, this is unlikely to happen. - #[cfg(safeguards_at_least = "strict")] + #[cfg(safeguards_strict)] if let Some(instance_id) = maybe_instance_id { crate::classes::ensure_object_alive(instance_id, object_ptr, &call_ctx); } @@ -307,7 +307,7 @@ impl Signature { // $crate::out!("out_class_ptrcall: {call_ctx}"); // paranoid since we already check the validity in check_rtti, this is unlikely to happen. - #[cfg(safeguards_at_least = "strict")] + #[cfg(safeguards_strict)] if let Some(instance_id) = maybe_instance_id { crate::classes::ensure_object_alive(instance_id, object_ptr, &call_ctx); } diff --git a/godot-core/src/obj/base.rs b/godot-core/src/obj/base.rs index f1201e316..0a359968d 100644 --- a/godot-core/src/obj/base.rs +++ b/godot-core/src/obj/base.rs @@ -5,13 +5,14 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -#[cfg(safeguards_at_least = "strict")] +#[cfg(safeguards_strict)] use std::cell::Cell; use std::cell::RefCell; use std::collections::hash_map::Entry; use std::collections::HashMap; use std::fmt::{Debug, Display, Formatter, Result as FmtResult}; use std::mem::ManuallyDrop; +#[cfg(safeguards_strict)] use std::rc::Rc; use crate::builtin::Callable; @@ -27,7 +28,7 @@ thread_local! { } /// Represents the initialization state of a `Base` object. -#[cfg(safeguards_at_least = "strict")] +#[cfg(safeguards_strict)] #[derive(Debug, Copy, Clone, PartialEq, Eq)] enum InitState { /// Object is being constructed (inside `I*::init()` or `Gd::from_init_fn()`). @@ -38,14 +39,14 @@ enum InitState { Script, } -#[cfg(safeguards_at_least = "strict")] +#[cfg(safeguards_strict)] macro_rules! base_from_obj { ($obj:expr, $state:expr) => { Base::from_obj($obj, $state) }; } -#[cfg(not(safeguards_at_least = "strict"))] +#[cfg(not(safeguards_strict))] macro_rules! base_from_obj { ($obj:expr, $state:expr) => { Base::from_obj($obj) @@ -82,7 +83,7 @@ pub struct Base { /// Tracks the initialization state of this `Base` in Debug mode. /// /// Rc allows to "copy-construct" the base from an existing one, while still affecting the user-instance through the original `Base`. - #[cfg(safeguards_at_least = "strict")] + #[cfg(safeguards_strict)] init_state: Rc>, } @@ -95,14 +96,14 @@ impl Base { /// `base` must be alive at the time of invocation, i.e. user `init()` (which could technically destroy it) must not have run yet. /// If `base` is destroyed while the returned `Base` is in use, that constitutes a logic error, not a safety issue. pub(crate) unsafe fn from_base(base: &Base) -> Base { - #[cfg(safeguards_at_least = "strict")] + #[cfg(safeguards_strict)] assert!(base.obj.is_instance_valid()); let obj = Gd::from_obj_sys_weak(base.obj.obj_sys()); Self { obj: ManuallyDrop::new(obj), - #[cfg(safeguards_at_least = "strict")] + #[cfg(safeguards_strict)] init_state: Rc::clone(&base.init_state), } } @@ -115,7 +116,7 @@ impl Base { /// `gd` must be alive at the time of invocation. If it is destroyed while the returned `Base` is in use, that constitutes a logic /// error, not a safety issue. pub(crate) unsafe fn from_script_gd(gd: &Gd) -> Self { - #[cfg(safeguards_at_least = "strict")] + #[cfg(safeguards_strict)] assert!(gd.is_instance_valid()); let obj = Gd::from_obj_sys_weak(gd.obj_sys()); @@ -143,7 +144,7 @@ impl Base { base_from_obj!(obj, InitState::ObjectConstructing) } - #[cfg(safeguards_at_least = "strict")] + #[cfg(safeguards_strict)] fn from_obj(obj: Gd, init_state: InitState) -> Self { Self { obj: ManuallyDrop::new(obj), @@ -151,7 +152,7 @@ impl Base { } } - #[cfg(not(safeguards_at_least = "strict"))] + #[cfg(not(safeguards_strict))] fn from_obj(obj: Gd) -> Self { Self { obj: ManuallyDrop::new(obj), @@ -178,7 +179,7 @@ impl Base { /// # Panics (Debug) /// If called outside an initialization function, or for ref-counted objects on a non-main thread. pub fn to_init_gd(&self) -> Gd { - #[cfg(safeguards_at_least = "strict")] // debug_assert! still checks existence of symbols. + #[cfg(safeguards_strict)] // debug_assert! still checks existence of symbols. assert!( self.is_initializing(), "Base::to_init_gd() can only be called during object initialization, inside I*::init() or Gd::from_init_fn()" @@ -250,7 +251,7 @@ impl Base { /// Finalizes the initialization of this `Base` and returns whether pub(crate) fn mark_initialized(&mut self) { - #[cfg(safeguards_at_least = "strict")] + #[cfg(safeguards_strict)] { assert_eq!( self.init_state.get(), @@ -267,7 +268,7 @@ impl Base { /// Returns a [`Gd`] referencing the base object, assuming the derived object is fully constructed. #[doc(hidden)] pub fn __fully_constructed_gd(&self) -> Gd { - #[cfg(safeguards_at_least = "strict")] // debug_assert! still checks existence of symbols. + #[cfg(safeguards_strict)] // debug_assert! still checks existence of symbols. assert!( !self.is_initializing(), "WithBaseField::to_gd(), base(), base_mut() can only be called on fully-constructed objects, after I*::init() or Gd::from_init_fn()" @@ -298,7 +299,7 @@ impl Base { /// Returns a passive reference to the base object, for use in script contexts only. pub(crate) fn to_script_passive(&self) -> PassiveGd { - #[cfg(safeguards_at_least = "strict")] + #[cfg(safeguards_strict)] assert_eq!( self.init_state.get(), InitState::Script, @@ -310,7 +311,7 @@ impl Base { } /// Returns `true` if this `Base` is currently in the initializing state. - #[cfg(safeguards_at_least = "strict")] + #[cfg(safeguards_strict)] fn is_initializing(&self) -> bool { self.init_state.get() == InitState::ObjectConstructing } @@ -318,7 +319,7 @@ impl Base { /// Returns a [`Gd`] referencing the base object, assuming the derived object is fully constructed. #[doc(hidden)] pub fn __constructed_gd(&self) -> Gd { - #[cfg(safeguards_at_least = "strict")] // debug_assert! still checks existence of symbols. + #[cfg(safeguards_strict)] // debug_assert! still checks existence of symbols. assert!( !self.is_initializing(), "WithBaseField::to_gd(), base(), base_mut() can only be called on fully-constructed objects, after I*::init() or Gd::from_init_fn()" @@ -335,7 +336,7 @@ impl Base { /// # Safety /// Caller must ensure that the underlying object remains valid for the entire lifetime of the returned `PassiveGd`. pub(crate) unsafe fn constructed_passive(&self) -> PassiveGd { - #[cfg(safeguards_at_least = "strict")] // debug_assert! still checks existence of symbols. + #[cfg(safeguards_strict)] // debug_assert! still checks existence of symbols. assert!( !self.is_initializing(), "WithBaseField::base(), base_mut() can only be called on fully-constructed objects, after I*::init() or Gd::from_init_fn()" diff --git a/godot-core/src/obj/casts.rs b/godot-core/src/obj/casts.rs index f748027e7..9571963e2 100644 --- a/godot-core/src/obj/casts.rs +++ b/godot-core/src/obj/casts.rs @@ -48,7 +48,7 @@ impl CastSuccess { } /// Access shared reference to destination, without consuming object. - #[cfg(safeguards_at_least = "strict")] + #[cfg(safeguards_strict)] pub fn as_dest_ref(&self) -> &RawGd { self.check_validity(); &self.dest @@ -56,7 +56,7 @@ impl CastSuccess { /// Access exclusive reference to destination, without consuming object. pub fn as_dest_mut(&mut self) -> &mut RawGd { - #[cfg(safeguards_at_least = "strict")] + #[cfg(safeguards_strict)] self.check_validity(); &mut self.dest } @@ -71,14 +71,14 @@ impl CastSuccess { self.dest.instance_id_unchecked(), "traded_source must point to the same object as the destination" ); - #[cfg(safeguards_at_least = "strict")] + #[cfg(safeguards_strict)] self.check_validity(); std::mem::forget(traded_source); ManuallyDrop::into_inner(self.dest) } - #[cfg(safeguards_at_least = "strict")] + #[cfg(safeguards_strict)] fn check_validity(&self) { assert!(self.dest.is_null() || self.dest.is_instance_valid()); } diff --git a/godot-core/src/obj/gd.rs b/godot-core/src/obj/gd.rs index 5ede7b6ba..18af4b086 100644 --- a/godot-core/src/obj/gd.rs +++ b/godot-core/src/obj/gd.rs @@ -15,8 +15,8 @@ use sys::{static_assert_eq_size_align, SysPtr as _}; use crate::builtin::{Callable, GString, NodePath, StringName, Variant}; use crate::meta::error::{ConvertError, FromFfiError}; use crate::meta::{ - ArrayElement, AsArg, CallContext, ClassId, FromGodot, GodotConvert, GodotType, - PropertyHintInfo, RefArg, ToGodot, + ArrayElement, AsArg, ClassId, FromGodot, GodotConvert, GodotType, PropertyHintInfo, RefArg, + ToGodot, }; use crate::obj::{ bounds, cap, Bounds, DynGd, GdDerefTarget, GdMut, GdRef, GodotClass, Inherits, InstanceId, @@ -783,9 +783,7 @@ where } // If ref_counted returned None, that means the instance was destroyed - if ref_counted != Some(false) - || (cfg!(safeguards_at_least = "balanced") && !self.is_instance_valid()) - { + if ref_counted != Some(false) || (cfg!(safeguards_balanced) && !self.is_instance_valid()) { return error_or_panic("called free() on already destroyed object".to_string()); } @@ -793,9 +791,10 @@ where // static type information to be correct. This is a no-op in Release mode. // Skip check during panic unwind; would need to rewrite whole thing to use Result instead. Having BOTH panic-in-panic and bad type is // a very unlikely corner case. - #[cfg(safeguards_at_least = "strict")] + #[cfg(safeguards_strict)] if !is_panic_unwind { - self.raw.check_dynamic_type(&CallContext::gd::("free")); + self.raw + .check_dynamic_type(&crate::meta::CallContext::gd::("free")); } // SAFETY: object must be alive, which was just checked above. No multithreading here. diff --git a/godot-core/src/obj/raw_gd.rs b/godot-core/src/obj/raw_gd.rs index 61dddc637..e8d7c23d6 100644 --- a/godot-core/src/obj/raw_gd.rs +++ b/godot-core/src/obj/raw_gd.rs @@ -360,7 +360,7 @@ impl RawGd { debug_assert!(!self.is_null(), "cannot upcast null object refs"); // In Debug builds, go the long path via Godot FFI to verify the results are the same. - #[cfg(safeguards_at_least = "strict")] + #[cfg(safeguards_strict)] { // SAFETY: we forget the object below and do not leave the function before. let ffi_dest = self.ffi_cast::().expect("failed FFI upcast"); @@ -383,10 +383,10 @@ impl RawGd { /// Verify that the object is non-null and alive. In paranoid mode, additionally verify that it is of type `T` or derived. pub(crate) fn check_rtti(&self, method_name: &'static str) { - #[cfg(safeguards_at_least = "balanced")] + #[cfg(safeguards_balanced)] { let call_ctx = CallContext::gd::(method_name); - #[cfg(safeguards_at_least = "strict")] + #[cfg(safeguards_strict)] self.check_dynamic_type(&call_ctx); let instance_id = unsafe { self.instance_id_unchecked().unwrap_unchecked() }; @@ -395,7 +395,7 @@ impl RawGd { } /// Checks only type, not alive-ness. Used in Gd in case of `free()`. - #[cfg(safeguards_at_least = "strict")] + #[cfg(safeguards_strict)] pub(crate) fn check_dynamic_type(&self, call_ctx: &CallContext<'static>) { debug_assert!( !self.is_null(), @@ -515,7 +515,7 @@ where let ptr: sys::GDExtensionClassInstancePtr = binding.cast(); - #[cfg(safeguards_at_least = "strict")] + #[cfg(safeguards_strict)] crate::classes::ensure_binding_not_null::(ptr); self.cached_storage_ptr.set(ptr); diff --git a/godot-core/src/obj/rtti.rs b/godot-core/src/obj/rtti.rs index e97e27182..bf21a3c47 100644 --- a/godot-core/src/obj/rtti.rs +++ b/godot-core/src/obj/rtti.rs @@ -21,7 +21,7 @@ pub struct ObjectRtti { instance_id: InstanceId, /// Only in Debug mode: dynamic class. - #[cfg(safeguards_at_least = "strict")] + #[cfg(safeguards_strict)] class_name: crate::meta::ClassId, // // TODO(bromeon): class_id is not always most-derived class; ObjectRtti is sometimes constructed from a base class, via RawGd::from_obj_sys_weak(). @@ -36,7 +36,7 @@ impl ObjectRtti { Self { instance_id, - #[cfg(safeguards_at_least = "strict")] + #[cfg(safeguards_strict)] class_name: T::class_id(), } } @@ -45,7 +45,7 @@ impl ObjectRtti { /// /// # Panics /// In paranoid mode, if the object is not of type `T` or derived. - #[cfg(safeguards_at_least = "strict")] + #[cfg(safeguards_strict)] #[inline] pub fn check_type(&self) { crate::classes::ensure_object_inherits(self.class_name, T::class_id(), self.instance_id); diff --git a/godot-core/src/storage/mod.rs b/godot-core/src/storage/mod.rs index a3db331e5..552fd4df1 100644 --- a/godot-core/src/storage/mod.rs +++ b/godot-core/src/storage/mod.rs @@ -123,14 +123,14 @@ mod log_inactive { // ---------------------------------------------------------------------------------------------------------------------------------------------- // Tracking borrows in Debug mode -#[cfg(safeguards_at_least = "strict")] +#[cfg(safeguards_strict)] use borrow_info::DebugBorrowTracker; -#[cfg(not(safeguards_at_least = "strict"))] +#[cfg(not(safeguards_strict))] use borrow_info_noop::DebugBorrowTracker; use crate::obj::{Base, GodotClass}; -#[cfg(safeguards_at_least = "strict")] +#[cfg(safeguards_strict)] mod borrow_info { use std::backtrace::Backtrace; use std::fmt; @@ -195,7 +195,7 @@ mod borrow_info { } } -#[cfg(not(safeguards_at_least = "strict"))] +#[cfg(not(safeguards_strict))] mod borrow_info_noop { use std::fmt; diff --git a/godot-ffi/src/lib.rs b/godot-ffi/src/lib.rs index d9edd930e..2dd2a5c37 100644 --- a/godot-ffi/src/lib.rs +++ b/godot-ffi/src/lib.rs @@ -255,14 +255,12 @@ pub unsafe fn deinitialize() { } fn safeguards_level_string() -> &'static str { - if cfg!(safeguards_at_least = "strict") { + if cfg!(safeguards_strict) { "strict" - } else if cfg!(safeguards_at_least = "balanced") { + } else if cfg!(safeguards_balanced) { "balanced" - } else if cfg!(safeguards_at_least = "disengaged") { - "disengaged" } else { - unreachable!(); + "disengaged" } } diff --git a/godot/src/lib.rs b/godot/src/lib.rs index dcc16a133..d6c75b5e7 100644 --- a/godot/src/lib.rs +++ b/godot/src/lib.rs @@ -179,7 +179,7 @@ //! //! _Safeguards:_ //! -//! See [Safeguard levels](#safeguard-levels). Levels can only be downgraded by 1 at the moment. +//! See [Safeguard levels](#safeguard-levels). //! //! * **`safeguards-dev-balanced`** //! From 010b5a5442cfcd72d73981d28386752d978837e3 Mon Sep 17 00:00:00 2001 From: Jan Haller Date: Sat, 25 Oct 2025 22:51:22 +0200 Subject: [PATCH 37/54] Update itests to conform with new safeguard levels --- itest/rust/build.rs | 1 + .../builtin_tests/containers/variant_test.rs | 14 ++++----- itest/rust/src/framework/mod.rs | 8 +++++ itest/rust/src/object_tests/base_init_test.rs | 14 +++++---- itest/rust/src/object_tests/base_test.rs | 16 +++++----- .../rust/src/object_tests/object_swap_test.rs | 4 +-- itest/rust/src/object_tests/object_test.rs | 30 +++++++++---------- 7 files changed, 50 insertions(+), 37 deletions(-) diff --git a/itest/rust/build.rs b/itest/rust/build.rs index fd16833bd..6ca87b303 100644 --- a/itest/rust/build.rs +++ b/itest/rust/build.rs @@ -272,6 +272,7 @@ fn main() { rustfmt_if_needed(vec![rust_file]); godot_bindings::emit_godot_version_cfg(); + godot_bindings::emit_safeguard_levels(); // The godot crate has a __codegen-full default feature that enables the godot-codegen/codegen-full feature. When compiling the entire // workspace itest also gets compiled with full codegen due to feature unification. This causes compiler errors since the diff --git a/itest/rust/src/builtin_tests/containers/variant_test.rs b/itest/rust/src/builtin_tests/containers/variant_test.rs index 4eadfbb32..18e620157 100644 --- a/itest/rust/src/builtin_tests/containers/variant_test.rs +++ b/itest/rust/src/builtin_tests/containers/variant_test.rs @@ -21,7 +21,7 @@ use godot::obj::{Gd, InstanceId, NewAlloc, NewGd}; use godot::sys::GodotFfi; use crate::common::roundtrip; -use crate::framework::{expect_panic, itest, runs_release}; +use crate::framework::{expect_panic, expect_panic_or_ub, itest, runs_release}; const TEST_BASIS: Basis = Basis::from_rows( Vector3::new(1.0, 2.0, 3.0), @@ -272,7 +272,7 @@ fn variant_dead_object_conversions() { ); // Variant::to(). - expect_panic("Variant::to() with dead object should panic", || { + expect_panic_or_ub("Variant::to() with dead object should panic", || { let _: Gd = variant.to(); }); @@ -361,15 +361,15 @@ fn variant_array_from_untyped_conversions() { ) } -// Same builtin type INT, but incompatible integers (Debug-only). +// Same builtin type INT, but incompatible integers (strict-safeguards only). #[itest] fn variant_array_bad_integer_conversions() { let i32_array: Array = array![1, 2, 160, -40]; let i32_variant = i32_array.to_variant(); let i8_back = i32_variant.try_to::>(); - // In Debug mode, we expect an error upon conversion. - #[cfg(debug_assertions)] + // In strict safeguard mode, we expect an error upon conversion. + #[cfg(safeguards_strict)] { let err = i8_back.expect_err("Array -> Array conversion should fail"); assert_eq!( @@ -378,8 +378,8 @@ fn variant_array_bad_integer_conversions() { ) } - // In Release mode, we expect the conversion to succeed, but a panic to occur on element access. - #[cfg(not(debug_assertions))] + // In balanced/disengaged modes, we expect the conversion to succeed, but a panic to occur on element access. + #[cfg(not(safeguards_strict))] { let i8_array = i8_back.expect("Array -> Array conversion should succeed"); expect_panic("accessing element 160 as i8 should panic", || { diff --git a/itest/rust/src/framework/mod.rs b/itest/rust/src/framework/mod.rs index e5ced4764..4fb7a4b3a 100644 --- a/itest/rust/src/framework/mod.rs +++ b/itest/rust/src/framework/mod.rs @@ -200,6 +200,14 @@ pub fn expect_panic(context: &str, code: impl FnOnce()) { ); } +/// Run for code that panics in *strict* and *balanced* safeguard levels, but cause UB in *disengaged* level. +/// +/// The code is not executed for the latter. +pub fn expect_panic_or_ub(_context: &str, _code: impl FnOnce()) { + #[cfg(safeguards_balanced)] + expect_panic(_context, _code); +} + pin_project_lite::pin_project! { pub struct ExpectPanicFuture { context: &'static str, diff --git a/itest/rust/src/object_tests/base_init_test.rs b/itest/rust/src/object_tests/base_init_test.rs index d78e58ade..43a7d9497 100644 --- a/itest/rust/src/object_tests/base_init_test.rs +++ b/itest/rust/src/object_tests/base_init_test.rs @@ -10,7 +10,7 @@ use godot::builtin::Vector2; use godot::classes::notify::ObjectNotification; use godot::classes::{ClassDb, IRefCounted, RefCounted}; use godot::meta::ToGodot; -use godot::obj::{Base, Gd, InstanceId, NewAlloc, NewGd, Singleton, WithBaseField}; +use godot::obj::{Base, Gd, InstanceId, NewGd, Singleton, WithBaseField}; use godot::register::{godot_api, GodotClass}; use godot::task::TaskHandle; @@ -65,13 +65,14 @@ fn base_init_extracted_gd() { // Checks bad practice of rug-pulling the base pointer. #[itest] +#[cfg(safeguards_balanced)] fn base_init_freed_gd() { let mut free_executed = false; expect_panic("base object is destroyed", || { let _obj = Gd::::from_init_fn(|base| { let obj = base.to_init_gd(); - obj.free(); // Causes the problem, but doesn't panic yet. + obj.free(); // Causes the problem, but doesn't panic yet. UB in safeguards-disengaged. free_executed = true; Based { base, i: 456 } @@ -144,27 +145,28 @@ fn verify_complex_init((obj, base): (Gd, Gd)) -> Instance id } -#[cfg(debug_assertions)] +#[cfg(safeguards_strict)] #[itest] fn base_init_outside_init() { + use godot::obj::NewAlloc; let mut obj = Based::new_alloc(); expect_panic("to_init_gd() outside init() function", || { let guard = obj.bind_mut(); - let _gd = guard.base.to_init_gd(); // Panics in Debug builds. + let _gd = guard.base.to_init_gd(); // Panics in strict safeguard mode. }); obj.free(); } -#[cfg(debug_assertions)] +#[cfg(safeguards_strict)] #[itest] fn base_init_to_gd() { expect_panic("WithBaseField::to_gd() inside init() function", || { let _obj = Gd::::from_init_fn(|base| { let temp_obj = Based { base, i: 999 }; - // Call to self.to_gd() during initialization should panic in Debug builds. + // Call to self.to_gd() during initialization should panic in strict safeguard mode. let _gd = godot::obj::WithBaseField::to_gd(&temp_obj); temp_obj diff --git a/itest/rust/src/object_tests/base_test.rs b/itest/rust/src/object_tests/base_test.rs index 7278cc32e..52ca2b2bc 100644 --- a/itest/rust/src/object_tests/base_test.rs +++ b/itest/rust/src/object_tests/base_test.rs @@ -7,7 +7,7 @@ use godot::prelude::*; -use crate::framework::{expect_panic, itest}; +use crate::framework::{expect_panic_or_ub, itest}; #[itest(skip)] fn base_test_is_weak() { @@ -82,6 +82,8 @@ fn base_gd_self() { } // Hardening against https://github.com/godot-rust/gdext/issues/711. +// Furthermore, this now keeps expect_panic_or_ub() instead of expect_panic(), although it's questionable if much remains with all these checks +// disabled. There is still some logic remaining, and an alternative would be to #[cfg(safeguards_balanced)] this. #[itest] fn base_smuggling() { let (mut obj, extracted_base) = create_object_with_extracted_base(); @@ -98,19 +100,19 @@ fn base_smuggling() { extracted_base_obj.free(); // Access to object should now fail. - expect_panic("object with dead base: calling base methods", || { + expect_panic_or_ub("object with dead base: calling base methods", || { obj.get_position(); }); - expect_panic("object with dead base: bind()", || { + expect_panic_or_ub("object with dead base: bind()", || { obj.bind(); }); - expect_panic("object with dead base: instance_id()", || { + expect_panic_or_ub("object with dead base: instance_id()", || { obj.instance_id(); }); - expect_panic("object with dead base: clone()", || { + expect_panic_or_ub("object with dead base: clone()", || { let _ = obj.clone(); }); - expect_panic("object with dead base: upcast()", || { + expect_panic_or_ub("object with dead base: upcast()", || { obj.upcast::(); }); @@ -118,7 +120,7 @@ fn base_smuggling() { let (obj, extracted_base) = create_object_with_extracted_base(); obj.free(); - expect_panic("accessing extracted base of dead object", || { + expect_panic_or_ub("accessing extracted base of dead object", || { extracted_base.__constructed_gd().get_position(); }); } diff --git a/itest/rust/src/object_tests/object_swap_test.rs b/itest/rust/src/object_tests/object_swap_test.rs index b1cea92ea..d636bd01e 100644 --- a/itest/rust/src/object_tests/object_swap_test.rs +++ b/itest/rust/src/object_tests/object_swap_test.rs @@ -10,8 +10,8 @@ // A lot these tests also exist in the `object_test` module, where they test object lifetime rather than type swapping. // TODO consolidate them, so that it's less likely to forget edge cases. -// Disabled in Release mode, since we don't perform the subtype check there. -#![cfg(debug_assertions)] +// Disabled in balanced/disengaged levels, since we don't perform the subtype check there. +#![cfg(safeguards_strict)] use godot::builtin::GString; use godot::classes::{Node, Node3D, Object}; diff --git a/itest/rust/src/object_tests/object_test.rs b/itest/rust/src/object_tests/object_test.rs index 6fd31ae30..fa817ea3d 100644 --- a/itest/rust/src/object_tests/object_test.rs +++ b/itest/rust/src/object_tests/object_test.rs @@ -22,7 +22,7 @@ use godot::obj::{Base, Gd, Inherits, InstanceId, NewAlloc, NewGd, RawGd, Singlet use godot::register::{godot_api, GodotClass}; use godot::sys::{self, interface_fn, GodotFfi}; -use crate::framework::{expect_panic, itest, TestContext}; +use crate::framework::{expect_panic, expect_panic_or_ub, itest, TestContext}; // TODO: // * make sure that ptrcalls are used when possible (i.e. when type info available; maybe GDScript integration test) @@ -171,7 +171,7 @@ fn object_instance_id_when_freed() { node.clone().free(); // destroys object without moving out of reference assert!(!node.is_instance_valid()); - expect_panic("instance_id() on dead object", move || { + expect_panic_or_ub("instance_id() on dead object", move || { node.instance_id(); }); } @@ -235,7 +235,7 @@ fn object_user_bind_after_free() { let copy = obj.clone(); obj.free(); - expect_panic("bind() on dead user object", move || { + expect_panic_or_ub("bind() on dead user object", move || { let _ = copy.bind(); }); } @@ -247,7 +247,7 @@ fn object_user_free_during_bind() { let copy = obj.clone(); // TODO clone allowed while bound? - expect_panic("direct free() on user while it's bound", move || { + expect_panic_or_ub("direct free() on user while it's bound", move || { copy.free(); }); @@ -275,7 +275,7 @@ fn object_engine_freed_argument_passing(ctx: &TestContext) { // Destroy object and then pass it to a Godot engine API. node.free(); - expect_panic("pass freed Gd to Godot engine API (T=Node)", || { + expect_panic_or_ub("pass freed Gd to Godot engine API (T=Node)", || { tree.add_child(&node2); }); } @@ -288,10 +288,10 @@ fn object_user_freed_casts() { // Destroy object and then pass it to a Godot engine API (upcast itself works, see other tests). obj.free(); - expect_panic("Gd::upcast() on dead object (T=user)", || { + expect_panic_or_ub("Gd::upcast() on dead object (T=user)", || { let _ = obj2.upcast::(); }); - expect_panic("Gd::cast() on dead object (T=user)", || { + expect_panic_or_ub("Gd::cast() on dead object (T=user)", || { let _ = base_obj.cast::(); }); } @@ -306,7 +306,7 @@ fn object_user_freed_argument_passing() { // Destroy object and then pass it to a Godot engine API (upcast itself works, see other tests). obj.free(); - expect_panic("pass freed Gd to Godot engine API (T=user)", || { + expect_panic_or_ub("pass freed Gd to Godot engine API (T=user)", || { engine.register_singleton("NeverRegistered", &obj2); }); } @@ -344,7 +344,7 @@ fn object_user_call_after_free() { let mut copy = obj.clone(); obj.free(); - expect_panic("call() on dead user object", move || { + expect_panic_or_ub("call() on dead user object", move || { let _ = copy.call("get_instance_id", &[]); }); } @@ -355,7 +355,7 @@ fn object_engine_use_after_free() { let copy = node.clone(); node.free(); - expect_panic("call method on dead engine object", move || { + expect_panic_or_ub("call method on dead engine object", move || { copy.get_position(); }); } @@ -366,7 +366,7 @@ fn object_engine_use_after_free_varcall() { let mut copy = node.clone(); node.free(); - expect_panic("call method on dead engine object", move || { + expect_panic_or_ub("call method on dead engine object", move || { copy.call_deferred("get_position", &[]); }); } @@ -411,13 +411,13 @@ fn object_dead_eq() { { let lhs = a.clone(); - expect_panic("Gd::eq() panics when one operand is dead", move || { + expect_panic_or_ub("Gd::eq() panics when one operand is dead", move || { let _ = lhs == b; }); } { let rhs = a.clone(); - expect_panic("Gd::ne() panics when one operand is dead", move || { + expect_panic_or_ub("Gd::ne() panics when one operand is dead", move || { let _ = b2 != rhs; }); } @@ -826,7 +826,7 @@ fn object_engine_manual_double_free() { let node2 = node.clone(); node.free(); - expect_panic("double free()", move || { + expect_panic_or_ub("double free()", move || { node2.free(); }); } @@ -845,7 +845,7 @@ fn object_user_double_free() { let obj2 = obj.clone(); obj.call("free", &[]); - expect_panic("double free()", move || { + expect_panic_or_ub("double free()", move || { obj2.free(); }); } From a19eda55c0e9493324e5b3eea02ff6c7a6b3c9ec Mon Sep 17 00:00:00 2001 From: Jan Haller Date: Sun, 26 Oct 2025 18:39:45 +0100 Subject: [PATCH 38/54] Fix UB (check accidentally only done in strict, not balanced) --- godot-core/src/obj/base.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/godot-core/src/obj/base.rs b/godot-core/src/obj/base.rs index 0a359968d..05cd82e47 100644 --- a/godot-core/src/obj/base.rs +++ b/godot-core/src/obj/base.rs @@ -96,8 +96,11 @@ impl Base { /// `base` must be alive at the time of invocation, i.e. user `init()` (which could technically destroy it) must not have run yet. /// If `base` is destroyed while the returned `Base` is in use, that constitutes a logic error, not a safety issue. pub(crate) unsafe fn from_base(base: &Base) -> Base { - #[cfg(safeguards_strict)] - assert!(base.obj.is_instance_valid()); + #[cfg(safeguards_balanced)] + assert!( + base.obj.is_instance_valid(), + "Cannot construct Base; was object freed during initialization?" + ); let obj = Gd::from_obj_sys_weak(base.obj.obj_sys()); From 9c3e8e1275222f00422e6642c9ddf18a9c32b92b Mon Sep 17 00:00:00 2001 From: Jan Haller Date: Sun, 26 Oct 2025 16:07:32 +0100 Subject: [PATCH 39/54] Refactor and simplify object liveness/RTTI checks --- godot-codegen/src/generator/classes.rs | 25 ++++--- godot-core/src/classes/class_runtime.rs | 11 ++- godot-core/src/meta/signature.rs | 30 ++------ godot-core/src/obj/raw_gd.rs | 95 +++++++++++++++++++++---- godot-core/src/obj/rtti.rs | 12 +++- 5 files changed, 116 insertions(+), 57 deletions(-) diff --git a/godot-codegen/src/generator/classes.rs b/godot-codegen/src/generator/classes.rs index 8b6a031e0..6d9462de3 100644 --- a/godot-codegen/src/generator/classes.rs +++ b/godot-codegen/src/generator/classes.rs @@ -192,12 +192,14 @@ fn make_class(class: &Class, ctx: &mut Context, view: &ApiView) -> GeneratedClas let (assoc_memory, assoc_dyn_memory, is_exportable) = make_bounds(class, ctx); let internal_methods = quote! { - fn __checked_id(&self) -> Option { - // SAFETY: only Option due to layout-compatibility with RawGd; it is always Some because stored in Gd which is non-null. - let rtti = unsafe { self.rtti.as_ref().unwrap_unchecked() }; - #[cfg(safeguards_strict)] - rtti.check_type::(); - Some(rtti.instance_id()) + /// Creates a validated object for FFI boundary crossing. + /// + /// Low-level internal method. Validation (liveness/type checks) depend on safeguard level. + fn __validated_obj(&self) -> crate::obj::ValidatedObject { + // SAFETY: Self has the same layout as RawGd (object_ptr + rtti fields in same order). + let raw_gd = unsafe { std::mem::transmute::<&Self, &crate::obj::RawGd>(self) }; + + raw_gd.validated_object() } #[doc(hidden)] @@ -580,10 +582,10 @@ fn make_class_method_definition( let table_index = ctx.get_table_index(&MethodTableKey::from_class(class, method)); - let maybe_instance_id = if method.qualifier() == FnQualifier::Static { + let validated_obj = if method.qualifier() == FnQualifier::Static { quote! { None } } else { - quote! { self.__checked_id() } + quote! { Some(self.__validated_obj()) } }; let fptr_access = if cfg!(feature = "codegen-lazy-fptrs") { @@ -599,7 +601,6 @@ fn make_class_method_definition( quote! { fptr_by_index(#table_index) } }; - let object_ptr = &receiver.ffi_arg; let ptrcall_invocation = quote! { let method_bind = sys::#get_method_table().#fptr_access; @@ -607,8 +608,7 @@ fn make_class_method_definition( method_bind, #rust_class_name, #rust_method_name, - #object_ptr, - #maybe_instance_id, + #validated_obj, args, ) }; @@ -620,8 +620,7 @@ fn make_class_method_definition( method_bind, #rust_class_name, #rust_method_name, - #object_ptr, - #maybe_instance_id, + #validated_obj, args, varargs ) diff --git a/godot-core/src/classes/class_runtime.rs b/godot-core/src/classes/class_runtime.rs index 7e82063f3..ef7cd0df5 100644 --- a/godot-core/src/classes/class_runtime.rs +++ b/godot-core/src/classes/class_runtime.rs @@ -10,10 +10,13 @@ use crate::builtin::{GString, StringName, Variant, VariantType}; #[cfg(safeguards_strict)] use crate::classes::{ClassDb, Object}; +#[cfg(safeguards_balanced)] use crate::meta::CallContext; #[cfg(safeguards_strict)] use crate::meta::ClassId; -use crate::obj::{bounds, Bounds, Gd, GodotClass, InstanceId, RawGd, Singleton}; +#[cfg(safeguards_strict)] +use crate::obj::Singleton; +use crate::obj::{bounds, Bounds, Gd, GodotClass, InstanceId, RawGd}; use crate::sys; pub(crate) fn debug_string( @@ -191,6 +194,12 @@ where Gd::::from_obj_sys(object_ptr) } +/// Checks that the object with the given instance ID is still alive and that the pointer is valid. +/// +/// This does **not** perform type checking — use `ensure_object_type()` for that. +/// +/// # Panics (balanced+strict safeguards) +/// If the object has been freed or the instance ID points to a different object. #[cfg(safeguards_balanced)] pub(crate) fn ensure_object_alive( instance_id: InstanceId, diff --git a/godot-core/src/meta/signature.rs b/godot-core/src/meta/signature.rs index 470add4b8..4a9529deb 100644 --- a/godot-core/src/meta/signature.rs +++ b/godot-core/src/meta/signature.rs @@ -17,7 +17,7 @@ use crate::meta::{ FromGodot, GodotConvert, GodotType, InParamTuple, MethodParamOrReturnInfo, OutParamTuple, ParamTuple, ToGodot, }; -use crate::obj::{GodotClass, InstanceId}; +use crate::obj::{GodotClass, ValidatedObject}; pub(super) type CallResult = Result; @@ -62,7 +62,6 @@ where /// Receive a varcall from Godot, and return the value in `ret` as a variant pointer. /// /// # Safety - /// /// A call to this function must be caused by Godot making a varcall with parameters `Params` and return type `Ret`. #[inline] pub unsafe fn in_varcall( @@ -126,8 +125,6 @@ impl Signature { /// Make a varcall to the Godot engine for a class method. /// /// # Safety - /// - /// - `object_ptr` must be a live instance of a class with the type expected by `method_bind` /// - `method_bind` must expect explicit args `args`, varargs `varargs`, and return a value of type `Ret` #[inline] pub unsafe fn out_class_varcall( @@ -135,21 +132,13 @@ impl Signature { // Separate parameters to reduce tokens in generated class API. class_name: &'static str, method_name: &'static str, - object_ptr: sys::GDExtensionObjectPtr, - maybe_instance_id: Option, // if not static + validated_obj: Option, args: Params, varargs: &[Variant], ) -> CallResult { let call_ctx = CallContext::outbound(class_name, method_name); //$crate::out!("out_class_varcall: {call_ctx}"); - // Note: varcalls are not safe from failing, if they happen through an object pointer -> validity check necessary. - // paranoid since we already check the validity in check_rtti, this is unlikely to happen. - #[cfg(safeguards_strict)] - if let Some(instance_id) = maybe_instance_id { - crate::classes::ensure_object_alive(instance_id, object_ptr, &call_ctx); - } - let class_fn = sys::interface_fn!(object_method_bind_call); let variant = args.with_variants(|explicit_args| { @@ -162,7 +151,7 @@ impl Signature { let mut err = sys::default_call_error(); class_fn( method_bind.0, - object_ptr, + ValidatedObject::object_ptr(validated_obj.as_ref()), variant_ptrs.as_ptr(), variant_ptrs.len() as i64, return_ptr, @@ -290,8 +279,6 @@ impl Signature { /// Make a ptrcall to the Godot engine for a class method. /// /// # Safety - /// - /// - `object_ptr` must be a live instance of a class with the type expected by `method_bind` /// - `method_bind` must expect explicit args `args`, and return a value of type `Ret` #[inline] pub unsafe fn out_class_ptrcall( @@ -299,26 +286,19 @@ impl Signature { // Separate parameters to reduce tokens in generated class API. class_name: &'static str, method_name: &'static str, - object_ptr: sys::GDExtensionObjectPtr, - maybe_instance_id: Option, // if not static + validated_obj: Option, args: Params, ) -> Ret { let call_ctx = CallContext::outbound(class_name, method_name); // $crate::out!("out_class_ptrcall: {call_ctx}"); - // paranoid since we already check the validity in check_rtti, this is unlikely to happen. - #[cfg(safeguards_strict)] - if let Some(instance_id) = maybe_instance_id { - crate::classes::ensure_object_alive(instance_id, object_ptr, &call_ctx); - } - let class_fn = sys::interface_fn!(object_method_bind_ptrcall); unsafe { Self::raw_ptrcall(args, &call_ctx, |explicit_args, return_ptr| { class_fn( method_bind.0, - object_ptr, + ValidatedObject::object_ptr(validated_obj.as_ref()), explicit_args.as_ptr(), return_ptr, ); diff --git a/godot-core/src/obj/raw_gd.rs b/godot-core/src/obj/raw_gd.rs index e8d7c23d6..e260064f3 100644 --- a/godot-core/src/obj/raw_gd.rs +++ b/godot-core/src/obj/raw_gd.rs @@ -381,34 +381,61 @@ impl RawGd { } } - /// Verify that the object is non-null and alive. In paranoid mode, additionally verify that it is of type `T` or derived. + /// Validates object for use in `RawGd`/`Gd` methods (not FFI boundary calls). + /// + /// This is used for Rust-side object operations like `bind()`, `clone()`, `ffi_cast()`, etc. + /// For FFI boundary calls (generated engine methods), validation happens in signature.rs instead. + /// + /// # Panics + /// If validation fails. + #[cfg(safeguards_balanced)] pub(crate) fn check_rtti(&self, method_name: &'static str) { - #[cfg(safeguards_balanced)] - { - let call_ctx = CallContext::gd::(method_name); - #[cfg(safeguards_strict)] - self.check_dynamic_type(&call_ctx); - let instance_id = unsafe { self.instance_id_unchecked().unwrap_unchecked() }; + let call_ctx = CallContext::gd::(method_name); - classes::ensure_object_alive(instance_id, self.obj_sys(), &call_ctx); - } + // Type check (strict only). + #[cfg(safeguards_strict)] + self.check_dynamic_type(&call_ctx); + + // SAFETY: check_rtti() is only called for non-null pointers. + let instance_id = unsafe { self.instance_id_unchecked().unwrap_unchecked() }; + + // Liveness check (balanced + strict). + classes::ensure_object_alive(instance_id, self.obj_sys(), &call_ctx); } - /// Checks only type, not alive-ness. Used in Gd in case of `free()`. + #[cfg(not(safeguards_balanced))] + pub(crate) fn check_rtti(&self, _method_name: &'static str) { + // Disengaged level: no-op. + } + + /// Checks only type, not liveness. + /// + /// Used in specific scenarios like `Gd::free()` where we need type validation + /// but the object may already be dead. This is an internal helper for `check_rtti()`. #[cfg(safeguards_strict)] + #[inline] pub(crate) fn check_dynamic_type(&self, call_ctx: &CallContext<'static>) { - debug_assert!( + assert!( !self.is_null(), - "{call_ctx}: cannot call method on null object", + "internal bug: {call_ctx}: cannot call method on null object", ); let rtti = self.cached_rtti.as_ref(); - // SAFETY: code surrounding RawGd ensures that `self` is non-null; above is just a sanity check against internal bugs. + // SAFETY: RawGd non-null (checked above). let rtti = unsafe { rtti.unwrap_unchecked() }; rtti.check_type::(); } + /// Creates a validated object for FFI boundary crossing. + /// + /// This is a convenience wrapper around [`ValidatedObject::validate()`]. + #[doc(hidden)] + #[inline] + pub fn validated_object(&self) -> ValidatedObject { + ValidatedObject::validate(self) + } + // Not pub(super) because used by godot::meta::args::ObjectArg. pub(crate) fn obj_sys(&self) -> sys::GDExtensionObjectPtr { self.obj as sys::GDExtensionObjectPtr @@ -428,7 +455,7 @@ where { /// Hands out a guard for a shared borrow, through which the user instance can be read. /// - /// See [`crate::obj::Gd::bind()`] for a more in depth explanation. + /// See [`Gd::bind()`] for a more in depth explanation. // Note: possible names: write/read, hold/hold_mut, r/w, r/rw, ... pub(crate) fn bind(&self) -> GdRef<'_, T> { self.check_rtti("bind"); @@ -437,7 +464,7 @@ where /// Hands out a guard for an exclusive borrow, through which the user instance can be read and written. /// - /// See [`crate::obj::Gd::bind_mut()`] for a more in depth explanation. + /// See [`Gd::bind_mut()`] for a more in depth explanation. pub(crate) fn bind_mut(&mut self) -> GdMut<'_, T> { self.check_rtti("bind_mut"); GdMut::from_guard(self.storage().unwrap().get_mut()) @@ -758,6 +785,44 @@ impl fmt::Debug for RawGd { } } +// ---------------------------------------------------------------------------------------------------------------------------------------------- +// Type-state for already-validated objects before an engine API call + +/// Type-state object pointers that have been validated for engine API calls. +/// +/// Can be passed to [`Signature`](meta::signature::Signature) methods. Performs the following checks depending on safeguard level: +/// - `disengaged`: no validation. +/// - `balanced`: liveness check only. +/// - `strict`: liveness + type inheritance check. +#[doc(hidden)] +pub struct ValidatedObject { + object_ptr: sys::GDExtensionObjectPtr, +} + +impl ValidatedObject { + /// Validates a `RawGd` according to the type's invariants (depending on safeguard level). + /// + /// # Panics + /// If validation fails. + #[doc(hidden)] + #[inline] + pub fn validate(raw_gd: &RawGd) -> Self { + raw_gd.check_rtti("validated_object"); + + Self { + object_ptr: raw_gd.obj_sys(), + } + } + + /// Extracts the object pointer from an `Option`. + /// + /// Returns null pointer for `None` (static methods), or the validated pointer for `Some`. + #[inline] + pub fn object_ptr(opt: Option<&Self>) -> sys::GDExtensionObjectPtr { + opt.map(|v| v.object_ptr).unwrap_or(ptr::null_mut()) + } +} + // ---------------------------------------------------------------------------------------------------------------------------------------------- // Reusable functions, also shared with Gd, Variant, ObjectArg. diff --git a/godot-core/src/obj/rtti.rs b/godot-core/src/obj/rtti.rs index bf21a3c47..1a22b7663 100644 --- a/godot-core/src/obj/rtti.rs +++ b/godot-core/src/obj/rtti.rs @@ -41,10 +41,15 @@ impl ObjectRtti { } } - /// Checks that the object is of type `T` or derived. + /// Validates that the object's stored type matches or inherits from `T`. /// - /// # Panics - /// In paranoid mode, if the object is not of type `T` or derived. + /// Used internally by `RawGd::check_rtti()` for type validation in strict mode. + /// + /// Only checks the cached type from RTTI construction time. + /// This may not reflect runtime type changes (which shouldn't happen). + /// + /// # Panics (strict safeguards) + /// If the stored type does not inherit from `T`. #[cfg(safeguards_strict)] #[inline] pub fn check_type(&self) { @@ -53,6 +58,7 @@ impl ObjectRtti { #[inline] pub fn instance_id(&self) -> InstanceId { + // Do not add logic or validations here, this is passed in every FFI call. self.instance_id } } From a14de8f2aa0cfa2bf2ee77938e07cf4a87cea995 Mon Sep 17 00:00:00 2001 From: Jan Haller Date: Sun, 26 Oct 2025 18:47:05 +0100 Subject: [PATCH 40/54] Add new CI jobs with safeguard Cargo features --- .github/composite/godot-itest/action.yml | 25 ++++++++++++++++-- .github/workflows/full-ci.yml | 32 ++++++++++++++++++++---- .github/workflows/minimal-ci.yml | 26 +++++++++++++++++-- 3 files changed, 74 insertions(+), 9 deletions(-) diff --git a/.github/composite/godot-itest/action.yml b/.github/composite/godot-itest/action.yml index 7045fd458..4a383eb14 100644 --- a/.github/composite/godot-itest/action.yml +++ b/.github/composite/godot-itest/action.yml @@ -56,6 +56,11 @@ inputs: default: '' description: "If provided, acts as an argument for '--target', and results in output files written to ./target/{target}" + rust-target-dir: + required: false + default: '' + description: "If provided, determines where to find the generated dylib. Example: 'release' if Godot editor build would look for debug otherwise." + rust-cache-key: required: false default: '' @@ -151,6 +156,7 @@ runs: env: RUSTFLAGS: ${{ inputs.rust-env-rustflags }} TARGET: ${{ inputs.rust-target }} + OUTPUT_DIR: ${{ inputs.rust-target-dir || 'debug' }} run: | targetArgs="" if [[ -n "$TARGET" ]]; then @@ -162,8 +168,23 @@ runs: # Instead of modifying .gdextension, rename the output directory. if [[ -n "$TARGET" ]]; then - rm -rf target/debug - mv target/$TARGET/debug target + rm -rf target/debug target/release + echo "Build output (tree -L 2 target/$TARGET/$OUTPUT_DIR):" + tree -L 2 target/$TARGET/$OUTPUT_DIR || echo "(tree not installed)" + mv target/$TARGET/$OUTPUT_DIR target/ || { + echo "::error::Output dir $TARGET/$OUTPUT_DIR not found" + exit 1 + } + # echo "Intermediate dir (tree -L 2 target):" + # tree -L 2 target || echo "(tree not installed)" + if [[ "$OUTPUT_DIR" != "debug" ]]; then + mv target/$OUTPUT_DIR target/debug + fi + echo "Resulting dir (tree -L 2 target):" + tree -L 2 target || echo "(tree not installed)" + # echo "itest.gdextension contents:" + # cat itest/godot/itest.gdextension + # echo "----------------------------------------------------" fi shell: bash diff --git a/.github/workflows/full-ci.yml b/.github/workflows/full-ci.yml index a11adf0a6..17dbebb25 100644 --- a/.github/workflows/full-ci.yml +++ b/.github/workflows/full-ci.yml @@ -378,14 +378,14 @@ jobs: godot-prebuilt-patch: '4.2.2' hot-reload: api-4-2 - # Memory checks: special Godot binaries compiled with AddressSanitizer/LeakSanitizer to detect UB/leaks. + # Memory checks: special Godot binaries compiled with AddressSanitizer/LeakSanitizer to detect UB/leaks. Always Linux. # See also https://rustc-dev-guide.rust-lang.org/sanitizers.html. # # Additionally, the Godot source is patched to make dlclose() a no-op, as unloading dynamic libraries loses stacktrace and # cause false positives like println!. See https://github.com/google/sanitizers/issues/89. # # There is also a gcc variant besides clang, which is currently not used. - - name: linux-memcheck-nightly + - name: memcheck-nightly os: ubuntu-22.04 artifact-name: linux-memcheck-nightly godot-binary: godot.linuxbsd.editor.dev.x86_64.llvm.san @@ -395,7 +395,28 @@ jobs: # Sanitizers can't build proc-macros and build scripts; with --target, cargo ignores RUSTFLAGS for those two. rust-target: x86_64-unknown-linux-gnu - - name: linux-memcheck-4.2 + - name: memcheck-release-disengaged + os: ubuntu-22.04 + artifact-name: linux-memcheck-nightly + godot-binary: godot.linuxbsd.editor.dev.x86_64.llvm.san + rust-toolchain: nightly + rust-env-rustflags: -Zrandomize-layout -Zsanitizer=address + # Currently without itest/codegen-full-experimental. + rust-extra-args: --release --features godot/safeguards-release-disengaged + rust-target: x86_64-unknown-linux-gnu + rust-target-dir: release # We're running Godot debug build with Rust release dylib. + + - name: memcheck-dev-balanced + os: ubuntu-22.04 + artifact-name: linux-memcheck-nightly + godot-binary: godot.linuxbsd.editor.dev.x86_64.llvm.san + rust-toolchain: nightly + rust-env-rustflags: -Zrandomize-layout -Zsanitizer=address + # Currently without itest/codegen-full-experimental. + rust-extra-args: --features godot/safeguards-dev-balanced + rust-target: x86_64-unknown-linux-gnu + + - name: memcheck-4.2 os: ubuntu-22.04 artifact-name: linux-memcheck-4.2 godot-binary: godot.linuxbsd.editor.dev.x86_64.llvm.san @@ -419,6 +440,7 @@ jobs: rust-toolchain: ${{ matrix.rust-toolchain || 'stable' }} rust-env-rustflags: ${{ matrix.rust-env-rustflags }} rust-target: ${{ matrix.rust-target }} + rust-target-dir: ${{ matrix.rust-target-dir }} rust-cache-key: ${{ matrix.rust-cache-key }} with-llvm: ${{ contains(matrix.name, 'macos') && contains(matrix.rust-extra-args, 'api-custom') }} godot-check-header: ${{ matrix.godot-check-header }} @@ -500,7 +522,7 @@ jobs: - name: "Determine success or failure" run: | DEPENDENCIES='${{ toJson(needs) }}' - + echo "Dependency jobs:" all_success=true for job in $(echo "$DEPENDENCIES" | jq -r 'keys[]'); do @@ -510,7 +532,7 @@ jobs: all_success=false fi done - + if [[ "$all_success" == "false" ]]; then echo "One or more dependency jobs failed or were cancelled." exit 1 diff --git a/.github/workflows/minimal-ci.yml b/.github/workflows/minimal-ci.yml index 691f7669f..ae02c0bce 100644 --- a/.github/workflows/minimal-ci.yml +++ b/.github/workflows/minimal-ci.yml @@ -210,9 +210,9 @@ jobs: godot-binary: godot.linuxbsd.editor.dev.x86_64 godot-prebuilt-patch: '4.2.2' - # Memory checkers + # Memory checkers (always Linux). - - name: linux-memcheck + - name: memcheck os: ubuntu-22.04 artifact-name: linux-memcheck-nightly godot-binary: godot.linuxbsd.editor.dev.x86_64.llvm.san @@ -222,6 +222,27 @@ jobs: # Sanitizers can't build proc-macros and build scripts; with --target, cargo ignores RUSTFLAGS for those two. rust-target: x86_64-unknown-linux-gnu + - name: memcheck-release-disengaged + os: ubuntu-22.04 + artifact-name: linux-memcheck-nightly + godot-binary: godot.linuxbsd.editor.dev.x86_64.llvm.san + rust-toolchain: nightly + rust-env-rustflags: -Zrandomize-layout -Zsanitizer=address + # Currently without itest/codegen-full-experimental. + rust-extra-args: --release --features godot/safeguards-release-disengaged + rust-target: x86_64-unknown-linux-gnu + rust-target-dir: release # We're running Godot debug build with Rust release dylib. + + - name: memcheck-dev-balanced + os: ubuntu-22.04 + artifact-name: linux-memcheck-nightly + godot-binary: godot.linuxbsd.editor.dev.x86_64.llvm.san + rust-toolchain: nightly + rust-env-rustflags: -Zrandomize-layout -Zsanitizer=address + # Currently without itest/codegen-full-experimental. + rust-extra-args: --features godot/safeguards-dev-balanced + rust-target: x86_64-unknown-linux-gnu + steps: - uses: actions/checkout@v4 @@ -236,6 +257,7 @@ jobs: rust-toolchain: ${{ matrix.rust-toolchain || 'stable' }} rust-env-rustflags: ${{ matrix.rust-env-rustflags }} rust-target: ${{ matrix.rust-target }} + rust-target-dir: ${{ matrix.rust-target-dir }} with-llvm: ${{ contains(matrix.name, 'macos') && contains(matrix.rust-extra-args, 'api-custom') }} godot-check-header: ${{ matrix.godot-check-header }} godot-indirect-json: ${{ matrix.godot-indirect-json }} From 84c0c1114fa217d340c16f053a5cfb0092e2a02f Mon Sep 17 00:00:00 2001 From: Jan Haller Date: Sun, 26 Oct 2025 20:22:55 +0100 Subject: [PATCH 41/54] Fix codegen regression: `Array>` -> `Array` --- godot-codegen/src/conv/type_conversions.rs | 5 ++++- itest/rust/src/object_tests/object_test.rs | 7 +++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/godot-codegen/src/conv/type_conversions.rs b/godot-codegen/src/conv/type_conversions.rs index d63e7698e..d75a453d1 100644 --- a/godot-codegen/src/conv/type_conversions.rs +++ b/godot-codegen/src/conv/type_conversions.rs @@ -232,8 +232,11 @@ fn to_rust_type_uncached(full_ty: &GodotTy, ctx: &mut Context) -> RustTy { elem_type: quote! { Array<#rust_elem_ty> }, } } else { + // In Array, store Gd and not Option elements. + let without_option = rust_elem_ty.tokens_non_null(); + RustTy::EngineArray { - tokens: quote! { Array<#rust_elem_ty> }, + tokens: quote! { Array<#without_option> }, elem_class: elem_ty.to_string(), } }; diff --git a/itest/rust/src/object_tests/object_test.rs b/itest/rust/src/object_tests/object_test.rs index 6fd31ae30..5ef989541 100644 --- a/itest/rust/src/object_tests/object_test.rs +++ b/itest/rust/src/object_tests/object_test.rs @@ -11,12 +11,11 @@ use std::cell::{Cell, RefCell}; use std::rc::Rc; -use godot::builtin::{GString, StringName, Variant, Vector3}; +use godot::builtin::{Array, GString, StringName, Variant, Vector3}; use godot::classes::{ file_access, Engine, FileAccess, IRefCounted, Node, Node2D, Node3D, Object, RefCounted, }; use godot::global::godot_str; -#[allow(deprecated)] use godot::meta::{FromGodot, GodotType, ToGodot}; use godot::obj::{Base, Gd, Inherits, InstanceId, NewAlloc, NewGd, RawGd, Singleton}; use godot::register::{godot_api, GodotClass}; @@ -878,6 +877,10 @@ fn object_get_scene_tree(ctx: &TestContext) { let count = tree.get_child_count(); assert_eq!(count, 1); + + // Explicit type as regression test: https://github.com/godot-rust/gdext/pull/1385 + let nodes: Array> = tree.get_children(); + assert_eq!(nodes.len(), 1); } // implicitly tested: node does not leak #[itest] From 2100dbf81661a28eb5e45282fb62b320de140454 Mon Sep 17 00:00:00 2001 From: Jan Haller Date: Sun, 26 Oct 2025 22:00:36 +0100 Subject: [PATCH 42/54] Update crate version: 0.4.1 -> 0.4.2 --- Changelog.md | 23 ++++++++++++++++++++++- godot-bindings/Cargo.toml | 2 +- godot-cell/Cargo.toml | 2 +- godot-codegen/Cargo.toml | 6 +++--- godot-core/Cargo.toml | 10 +++++----- godot-ffi/Cargo.toml | 8 ++++---- godot-macros/Cargo.toml | 4 ++-- godot/Cargo.toml | 8 ++++---- 8 files changed, 42 insertions(+), 21 deletions(-) diff --git a/Changelog.md b/Changelog.md index f49add90b..f3f603fc3 100644 --- a/Changelog.md +++ b/Changelog.md @@ -10,12 +10,33 @@ Cutting-edge API docs of the `master` branch are available [here](https://godot- ## Quick navigation -- [v0.4.0](#v040), [v0.4.1](#v041) +- [v0.4.0](#v040), [v0.4.1](#v041), [v0.4.2](#v042) - [v0.3.0](#v030), [v0.3.1](#v031), [v0.3.2](#v032), [v0.3.3](#v033), [v0.3.4](#v034), [v0.3.5](#v035) - [v0.2.0](#v020), [v0.2.1](#v021), [v0.2.2](#v022), [v0.2.3](#v023), [v0.2.4](#v024) - [v0.1.1](#v011), [v0.1.2](#v012), [v0.1.3](#v013) +## [v0.4.2](https://docs.rs/godot/0.4.2) + +_26 October 2025_ + +### 🌻 Features + +- Simple API to fetch autoloads ([#1381](https://github.com/godot-rust/gdext/pull/1381)) +- Experimental support for required parameters/returns in Godot APIs ([#1383](https://github.com/godot-rust/gdext/pull/1383)) + +### 🧹 Quality of life + +- `ExtensionLibrary::on_main_loop_*`: merge into new `on_stage_init/deinit` API ([#1380](https://github.com/godot-rust/gdext/pull/1380)) +- Rename builtin `hash()` -> `hash_u32()`; add tests ([#1366](https://github.com/godot-rust/gdext/pull/1366)) + +### 🛠️ Bugfixes + +- Backport Godot fix for incorrect `Glyph` native-struct ([#1369](https://github.com/godot-rust/gdext/pull/1369)) +- Validate call params for `gd_self` virtual methods ([#1382](https://github.com/godot-rust/gdext/pull/1382)) +- Fix codegen regression: `Array>` -> `Array` ([#1385](https://github.com/godot-rust/gdext/pull/1385)) + + ## [v0.4.1](https://docs.rs/godot/0.4.1) _23 October 2025_ diff --git a/godot-bindings/Cargo.toml b/godot-bindings/Cargo.toml index 9305c9b2f..4ca57daef 100644 --- a/godot-bindings/Cargo.toml +++ b/godot-bindings/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "godot-bindings" -version = "0.4.1" +version = "0.4.2" edition = "2021" rust-version = "1.87" license = "MPL-2.0" diff --git a/godot-cell/Cargo.toml b/godot-cell/Cargo.toml index e2a5c7746..9a3d30d4a 100644 --- a/godot-cell/Cargo.toml +++ b/godot-cell/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "godot-cell" -version = "0.4.1" +version = "0.4.2" edition = "2021" rust-version = "1.87" license = "MPL-2.0" diff --git a/godot-codegen/Cargo.toml b/godot-codegen/Cargo.toml index b97445101..eafd1f320 100644 --- a/godot-codegen/Cargo.toml +++ b/godot-codegen/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "godot-codegen" -version = "0.4.1" +version = "0.4.2" edition = "2021" rust-version = "1.87" license = "MPL-2.0" @@ -23,7 +23,7 @@ experimental-threads = [] experimental-required-objs = [] [dependencies] -godot-bindings = { path = "../godot-bindings", version = "=0.4.1" } +godot-bindings = { path = "../godot-bindings", version = "=0.4.2" } heck = { workspace = true } nanoserde = { workspace = true } @@ -32,7 +32,7 @@ quote = { workspace = true } regex = { workspace = true } [build-dependencies] -godot-bindings = { path = "../godot-bindings", version = "=0.4.1" } # emit_godot_version_cfg +godot-bindings = { path = "../godot-bindings", version = "=0.4.2" } # emit_godot_version_cfg # https://docs.rs/about/metadata [package.metadata.docs.rs] diff --git a/godot-core/Cargo.toml b/godot-core/Cargo.toml index d11425f30..709838a82 100644 --- a/godot-core/Cargo.toml +++ b/godot-core/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "godot-core" -version = "0.4.1" +version = "0.4.2" edition = "2021" rust-version = "1.87" license = "MPL-2.0" @@ -40,16 +40,16 @@ api-4-5 = ["godot-ffi/api-4-5"] # ]] [dependencies] -godot-ffi = { path = "../godot-ffi", version = "=0.4.1" } +godot-ffi = { path = "../godot-ffi", version = "=0.4.2" } # See https://docs.rs/glam/latest/glam/index.html#feature-gates glam = { workspace = true } serde = { workspace = true, optional = true } -godot-cell = { path = "../godot-cell", version = "=0.4.1" } +godot-cell = { path = "../godot-cell", version = "=0.4.2" } [build-dependencies] -godot-bindings = { path = "../godot-bindings", version = "=0.4.1" } -godot-codegen = { path = "../godot-codegen", version = "=0.4.1" } +godot-bindings = { path = "../godot-bindings", version = "=0.4.2" } +godot-codegen = { path = "../godot-codegen", version = "=0.4.2" } # Reverse dev dependencies so doctests can use `godot::` prefix. [dev-dependencies] diff --git a/godot-ffi/Cargo.toml b/godot-ffi/Cargo.toml index f99655d0a..16a016c23 100644 --- a/godot-ffi/Cargo.toml +++ b/godot-ffi/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "godot-ffi" -version = "0.4.1" +version = "0.4.2" edition = "2021" rust-version = "1.87" license = "MPL-2.0" @@ -37,11 +37,11 @@ libc = { workspace = true } [target.'cfg(target_family = "wasm")'.dependencies] # Only needed for WASM identifier generation. -godot-macros = { path = "../godot-macros", version = "=0.4.1", features = ["experimental-wasm"] } +godot-macros = { path = "../godot-macros", version = "=0.4.2", features = ["experimental-wasm"] } [build-dependencies] -godot-bindings = { path = "../godot-bindings", version = "=0.4.1" } -godot-codegen = { path = "../godot-codegen", version = "=0.4.1" } +godot-bindings = { path = "../godot-bindings", version = "=0.4.2" } +godot-codegen = { path = "../godot-codegen", version = "=0.4.2" } # https://docs.rs/about/metadata [package.metadata.docs.rs] diff --git a/godot-macros/Cargo.toml b/godot-macros/Cargo.toml index fb6f3a80f..43b600966 100644 --- a/godot-macros/Cargo.toml +++ b/godot-macros/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "godot-macros" -version = "0.4.1" +version = "0.4.2" edition = "2021" rust-version = "1.87" license = "MPL-2.0" @@ -29,7 +29,7 @@ litrs = { workspace = true, optional = true } venial = { workspace = true } [build-dependencies] -godot-bindings = { path = "../godot-bindings", version = "=0.4.1" } # emit_godot_version_cfg +godot-bindings = { path = "../godot-bindings", version = "=0.4.2" } # emit_godot_version_cfg # Reverse dev dependencies so doctests can use `godot::` prefix. [dev-dependencies] diff --git a/godot/Cargo.toml b/godot/Cargo.toml index 35b53e0f2..1e5af5a95 100644 --- a/godot/Cargo.toml +++ b/godot/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "godot" -version = "0.4.1" +version = "0.4.2" edition = "2021" rust-version = "1.87" license = "MPL-2.0" @@ -10,7 +10,7 @@ description = "Rust bindings for Godot 4" authors = ["Bromeon", "godot-rust contributors"] repository = "https://github.com/godot-rust/gdext" homepage = "https://godot-rust.github.io" -documentation = "https://docs.rs/godot/0.4.1" +documentation = "https://docs.rs/godot/0.4.2" readme = "crate-readme.md" [features] @@ -48,8 +48,8 @@ __debug-log = ["godot-core/debug-log"] __trace = ["godot-core/trace"] [dependencies] -godot-core = { path = "../godot-core", version = "=0.4.1" } -godot-macros = { path = "../godot-macros", version = "=0.4.1" } +godot-core = { path = "../godot-core", version = "=0.4.2" } +godot-macros = { path = "../godot-macros", version = "=0.4.2" } # https://docs.rs/about/metadata [package.metadata.docs.rs] From 709c1e319757cdad6dc114e704f9b671c2dc687b Mon Sep 17 00:00:00 2001 From: Jan Haller Date: Tue, 28 Oct 2025 08:10:36 +0100 Subject: [PATCH 43/54] Verify that panicking ptrcalls return Godot default value --- itest/godot/input/GenFfiTests.template.gd | 15 +++++++++++++++ itest/rust/build.rs | 19 +++++++++++++------ 2 files changed, 28 insertions(+), 6 deletions(-) diff --git a/itest/godot/input/GenFfiTests.template.gd b/itest/godot/input/GenFfiTests.template.gd index c01966ecd..a732a3e97 100644 --- a/itest/godot/input/GenFfiTests.template.gd +++ b/itest/godot/input/GenFfiTests.template.gd @@ -67,6 +67,21 @@ func test_ptrcall_IDENT(): mark_test_succeeded() #) +# Functions that are invoked via ptrcall do not have an API to propagate the error back to the caller, but Godot pre-initializes their +# return value to the default value of that type. This test verifies that in case of panic, the default value (per Godot) is returned. +#( +func test_ptrcall_panic_IDENT(): + mark_test_pending() + var ffi := GenFfi.new() + + var from_rust: TYPE = ffi.panic_IDENT() + _check_callconv("panic_IDENT", "ptrcall") + + var expected_default: TYPE # initialize to default (null for objects) + assert_eq(from_rust, expected_default, "return value from panicked ptrcall fn == default value") + mark_test_succeeded() +#) + #( func test_ptrcall_static_IDENT(): mark_test_pending() diff --git a/itest/rust/build.rs b/itest/rust/build.rs index 6ca87b303..d664baaaa 100644 --- a/itest/rust/build.rs +++ b/itest/rust/build.rs @@ -320,12 +320,14 @@ fn generate_rust_methods(inputs: &[Input]) -> Vec { .. } = input; - let return_method = format_ident!("return_{}", ident); - let accept_method = format_ident!("accept_{}", ident); - let mirror_method = format_ident!("mirror_{}", ident); - let return_static_method = format_ident!("return_static_{}", ident); - let accept_static_method = format_ident!("accept_static_{}", ident); - let mirror_static_method = format_ident!("mirror_static_{}", ident); + let return_method = format_ident!("return_{ident}"); + let accept_method = format_ident!("accept_{ident}"); + let mirror_method = format_ident!("mirror_{ident}"); + let panic_method = format_ident!("panic_{ident}"); + + let return_static_method = format_ident!("return_static_{ident}"); + let accept_static_method = format_ident!("accept_static_{ident}"); + let mirror_static_method = format_ident!("mirror_static_{ident}"); quote! { #[func] @@ -343,6 +345,11 @@ fn generate_rust_methods(inputs: &[Input]) -> Vec { i } + #[func] + fn #panic_method(&self) -> #rust_ty { + panic!("intentional panic in `{}`", stringify!(#panic_method)); + } + #[func] fn #return_static_method() -> #rust_ty { #rust_val From 98477e94b59e28df35c86a97ddee5ed23b9eaf37 Mon Sep 17 00:00:00 2001 From: Jan Haller Date: Tue, 28 Oct 2025 12:21:23 +0100 Subject: [PATCH 44/54] Change ptrcalls to use Result instead of panics --- godot-core/src/builtin/callable.rs | 4 +- godot-core/src/meta/error/call_error.rs | 3 + godot-core/src/meta/param_tuple.rs | 5 +- godot-core/src/meta/param_tuple/impls.rs | 29 ++++---- godot-core/src/meta/signature.rs | 15 +++-- godot-core/src/private.rs | 78 +++++++++++----------- godot-macros/src/class/data_models/func.rs | 12 ++-- 7 files changed, 72 insertions(+), 74 deletions(-) diff --git a/godot-core/src/builtin/callable.rs b/godot-core/src/builtin/callable.rs index 2071ceb1a..9d174feff 100644 --- a/godot-core/src/builtin/callable.rs +++ b/godot-core/src/builtin/callable.rs @@ -677,7 +677,7 @@ mod custom_callable { let name = ""; let ctx = meta::CallContext::custom_callable(name); - crate::private::handle_varcall_panic(&ctx, &mut *r_error, move || { + crate::private::handle_fallible_varcall(&ctx, &mut *r_error, move || { // Get the RustCallable again inside closure so it doesn't have to be UnwindSafe. let c: &mut C = CallableUserdata::inner_from_raw(callable_userdata); let result = c.invoke(arg_refs); @@ -707,7 +707,7 @@ mod custom_callable { let name = ""; let ctx = meta::CallContext::custom_callable(name); - crate::private::handle_varcall_panic(&ctx, &mut *r_error, move || { + crate::private::handle_fallible_varcall(&ctx, &mut *r_error, move || { // Get the FnWrapper again inside closure so the FnMut doesn't have to be UnwindSafe. let w: &mut FnWrapper = CallableUserdata::inner_from_raw(callable_userdata); diff --git a/godot-core/src/meta/error/call_error.rs b/godot-core/src/meta/error/call_error.rs index 75adc654a..c8d8c36cc 100644 --- a/godot-core/src/meta/error/call_error.rs +++ b/godot-core/src/meta/error/call_error.rs @@ -16,6 +16,9 @@ use crate::meta::{CallContext, ToGodot}; use crate::private::PanicPayload; use crate::sys; +/// Result type for function calls that can fail. +pub(crate) type CallResult = Result; + /// Error capable of representing failed function calls. /// /// This type is returned from _varcall_ functions in the Godot API that begin with `try_` prefixes, diff --git a/godot-core/src/meta/param_tuple.rs b/godot-core/src/meta/param_tuple.rs index de1ab88b7..d2901aec7 100644 --- a/godot-core/src/meta/param_tuple.rs +++ b/godot-core/src/meta/param_tuple.rs @@ -7,8 +7,9 @@ use godot_ffi as sys; -use super::{CallContext, CallResult, PropertyInfo}; use crate::builtin::Variant; +use crate::meta::error::CallResult; +use crate::meta::{CallContext, PropertyInfo}; mod impls; @@ -64,7 +65,7 @@ pub trait InParamTuple: ParamTuple { args_ptr: *const sys::GDExtensionConstTypePtr, call_type: sys::PtrcallType, call_ctx: &CallContext, - ) -> Self; + ) -> CallResult; /// Converts `array` to `Self` by calling [`from_variant`](crate::meta::FromGodot::from_variant) on each argument. fn from_variant_array(array: &[&Variant]) -> Self; diff --git a/godot-core/src/meta/param_tuple/impls.rs b/godot-core/src/meta/param_tuple/impls.rs index 4c55ab35d..7d8e6605d 100644 --- a/godot-core/src/meta/param_tuple/impls.rs +++ b/godot-core/src/meta/param_tuple/impls.rs @@ -14,10 +14,10 @@ use godot_ffi as sys; use sys::GodotFfi; use crate::builtin::Variant; -use crate::meta::error::{CallError, ConvertError}; +use crate::meta::error::{CallError, CallResult}; use crate::meta::{ - signature, ArgPassing, CallContext, FromGodot, GodotConvert, GodotType, InParamTuple, - OutParamTuple, ParamTuple, ToGodot, + ArgPassing, CallContext, FromGodot, GodotConvert, GodotType, InParamTuple, OutParamTuple, + ParamTuple, ToGodot, }; macro_rules! count_idents { @@ -57,7 +57,7 @@ macro_rules! unsafe_impl_param_tuple { unsafe fn from_varcall_args( args_ptr: *const sys::GDExtensionConstVariantPtr, call_ctx: &crate::meta::CallContext, - ) -> signature::CallResult { + ) -> CallResult { let args = ( $( // SAFETY: `args_ptr` is an array with length `Self::LEN` and each element is a valid pointer, since they @@ -80,14 +80,16 @@ macro_rules! unsafe_impl_param_tuple { args_ptr: *const sys::GDExtensionConstTypePtr, call_type: sys::PtrcallType, call_ctx: &crate::meta::CallContext, - ) -> Self { - ( + ) -> CallResult { + let tuple = ( $( // SAFETY: `args_ptr` has length `Self::LEN` and `$n` is less than `Self::LEN`, and `args_ptr` must be an array whose // `$n`-th element is of type `$P`. - unsafe { ptrcall_arg::<$P, $n>(args_ptr, call_ctx, call_type) }, + unsafe { ptrcall_arg::<$P, $n>(args_ptr, call_ctx, call_type)? }, )* - ) + ); + + Ok(tuple) // If none of the `?` above were hit. } fn from_variant_array(array: &[&Variant]) -> Self { @@ -196,7 +198,7 @@ pub(super) unsafe fn ptrcall_arg( args_ptr: *const sys::GDExtensionConstTypePtr, call_ctx: &CallContext, call_type: sys::PtrcallType, -) -> P { +) -> CallResult

{ // SAFETY: It is safe to dereference `args_ptr` at `N`. let offset_ptr = unsafe { *args_ptr.offset(N) }; @@ -207,7 +209,7 @@ pub(super) unsafe fn ptrcall_arg( ::try_from_ffi(ffi) .and_then(P::try_from_godot) - .unwrap_or_else(|err| param_error::

(call_ctx, N as i32, err)) + .map_err(|err| CallError::failed_param_conversion::

(call_ctx, N, err)) } /// Converts `arg` into a value of type `P`. @@ -219,7 +221,7 @@ pub(super) unsafe fn varcall_arg( arg: sys::GDExtensionConstVariantPtr, call_ctx: &CallContext, param_index: isize, -) -> Result { +) -> CallResult

{ // SAFETY: It is safe to dereference `args_ptr` at `N` as a `Variant`. let variant_ref = unsafe { Variant::borrow_var_sys(arg) }; @@ -228,11 +230,6 @@ pub(super) unsafe fn varcall_arg( .map_err(|err| CallError::failed_param_conversion::

(call_ctx, param_index, err)) } -fn param_error

(call_ctx: &CallContext, index: i32, err: ConvertError) -> ! { - let param_ty = std::any::type_name::

(); - panic!("in function `{call_ctx}` at parameter [{index}] of type {param_ty}: {err}"); -} - fn assert_array_length(array: &[&Variant]) { assert_eq!( array.len(), diff --git a/godot-core/src/meta/signature.rs b/godot-core/src/meta/signature.rs index 4a9529deb..00844abf9 100644 --- a/godot-core/src/meta/signature.rs +++ b/godot-core/src/meta/signature.rs @@ -9,18 +9,17 @@ use std::borrow::Cow; use std::fmt; use std::marker::PhantomData; -use godot_ffi::{self as sys, GodotFfi}; +use godot_ffi as sys; +use sys::GodotFfi; use crate::builtin::Variant; -use crate::meta::error::{CallError, ConvertError}; +use crate::meta::error::{CallError, CallResult, ConvertError}; use crate::meta::{ FromGodot, GodotConvert, GodotType, InParamTuple, MethodParamOrReturnInfo, OutParamTuple, ParamTuple, ToGodot, }; use crate::obj::{GodotClass, ValidatedObject}; -pub(super) type CallResult = Result; - /// A full signature for a function. /// /// For in-calls (that is, calls from the Godot engine to Rust code) `Params` will implement [`InParamTuple`] and `Ret` @@ -101,19 +100,21 @@ where ret: sys::GDExtensionTypePtr, func: fn(sys::GDExtensionClassInstancePtr, Params) -> Ret, call_type: sys::PtrcallType, - ) { + ) -> CallResult<()> { // $crate::out!("in_ptrcall: {call_ctx}"); #[cfg(feature = "trace")] trace::push(true, true, call_ctx); // SAFETY: TODO. - let args = unsafe { Params::from_ptrcall_args(args_ptr, call_type, call_ctx) }; + let args = unsafe { Params::from_ptrcall_args(args_ptr, call_type, call_ctx)? }; // SAFETY: // `ret` is always a pointer to an initialized value of type $R // TODO: double-check the above - unsafe { ptrcall_return::(func(instance_ptr, args), ret, call_ctx, call_type) } + unsafe { ptrcall_return::(func(instance_ptr, args), ret, call_ctx, call_type) }; + + Ok(()) } } diff --git a/godot-core/src/private.rs b/godot-core/src/private.rs index 6e3148a96..d92a2d7c3 100644 --- a/godot-core/src/private.rs +++ b/godot-core/src/private.rs @@ -13,7 +13,7 @@ use std::sync::atomic; use sys::Global; use crate::global::godot_error; -use crate::meta::error::CallError; +use crate::meta::error::{CallError, CallResult}; use crate::meta::CallContext; use crate::obj::Gd; use crate::{classes, sys}; @@ -417,7 +417,7 @@ impl PanicPayload { /// /// Returns `Err(message)` if a panic occurred, and `Ok(result)` with the result of `code` otherwise. /// -/// In contrast to [`handle_varcall_panic`] and [`handle_ptrcall_panic`], this function is not intended for use in `try_` functions, +/// In contrast to [`handle_fallible_varcall`] and [`handle_fallible_ptrcall`], this function is not intended for use in `try_` functions, /// where the error is propagated as a `CallError` in a global variable. pub fn handle_panic(error_context: E, code: F) -> Result where @@ -440,59 +440,57 @@ where result } -// TODO(bromeon): make call_ctx lazy-evaluated (like error_ctx) everywhere; -// or make it eager everywhere and ensure it's cheaply constructed in the call sites. -pub fn handle_varcall_panic( +/// Invokes a function with the _varcall_ calling convention, handling both expected errors and user panics. +pub fn handle_fallible_varcall( call_ctx: &CallContext, out_err: &mut sys::GDExtensionCallError, code: F, ) where - F: FnOnce() -> Result + std::panic::UnwindSafe, + F: FnOnce() -> CallResult + std::panic::UnwindSafe, { - let outcome: Result, PanicPayload> = - handle_panic(|| call_ctx.to_string(), code); - - let call_error = match outcome { - // All good. - Ok(Ok(_result)) => return, - - // Call error signalled by Godot's or gdext's validation. - Ok(Err(err)) => err, - - // Panic occurred (typically through user): forward message. - Err(panic_msg) => CallError::failed_by_user_panic(call_ctx, panic_msg), - }; - - let error_id = report_call_error(call_error, true); - - // Abuse 'argument' field to store our ID. - *out_err = sys::GDExtensionCallError { - error: sys::GODOT_RUST_CUSTOM_CALL_ERROR, - argument: error_id, - expected: 0, + if let Some(error_id) = handle_fallible_call(call_ctx, code, true) { + // Abuse 'argument' field to store our ID. + *out_err = sys::GDExtensionCallError { + error: sys::GODOT_RUST_CUSTOM_CALL_ERROR, + argument: error_id, + expected: 0, + }; }; //sys::interface_fn!(variant_new_nil)(sys::AsUninit::as_uninit(ret)); } -pub fn handle_ptrcall_panic(call_ctx: &CallContext, code: F) +/// Invokes a function with the _ptrcall_ calling convention, handling both expected errors and user panics. +pub fn handle_fallible_ptrcall(call_ctx: &CallContext, code: F) where - F: FnOnce() -> R + std::panic::UnwindSafe, + F: FnOnce() -> CallResult<()> + std::panic::UnwindSafe, { - let outcome: Result = handle_panic(|| call_ctx.to_string(), code); + handle_fallible_call(call_ctx, code, false); +} + +/// Common error handling for fallible calls, handling detectable errors and user panics. +/// +/// Returns `None` if the call succeeded, or `Some(error_id)` if it failed. +/// +/// `track_globally` indicates whether the error should be stored as an index in the global error database (for varcall calls), to convey +/// out-of-band, godot-rust specific error information to the caller. +fn handle_fallible_call(call_ctx: &CallContext, code: F, track_globally: bool) -> Option +where + F: FnOnce() -> CallResult + std::panic::UnwindSafe, +{ + let outcome: Result, PanicPayload> = handle_panic(|| call_ctx.to_string(), code); let call_error = match outcome { // All good. - Ok(_result) => return, + Ok(Ok(_result)) => return None, - // Panic occurred (typically through user): forward message. - Err(payload) => CallError::failed_by_user_panic(call_ctx, payload), - }; + // Error from Godot or godot-rust validation (e.g. parameter conversion). + Ok(Err(err)) => err, - let _id = report_call_error(call_error, false); -} + // User panic occurred: forward message. + Err(panic_msg) => CallError::failed_by_user_panic(call_ctx, panic_msg), + }; -fn report_call_error(call_error: CallError, track_globally: bool) -> i32 { // Print failed calls to Godot's console. // TODO Level 1 is not yet set, so this will always print if level != 0. Needs better logic to recognize try_* calls and avoid printing. // But a bit tricky with multiple threads and re-entrancy; maybe pass in info in error struct. @@ -501,11 +499,13 @@ fn report_call_error(call_error: CallError, track_globally: bool) -> i32 { } // Once there is a way to auto-remove added errors, this could be always true. - if track_globally { + let error_id = if track_globally { call_error_insert(call_error) } else { 0 - } + }; + + Some(error_id) } // Currently unused; implemented due to temporary need and may come in handy. diff --git a/godot-macros/src/class/data_models/func.rs b/godot-macros/src/class/data_models/func.rs index 744707403..9d8601fee 100644 --- a/godot-macros/src/class/data_models/func.rs +++ b/godot-macros/src/class/data_models/func.rs @@ -86,7 +86,7 @@ pub fn make_virtual_callback( ret: sys::GDExtensionTypePtr, ) { let call_ctx = #call_ctx; - let _success = ::godot::private::handle_ptrcall_panic( + ::godot::private::handle_fallible_ptrcall( &call_ctx, || #invocation ); @@ -566,7 +566,7 @@ fn make_varcall_fn(call_ctx: &TokenStream, wrapped_method: &TokenStream) -> Toke err: *mut sys::GDExtensionCallError, ) { let call_ctx = #call_ctx; - ::godot::private::handle_varcall_panic( + ::godot::private::handle_fallible_varcall( &call_ctx, &mut *err, || #invocation @@ -587,14 +587,10 @@ fn make_ptrcall_fn(call_ctx: &TokenStream, wrapped_method: &TokenStream) -> Toke ret: sys::GDExtensionTypePtr, ) { let call_ctx = #call_ctx; - let _success = ::godot::private::handle_panic( - || format!("{call_ctx}"), + ::godot::private::handle_fallible_ptrcall( + &call_ctx, || #invocation ); - - // if success.is_err() { - // // TODO set return value to T::default()? - // } } } } From 230547ef9febd5b1bf0dcd8b97d3dbd01257ad1c Mon Sep 17 00:00:00 2001 From: Lili Zoey Zyli Date: Tue, 28 Oct 2025 16:31:04 +0100 Subject: [PATCH 45/54] Add `rename` to `#[var]` --- .../src/class/data_models/field_var.rs | 20 +++++++-- .../src/class/data_models/property.rs | 10 ++--- godot-macros/src/lib.rs | 18 +++++++- itest/rust/src/object_tests/property_test.rs | 41 +++++++++++++++++++ 4 files changed, 80 insertions(+), 9 deletions(-) diff --git a/godot-macros/src/class/data_models/field_var.rs b/godot-macros/src/class/data_models/field_var.rs index 173017a2e..9ec2271bd 100644 --- a/godot-macros/src/class/data_models/field_var.rs +++ b/godot-macros/src/class/data_models/field_var.rs @@ -18,6 +18,8 @@ use crate::{util, ParseResult}; /// Store info from `#[var]` attribute. #[derive(Clone, Debug)] pub struct FieldVar { + /// What name this variable should have in Godot, if `None` then the Rust name should be used. + pub rename: Option, pub getter: GetterSetter, pub setter: GetterSetter, pub hint: FieldHint, @@ -29,6 +31,7 @@ impl FieldVar { /// Parse a `#[var]` attribute to a `FieldVar` struct. /// /// Possible keys: + /// - `rename = ident` /// - `get = expr` /// - `set = expr` /// - `hint = ident` @@ -36,6 +39,7 @@ impl FieldVar { /// - `usage_flags = pub(crate) fn new_from_kv(parser: &mut KvParser) -> ParseResult { let span = parser.span(); + let rename = parser.handle_ident("rename")?; let getter = GetterSetter::parse(parser, "get")?; let setter = GetterSetter::parse(parser, "set")?; @@ -64,6 +68,7 @@ impl FieldVar { }; Ok(FieldVar { + rename, getter, setter, hint, @@ -84,6 +89,7 @@ impl FieldVar { impl Default for FieldVar { fn default() -> Self { Self { + rename: Default::default(), getter: Default::default(), setter: Default::default(), hint: Default::default(), @@ -131,11 +137,12 @@ impl GetterSetter { class_name: &Ident, kind: GetSet, field: &Field, + rename: &Option, ) -> Option { match self { GetterSetter::Omitted => None, GetterSetter::Generated => Some(GetterSetterImpl::from_generated_impl( - class_name, kind, field, + class_name, kind, field, rename, )), GetterSetter::Custom(function_name) => { Some(GetterSetterImpl::from_custom_impl(function_name)) @@ -173,14 +180,21 @@ pub struct GetterSetterImpl { } impl GetterSetterImpl { - fn from_generated_impl(class_name: &Ident, kind: GetSet, field: &Field) -> Self { + fn from_generated_impl( + class_name: &Ident, + kind: GetSet, + field: &Field, + rename: &Option, + ) -> Self { let Field { name: field_name, ty: field_type, .. } = field; - let function_name = format_ident!("{}{field_name}", kind.prefix()); + let var_name = rename.as_ref().unwrap_or(field_name); + + let function_name = format_ident!("{}{var_name}", kind.prefix()); let signature; let function_body; diff --git a/godot-macros/src/class/data_models/property.rs b/godot-macros/src/class/data_models/property.rs index 75671575e..b98bed71c 100644 --- a/godot-macros/src/class/data_models/property.rs +++ b/godot-macros/src/class/data_models/property.rs @@ -76,10 +76,8 @@ pub fn make_property_impl(class_name: &Ident, fields: &Fields) -> TokenStream { }; make_groups_registrations(group, subgroup, &mut export_tokens, class_name); - - let field_name = field_ident.to_string(); - let FieldVar { + rename, getter, setter, hint, @@ -87,6 +85,8 @@ pub fn make_property_impl(class_name: &Ident, fields: &Fields) -> TokenStream { .. } = var; + let field_name = rename.as_ref().unwrap_or(field_ident).to_string(); + let export_hint; let registration_fn; @@ -151,14 +151,14 @@ pub fn make_property_impl(class_name: &Ident, fields: &Fields) -> TokenStream { // Note: {getter,setter}_tokens can be either a path `Class_Functions::constant_name` or an empty string `""`. let getter_tokens = make_getter_setter( - getter.to_impl(class_name, GetSet::Get, field), + getter.to_impl(class_name, GetSet::Get, field, &rename), &mut getter_setter_impls, &mut func_name_consts, &mut export_tokens, class_name, ); let setter_tokens = make_getter_setter( - setter.to_impl(class_name, GetSet::Set, field), + setter.to_impl(class_name, GetSet::Set, field, &rename), &mut getter_setter_impls, &mut func_name_consts, &mut export_tokens, diff --git a/godot-macros/src/lib.rs b/godot-macros/src/lib.rs index 0f35ff4d5..3970094f5 100644 --- a/godot-macros/src/lib.rs +++ b/godot-macros/src/lib.rs @@ -206,7 +206,23 @@ use crate::util::{bail, ident, KvParser}; /// } /// ``` /// -/// To create a property without a backing field to store data, you can use [`PhantomVar`](../obj/struct.PhantomVar.html). +/// If you want the field to have a different name in Godot and Rust, you can use `rename`: +/// +/// ``` +/// # use godot::prelude::*; +/// #[derive(GodotClass)] +/// # #[class(init)] +/// struct MyStruct { +/// #[var(rename = my_godot_field)] +/// my_rust_field: i64, +/// } +/// ``` +/// +/// With this, you can access this field as `my_rust_field` in Rust code, however when accessing it from Godot you have to +/// use `my_godot_field` instead (including when using methods such as [`Object::get`](../classes/struct.Object.html#method.get)). +/// The generated getters and setters will also be named `get/set_my_godot_field`, instead of `get/set_my_rust_field`. +/// +/// To create a property without a backing field to store data, you can use [`PhantomVar`](../prelude/struct.PhantomVar.html). /// This disables autogenerated getters and setters for that field. /// /// ## Export properties -- `#[export]` diff --git a/itest/rust/src/object_tests/property_test.rs b/itest/rust/src/object_tests/property_test.rs index 1ef54c87a..e13b18848 100644 --- a/itest/rust/src/object_tests/property_test.rs +++ b/itest/rust/src/object_tests/property_test.rs @@ -53,6 +53,9 @@ struct HasProperty { #[var] packed_int_array: PackedInt32Array, + + #[var(rename = renamed_variable)] + unused_name: GString, } #[godot_api] @@ -147,10 +150,48 @@ impl INode for HasProperty { texture_val: OnEditor::default(), texture_val_rw: None, packed_int_array: PackedInt32Array::new(), + unused_name: GString::new(), } } } +#[itest] +fn test_renamed_var() { + let mut obj = HasProperty::new_alloc(); + + let prop_list = obj.get_property_list(); + assert!(prop_list + .iter_shared() + .any(|d| d.get("name") == Some("renamed_variable".to_variant()))); + assert!(!prop_list + .iter_shared() + .any(|d| d.get("name") == Some("unused_name".to_variant()))); + + assert_eq!(obj.get("renamed_variable"), GString::new().to_variant()); + assert_eq!(obj.get("unused_name"), Variant::nil()); + + let new_value = "variable changed".to_variant(); + obj.set("renamed_variable", &new_value); + obj.set("unused_name", &"something different".to_variant()); + assert_eq!(obj.get("renamed_variable"), new_value); + assert_eq!(obj.get("unused_name"), Variant::nil()); + + obj.free(); +} + +#[itest] +fn test_renamed_var_getter_setter() { + let obj = HasProperty::new_alloc(); + + assert!(obj.has_method("get_renamed_variable")); + assert!(obj.has_method("set_renamed_variable")); + assert!(!obj.has_method("get_unused_name")); + assert!(!obj.has_method("get_unused_name")); + assert_eq!(obj.bind().get_renamed_variable(), GString::new()); + + obj.free(); +} + #[derive(Default, Copy, Clone)] #[repr(i64)] enum SomeCStyleEnum { From 41c0aadf583f93609a752b376dc27d8782ce50de Mon Sep 17 00:00:00 2001 From: Jan Haller Date: Sat, 1 Nov 2025 10:42:23 +0100 Subject: [PATCH 46/54] Improve diagnostics in XML register-docs test --- .gitignore | 3 ++ itest/rust/src/framework/mod.rs | 7 ++++ .../src/register_tests/register_docs_test.rs | 35 +++++++++++++++++-- 3 files changed, 43 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index bb87efffe..876de4335 100644 --- a/.gitignore +++ b/.gitignore @@ -29,3 +29,6 @@ exp.rs # Windows specific desktop.ini + +# Test outputs +/itest/rust/src/register_tests/res/actual_registered_docs.xml diff --git a/itest/rust/src/framework/mod.rs b/itest/rust/src/framework/mod.rs index 4fb7a4b3a..0cfb4c82b 100644 --- a/itest/rust/src/framework/mod.rs +++ b/itest/rust/src/framework/mod.rs @@ -321,6 +321,13 @@ pub fn runs_release() -> bool { !Os::singleton().is_debug_build() } +/// Whether we are running in GitHub Actions CI. +/// +/// Must not be used to influence test logic. Only for logging and diagnostics. +pub fn runs_github_ci() -> bool { + std::env::var("GITHUB_ACTIONS").as_deref() == Ok("true") +} + /// Create a `GDScript` script from code, compiles and returns it. pub fn create_gdscript(code: &str) -> Gd { let mut script = GDScript::new_gd(); diff --git a/itest/rust/src/register_tests/register_docs_test.rs b/itest/rust/src/register_tests/register_docs_test.rs index d0c23037e..85a5ae04f 100644 --- a/itest/rust/src/register_tests/register_docs_test.rs +++ b/itest/rust/src/register_tests/register_docs_test.rs @@ -321,13 +321,44 @@ impl FairlyDocumented { #[itest] fn test_register_docs() { - let xml = find_class_docs("FairlyDocumented"); + let actual_xml = find_class_docs("FairlyDocumented"); // Uncomment if implementation changes and expected output file should be rewritten. // std::fs::write("../rust/src/register_tests/res/registered_docs.xml", &xml) // .expect("failed to write docs XML file"); - assert_eq!(include_str!("res/registered_docs.xml"), xml); + let expected_xml = include_str!("res/registered_docs.xml"); + + if actual_xml == expected_xml { + return; // All good. + } + + // In GitHub Actions, print output of expected vs. actual. + if crate::framework::runs_github_ci() { + panic!( + "Registered docs XML does not match expected output.\ + \n============================================================\ + \nExpected:\n\n{expected_xml}\n\ + \n------------------------------------------------------------\ + \nActual:\n\n{actual_xml}\n\ + \n============================================================\n" + ); + } + + // Locally, write to a file for manual inspection/diffing. + let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")) + .join("src") + .join("register_tests") + .join("res") + .join("actual_registered_docs.xml"); + + std::fs::write(&path, &actual_xml).expect("write `actual_registered_docs.xml` failed"); + + panic!( + "Registered docs XML does not match expected output.\n\ + Actual output has been written to following file:\n {path}\n", + path = path.display() + ); } fn find_class_docs(class_name: &str) -> String { From bcc478938171aad9113d16c46ce8ba00cb79153c Mon Sep 17 00:00:00 2001 From: Jan Haller Date: Sat, 1 Nov 2025 13:38:39 +0100 Subject: [PATCH 47/54] Simplify + clean up XML doc generation --- godot-core/src/docs.rs | 125 +++++++++--------- godot-macros/src/docs/extract_docs.rs | 19 +-- godot-macros/src/docs/mod.rs | 12 +- itest/rust/src/framework/mod.rs | 1 + .../src/register_tests/register_docs_test.rs | 2 +- .../register_tests/res/registered_docs.xml | 2 +- 6 files changed, 82 insertions(+), 79 deletions(-) diff --git a/godot-core/src/docs.rs b/godot-core/src/docs.rs index 46d04a8a3..17b1019a0 100644 --- a/godot-core/src/docs.rs +++ b/godot-core/src/docs.rs @@ -60,7 +60,7 @@ pub struct StructDocs { pub description: &'static str, pub experimental: &'static str, pub deprecated: &'static str, - pub members: &'static str, + pub properties: &'static str, } /// Keeps documentation for inherent `impl` blocks (primary and secondary), such as: @@ -82,17 +82,20 @@ pub struct StructDocs { /// All fields are XML parts, escaped where necessary. #[derive(Default, Clone, Debug)] pub struct InherentImplDocs { - pub methods: &'static str, - pub signals: &'static str, - pub constants: &'static str, + pub methods_xml: &'static str, + pub signals_xml: &'static str, + pub constants_xml: &'static str, } +/// Godot editor documentation for a class, combined from individual definitions (struct + impls). +/// +/// All fields are collections of XML parts, escaped where necessary. #[derive(Default)] -struct DocPieces { +struct AggregatedDocs { definition: StructDocs, - methods: Vec<&'static str>, - signals: Vec<&'static str>, - constants: Vec<&'static str>, + methods_xmls: Vec<&'static str>, + signals_xmls: Vec<&'static str>, + constants_xmls: Vec<&'static str>, } /// This function scours the registered plugins to find their documentation pieces, @@ -110,77 +113,75 @@ struct DocPieces { /// strings of not-yet-parented XML tags (or empty string if no method has been documented). #[doc(hidden)] pub fn gather_xml_docs() -> impl Iterator { - let mut map = HashMap::::new(); - crate::private::iterate_docs_plugins(|x| { - let class_name = x.class_name; - match &x.item { - DocsItem::Struct(s) => { - map.entry(class_name).or_default().definition = *s; + let mut map = HashMap::::new(); + + crate::private::iterate_docs_plugins(|shard| { + let class_name = shard.class_name; + match &shard.item { + DocsItem::Struct(struct_docs) => { + map.entry(class_name).or_default().definition = *struct_docs; } + DocsItem::InherentImpl(trait_docs) => { - let InherentImplDocs { - methods, - constants, - signals, - } = trait_docs; - map.entry(class_name).or_default().methods.push(methods); map.entry(class_name) - .and_modify(|pieces| pieces.constants.push(constants)); + .or_default() + .methods_xmls + .push(trait_docs.methods_xml); + + map.entry(class_name) + .and_modify(|pieces| pieces.constants_xmls.push(trait_docs.constants_xml)); + map.entry(class_name) - .and_modify(|pieces| pieces.signals.push(signals)); + .and_modify(|pieces| pieces.signals_xmls.push(trait_docs.signals_xml)); } - DocsItem::ITraitImpl(methods) => { - map.entry(class_name).or_default().methods.push(methods); + + DocsItem::ITraitImpl(methods_xml) => { + map.entry(class_name) + .or_default() + .methods_xmls + .push(methods_xml); } } }); map.into_iter().map(|(class, pieces)| { - let StructDocs { - base, - description, - experimental, - deprecated, - members, - } = pieces.definition; - - - let method_docs = String::from_iter(pieces.methods); - let signal_docs = String::from_iter(pieces.signals); - let constant_docs = String::from_iter(pieces.constants); - - let methods_block = if method_docs.is_empty() { - String::new() - } else { - format!("{method_docs}") - }; - let signals_block = if signal_docs.is_empty() { - String::new() - } else { - format!("{signal_docs}") - }; - let constants_block = if constant_docs.is_empty() { - String::new() - } else { - format!("{constant_docs}") - }; - let (brief, description) = match description - .split_once("[br]") { - Some((brief, description)) => (brief, description.trim_start_matches("[br]")), - None => (description, ""), - }; - - format!(r#" + let StructDocs { + base, + description, + experimental, + deprecated, + properties, + } = pieces.definition; + + let methods_block = wrap_in_xml_block("methods", pieces.methods_xmls); + let signals_block = wrap_in_xml_block("signals", pieces.signals_xmls); + let constants_block = wrap_in_xml_block("constants", pieces.constants_xmls); + + let (brief, description) = match description.split_once("[br]") { + Some((brief, description)) => (brief, description.trim_start_matches("[br]")), + None => (description, ""), + }; + + format!(r#" {brief} {description} {methods_block} {constants_block} {signals_block} -{members} +{properties} "#) - }, - ) + }) +} + +fn wrap_in_xml_block(tag: &str, blocks: Vec<&'static str>) -> String { + let content = String::from_iter(blocks); + + if content.is_empty() { + String::new() + } else { + format!("<{tag}>{content}") + } } /// # Safety diff --git a/godot-macros/src/docs/extract_docs.rs b/godot-macros/src/docs/extract_docs.rs index 3ccdbd005..36070330f 100644 --- a/godot-macros/src/docs/extract_docs.rs +++ b/godot-macros/src/docs/extract_docs.rs @@ -34,27 +34,28 @@ pub fn document_struct( description: &[venial::Attribute], fields: &[Field], ) -> TokenStream { - let base_escaped = xml_escape(base); let XmlParagraphs { description_content, deprecated_attr, experimental_attr, } = attribute_docs_to_xml_paragraphs(description).unwrap_or_default(); - let members = fields + let properties = fields .iter() .filter(|field| field.var.is_some() || field.export.is_some()) .filter_map(format_member_xml) .collect::(); + let base_escaped = xml_escape(base); + quote! { - ::godot::docs::StructDocs { - base: #base_escaped, - description: #description_content, - experimental: #experimental_attr, - deprecated: #deprecated_attr, - members: #members, - } + ::godot::docs::StructDocs { + base: #base_escaped, + description: #description_content, + experimental: #experimental_attr, + deprecated: #deprecated_attr, + properties: #properties, + } } } diff --git a/godot-macros/src/docs/mod.rs b/godot-macros/src/docs/mod.rs index 4bb1b80f5..c54d72385 100644 --- a/godot-macros/src/docs/mod.rs +++ b/godot-macros/src/docs/mod.rs @@ -51,11 +51,11 @@ mod docs_generators { quote! { ::godot::sys::plugin_add!(#prv::__GODOT_DOCS_REGISTRY; #prv::DocsPlugin::new::<#class_name>( - #prv::DocsItem::InherentImpl(#prv::InherentImplDocs { - methods: #method_xml_elems, - signals: #signal_xml_elems, - constants: #constant_xml_elems - }) + #prv::DocsItem::InherentImpl(#prv::InherentImplDocs { + methods_xml: #method_xml_elems, + signals_xml: #signal_xml_elems, + constants_xml: #constant_xml_elems + }) )); } } @@ -69,7 +69,7 @@ mod docs_generators { quote! { ::godot::sys::plugin_add!(#prv::__GODOT_DOCS_REGISTRY; #prv::DocsPlugin::new::<#class_name>( - #prv::DocsItem::ITraitImpl(#virtual_methods) + #prv::DocsItem::ITraitImpl(#virtual_methods) )); } } diff --git a/itest/rust/src/framework/mod.rs b/itest/rust/src/framework/mod.rs index 0cfb4c82b..422794e21 100644 --- a/itest/rust/src/framework/mod.rs +++ b/itest/rust/src/framework/mod.rs @@ -324,6 +324,7 @@ pub fn runs_release() -> bool { /// Whether we are running in GitHub Actions CI. /// /// Must not be used to influence test logic. Only for logging and diagnostics. +#[allow(dead_code)] pub fn runs_github_ci() -> bool { std::env::var("GITHUB_ACTIONS").as_deref() == Ok("true") } diff --git a/itest/rust/src/register_tests/register_docs_test.rs b/itest/rust/src/register_tests/register_docs_test.rs index 85a5ae04f..9504e4359 100644 --- a/itest/rust/src/register_tests/register_docs_test.rs +++ b/itest/rust/src/register_tests/register_docs_test.rs @@ -316,7 +316,7 @@ impl FairlyDocumented { impl FairlyDocumented { /// Documented method in other godot_api secondary block #[func] - fn trinary_but_documented(&self, _smth: i64) {} + fn tertiary_but_documented(&self, _smth: i64) {} } #[itest] diff --git a/itest/rust/src/register_tests/res/registered_docs.xml b/itest/rust/src/register_tests/res/registered_docs.xml index bf378abdb..f2d75fcc7 100644 --- a/itest/rust/src/register_tests/res/registered_docs.xml +++ b/itest/rust/src/register_tests/res/registered_docs.xml @@ -81,7 +81,7 @@ public class Player : Node2D - + From 7e92794df8816050ccca739cda83cb50bca90d40 Mon Sep 17 00:00:00 2001 From: Jan Haller Date: Sat, 1 Nov 2025 14:11:06 +0100 Subject: [PATCH 48/54] Sort XML blocks for deterministic output; update expected XML --- godot-core/src/docs.rs | 6 +++- .../register_tests/res/registered_docs.xml | 32 +++++++++---------- 2 files changed, 21 insertions(+), 17 deletions(-) diff --git a/godot-core/src/docs.rs b/godot-core/src/docs.rs index 17b1019a0..fc7bcf43b 100644 --- a/godot-core/src/docs.rs +++ b/godot-core/src/docs.rs @@ -174,7 +174,11 @@ pub fn gather_xml_docs() -> impl Iterator { }) } -fn wrap_in_xml_block(tag: &str, blocks: Vec<&'static str>) -> String { +fn wrap_in_xml_block(tag: &str, mut blocks: Vec<&'static str>) -> String { + // We sort the blocks for deterministic output. No need to sort individual methods/signals/constants, this is already done by Godot. + // See https://github.com/godot-rust/gdext/pull/1391 for more information. + blocks.sort(); + let content = String::from_iter(blocks); if content.is_empty() { diff --git a/itest/rust/src/register_tests/res/registered_docs.xml b/itest/rust/src/register_tests/res/registered_docs.xml index f2d75fcc7..6ef6f7f29 100644 --- a/itest/rust/src/register_tests/res/registered_docs.xml +++ b/itest/rust/src/register_tests/res/registered_docs.xml @@ -25,6 +25,22 @@ public class Player : Node2D + + + + + Documented method in godot_api secondary block + + + + + + + + Documented method in other godot_api secondary block + + + @@ -72,22 +88,6 @@ public class Player : Node2D ????? probably - - - - - - Documented method in godot_api secondary block - - - - - - - - Documented method in other godot_api secondary block - - Documentation.HmmmmWho would know that!this docstring has < a special character From b07d883700b48e5e8deed1513a28d6023e551821 Mon Sep 17 00:00:00 2001 From: Jan Haller Date: Sun, 26 Oct 2025 20:23:29 +0100 Subject: [PATCH 49/54] Various small cleanups --- godot-codegen/src/generator/signals.rs | 2 ++ godot-codegen/src/models/domain.rs | 12 ++++++---- godot-core/src/builtin/basis.rs | 24 +++++++++---------- godot-core/src/builtin/callable.rs | 2 +- godot-core/src/builtin/collections/array.rs | 2 +- .../src/builtin/collections/dictionary.rs | 2 +- godot-core/src/builtin/mod.rs | 2 +- godot-core/src/builtin/string/gstring.rs | 2 +- godot-core/src/builtin/string/node_path.rs | 4 ++-- godot-core/src/builtin/string/string_name.rs | 2 +- godot-core/src/builtin/variant/mod.rs | 6 ++--- .../src/class/data_models/inherent_impl.rs | 6 ++++- godot-macros/src/class/derive_godot_class.rs | 2 +- .../builtin_tests/containers/signal_test.rs | 1 - itest/rust/src/engine_tests/async_test.rs | 3 ++- 15 files changed, 40 insertions(+), 32 deletions(-) diff --git a/godot-codegen/src/generator/signals.rs b/godot-codegen/src/generator/signals.rs index c74f382f3..f749fc656 100644 --- a/godot-codegen/src/generator/signals.rs +++ b/godot-codegen/src/generator/signals.rs @@ -10,6 +10,8 @@ // for these signals, and integration is slightly different due to lack of WithBaseField trait. Nonetheless, some parts could potentially // be extracted into a future crate shared by godot-codegen and godot-macros. +// TODO(v0.5): signal parameters are Gd instead of conservatively Option>, which is a bug. + use proc_macro2::{Ident, TokenStream}; use quote::{format_ident, quote}; diff --git a/godot-codegen/src/models/domain.rs b/godot-codegen/src/models/domain.rs index a95a515a2..5154bae04 100644 --- a/godot-codegen/src/models/domain.rs +++ b/godot-codegen/src/models/domain.rs @@ -737,19 +737,22 @@ pub enum RustTy { /// C-style raw pointer to a `RustTy`. RawPointer { inner: Box, is_const: bool }, - /// `Array>` + /// `Array>`. Never contains `Option` elements. EngineArray { tokens: TokenStream, - #[allow(dead_code)] // only read in minimal config + + #[allow(dead_code)] // Only read in minimal config. elem_class: String, }, /// `module::Enum` or `module::Bitfield` EngineEnum { tokens: TokenStream, - /// `None` for globals - #[allow(dead_code)] // only read in minimal config + + /// `None` for globals. + #[allow(dead_code)] // Only read in minimal config. surrounding_class: Option, + is_bitfield: bool, }, @@ -794,7 +797,6 @@ impl RustTy { /// Returns tokens without `Option` wrapper, even for nullable engine classes. /// /// For `EngineClass`, always returns `Gd` regardless of nullability. For other types, behaves the same as `ToTokens`. - // TODO(v0.5): only used for signal params, which is a bug. Those should conservatively be Option> as well. // Might also be useful to directly extract inner `gd_tokens` field. pub fn tokens_non_null(&self) -> TokenStream { match self { diff --git a/godot-core/src/builtin/basis.rs b/godot-core/src/builtin/basis.rs index 23bae4563..e8bdcf702 100644 --- a/godot-core/src/builtin/basis.rs +++ b/godot-core/src/builtin/basis.rs @@ -703,22 +703,22 @@ mod test { Basis::from_euler(EulerOrder::XYZ, euler_xyz_from_rotation); let res = to_rotation.inverse() * rotation_from_xyz_computed_euler; + let col_a = res.col_a(); + let col_b = res.col_b(); + let col_c = res.col_c(); assert!( - (res.col_a() - Vector3::new(1.0, 0.0, 0.0)).length() <= 0.1, - "Double check with XYZ rot order failed, due to X {} with {deg_original_euler} using {rot_order:?}", - res.col_a(), - ); + (col_a - Vector3::new(1.0, 0.0, 0.0)).length() <= 0.1, + "Double check with XYZ rot order failed, due to A {col_a} with {deg_original_euler} using {rot_order:?}", + ); assert!( - (res.col_b() - Vector3::new(0.0, 1.0, 0.0)).length() <= 0.1, - "Double check with XYZ rot order failed, due to Y {} with {deg_original_euler} using {rot_order:?}", - res.col_b(), - ); + (col_b - Vector3::new(0.0, 1.0, 0.0)).length() <= 0.1, + "Double check with XYZ rot order failed, due to B {col_b} with {deg_original_euler} using {rot_order:?}", + ); assert!( - (res.col_c() - Vector3::new(0.0, 0.0, 1.0)).length() <= 0.1, - "Double check with XYZ rot order failed, due to Z {} with {deg_original_euler} using {rot_order:?}", - res.col_c(), - ); + (col_c - Vector3::new(0.0, 0.0, 1.0)).length() <= 0.1, + "Double check with XYZ rot order failed, due to C {col_c} with {deg_original_euler} using {rot_order:?}", + ); } #[test] diff --git a/godot-core/src/builtin/callable.rs b/godot-core/src/builtin/callable.rs index 9d174feff..783c6ef93 100644 --- a/godot-core/src/builtin/callable.rs +++ b/godot-core/src/builtin/callable.rs @@ -461,7 +461,7 @@ impl Callable { /// _Godot equivalent: `hash`_ } - #[deprecated = "renamed to hash_u32"] + #[deprecated = "renamed to `hash_u32`"] pub fn hash(&self) -> u32 { self.as_inner().hash().try_into().unwrap() } diff --git a/godot-core/src/builtin/collections/array.rs b/godot-core/src/builtin/collections/array.rs index 4079b7bdc..ba4df6481 100644 --- a/godot-core/src/builtin/collections/array.rs +++ b/godot-core/src/builtin/collections/array.rs @@ -293,7 +293,7 @@ impl Array { /// because different arrays can have identical hash values due to hash collisions. } - #[deprecated = "renamed to hash_u32"] + #[deprecated = "renamed to `hash_u32`"] pub fn hash(&self) -> u32 { self.as_inner().hash().try_into().unwrap() } diff --git a/godot-core/src/builtin/collections/dictionary.rs b/godot-core/src/builtin/collections/dictionary.rs index ebe00e42d..166c48bde 100644 --- a/godot-core/src/builtin/collections/dictionary.rs +++ b/godot-core/src/builtin/collections/dictionary.rs @@ -289,7 +289,7 @@ impl Dictionary { /// Returns a 32-bit integer hash value representing the dictionary and its contents. } - #[deprecated = "renamed to hash_u32"] + #[deprecated = "renamed to `hash_u32`"] #[must_use] pub fn hash(&self) -> u32 { self.as_inner().hash().try_into().unwrap() diff --git a/godot-core/src/builtin/mod.rs b/godot-core/src/builtin/mod.rs index f8d832d72..61227c6d2 100644 --- a/godot-core/src/builtin/mod.rs +++ b/godot-core/src/builtin/mod.rs @@ -118,7 +118,7 @@ pub mod inner { #[macro_export] macro_rules! declare_hash_u32_method { - ($ ($docs:tt)+ ) => { + ( $( $docs:tt )+ ) => { $( $docs )+ pub fn hash_u32(&self) -> u32 { self.as_inner().hash().try_into().expect("Godot hashes are uint32_t") diff --git a/godot-core/src/builtin/string/gstring.rs b/godot-core/src/builtin/string/gstring.rs index ca2470d67..b001d4ce2 100644 --- a/godot-core/src/builtin/string/gstring.rs +++ b/godot-core/src/builtin/string/gstring.rs @@ -160,7 +160,7 @@ impl GString { /// Returns a 32-bit integer hash value representing the string. } - #[deprecated = "renamed to hash_u32"] + #[deprecated = "renamed to `hash_u32`"] pub fn hash(&self) -> u32 { self.as_inner() .hash() diff --git a/godot-core/src/builtin/string/node_path.rs b/godot-core/src/builtin/string/node_path.rs index e57317235..c518da209 100644 --- a/godot-core/src/builtin/string/node_path.rs +++ b/godot-core/src/builtin/string/node_path.rs @@ -118,10 +118,10 @@ impl NodePath { } crate::declare_hash_u32_method! { - /// Returns a 32-bit integer hash value representing the string. + /// Returns a 32-bit integer hash value representing the node path. } - #[deprecated = "renamed to hash_u32"] + #[deprecated = "renamed to `hash_u32`"] pub fn hash(&self) -> u32 { self.as_inner() .hash() diff --git a/godot-core/src/builtin/string/string_name.rs b/godot-core/src/builtin/string/string_name.rs index fd9fe8644..418c29a69 100644 --- a/godot-core/src/builtin/string/string_name.rs +++ b/godot-core/src/builtin/string/string_name.rs @@ -143,7 +143,7 @@ impl StringName { /// Returns a 32-bit integer hash value representing the string. } - #[deprecated = "renamed to hash_u32"] + #[deprecated = "renamed to `hash_u32`"] pub fn hash(&self) -> u32 { self.as_inner() .hash() diff --git a/godot-core/src/builtin/variant/mod.rs b/godot-core/src/builtin/variant/mod.rs index 5d189ca2e..bec3fa10b 100644 --- a/godot-core/src/builtin/variant/mod.rs +++ b/godot-core/src/builtin/variant/mod.rs @@ -311,7 +311,7 @@ impl Variant { /// /// _Godot equivalent : `@GlobalScope.hash()`_ pub fn hash_u32(&self) -> u32 { - // @GlobalScope.hash() actually calls the VariantUtilityFunctions::hash(&Variant) function (cpp). + // @GlobalScope.hash() actually calls the VariantUtilityFunctions::hash(&Variant) function (C++). // This function calls the passed reference's `hash` method, which returns a uint32_t. // Therefore, casting this function to u32 is always fine. unsafe { interface_fn!(variant_hash)(self.var_sys()) } @@ -319,9 +319,9 @@ impl Variant { .expect("Godot hashes are uint32_t") } - #[deprecated = "renamed to hash_u32 and type changed to u32"] + #[deprecated = "renamed to `hash_u32` and type changed to `u32`"] pub fn hash(&self) -> i64 { - unsafe { interface_fn!(variant_hash)(self.var_sys()) } + self.hash_u32().into() } /// Interpret the `Variant` as `bool`. diff --git a/godot-macros/src/class/data_models/inherent_impl.rs b/godot-macros/src/class/data_models/inherent_impl.rs index 2bcd8ac74..e40202538 100644 --- a/godot-macros/src/class/data_models/inherent_impl.rs +++ b/godot-macros/src/class/data_models/inherent_impl.rs @@ -425,7 +425,11 @@ fn add_virtual_script_call( rename: &Option, gd_self_parameter: Option, ) -> String { - assert!(cfg!(since_api = "4.3")); + #[allow(clippy::assertions_on_constants)] + { + // Without braces, clippy removes the #[allow] for some reason... + assert!(cfg!(since_api = "4.3")); + } // Update parameter names, so they can be forwarded (e.g. a "_" declared by the user cannot). let is_params = function.params.iter_mut().skip(1); // skip receiver. diff --git a/godot-macros/src/class/derive_godot_class.rs b/godot-macros/src/class/derive_godot_class.rs index b1696db12..532fe7660 100644 --- a/godot-macros/src/class/derive_godot_class.rs +++ b/godot-macros/src/class/derive_godot_class.rs @@ -52,7 +52,7 @@ pub fn derive_godot_class(item: venial::Item) -> ParseResult { let class_name = &class.name; let class_name_str: String = struct_cfg .rename - .map_or_else(|| class.name.clone(), |rename| rename) + .unwrap_or_else(|| class.name.clone()) .to_string(); let class_name_allocation = quote! { ClassId::__alloc_next_unicode(#class_name_str) }; diff --git a/itest/rust/src/builtin_tests/containers/signal_test.rs b/itest/rust/src/builtin_tests/containers/signal_test.rs index 37ccd38b4..6ada19106 100644 --- a/itest/rust/src/builtin_tests/containers/signal_test.rs +++ b/itest/rust/src/builtin_tests/containers/signal_test.rs @@ -15,7 +15,6 @@ use godot::meta::{FromGodot, GodotConvert, ToGodot}; use godot::obj::{Base, Gd, InstanceId, NewAlloc, NewGd}; use godot::prelude::ConvertError; use godot::register::{godot_api, GodotClass}; -use godot::sys; use godot::sys::Global; use crate::framework::itest; diff --git a/itest/rust/src/engine_tests/async_test.rs b/itest/rust/src/engine_tests/async_test.rs index cfbb201f1..e2b80b50f 100644 --- a/itest/rust/src/engine_tests/async_test.rs +++ b/itest/rust/src/engine_tests/async_test.rs @@ -11,7 +11,6 @@ use godot::builtin::{array, vslice, Array, Callable, Signal}; use godot::classes::{Object, RefCounted}; use godot::obj::{Base, Gd, NewAlloc, NewGd}; use godot::prelude::{godot_api, GodotClass}; -use godot::sys; use godot::task::{self, create_test_signal_future_resolver, SignalFuture, TaskHandle}; use crate::framework::{expect_async_panic, itest, TestContext}; @@ -132,6 +131,8 @@ fn async_task_signal_future_panic() -> TaskHandle { #[cfg(feature = "experimental-threads")] #[itest(async)] fn signal_future_non_send_arg_panic() -> TaskHandle { + use godot::sys; + use crate::framework::ThreadCrosser; let mut object = RefCounted::new_gd(); From da6710c801e2b1c2d31c39a031a9e388a4da928d Mon Sep 17 00:00:00 2001 From: Jan Haller Date: Sat, 1 Nov 2025 10:09:11 +0100 Subject: [PATCH 50/54] check.sh: run rustfmt in nightly if available --- check.sh | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/check.sh b/check.sh index b377bfc3a..44a98926c 100755 --- a/check.sh +++ b/check.sh @@ -142,7 +142,13 @@ function findGodot() { # builtins like `test`. function cmd_fmt() { - run cargo fmt --all -- --check + # Run rustfmt in nightly toolchain if available. + if [[ $(rustup toolchain list) =~ nightly ]]; then + run cargo +nightly fmt --all -- --check + else + log -e "${YELLOW}Warning: nightly toolchain not found; stable rustfmt might not pass CI.${END}" + run cargo fmt --all -- --check + fi } function cmd_clippy() { From 105c86292af67b2825fd8abc9520aa484c7f87c5 Mon Sep 17 00:00:00 2001 From: Jan Haller Date: Sat, 1 Nov 2025 09:55:56 +0100 Subject: [PATCH 51/54] `#[bench(manual)]` for more control, including setup --- godot-macros/src/bench.rs | 75 +++++++++++++++++++++-------- itest/rust/src/benchmarks/mod.rs | 16 +++--- itest/rust/src/common.rs | 10 ---- itest/rust/src/framework/bencher.rs | 44 ++++++++++++++--- itest/rust/src/framework/mod.rs | 3 +- itest/rust/src/framework/runner.rs | 14 ++++-- 6 files changed, 114 insertions(+), 48 deletions(-) diff --git a/godot-macros/src/bench.rs b/godot-macros/src/bench.rs index 29a2b43c9..e729956be 100644 --- a/godot-macros/src/bench.rs +++ b/godot-macros/src/bench.rs @@ -19,23 +19,30 @@ pub fn attribute_bench(input_decl: venial::Item) -> ParseResult { _ => return bail!(&input_decl, "#[bench] can only be applied to functions"), }; - // Note: allow attributes for things like #[rustfmt] or #[clippy] - if func.generic_params.is_some() || !func.params.is_empty() || func.where_clause.is_some() { + // Disallow generics, but allow attributes for things like #[rustfmt] or #[clippy]. + if func.generic_params.is_some() || func.where_clause.is_some() { return bad_signature(&func); } - // Ignore -> (), as no one does that by accident. - // We need `ret` to make sure the type is correct and to avoid unused imports (by IDEs). - let Some(ret) = func.return_ty else { + let mut attr = KvParser::parse_required(&func.attributes, "bench", &func.name)?; + let manual = attr.handle_alone("manual")?; + let repetitions = attr.handle_usize("repeat")?; + attr.finish()?; + + // Validate attribute combinations. + if manual && repetitions.is_some() { return bail!( func, - "#[bench] function must return a value from its computation, to prevent optimizing the operation away" + "#[bench(manual)] cannot be combined with `repeat` -- pass repetitions to bench_measure() instead" ); - }; + } - let mut attr = KvParser::parse_required(&func.attributes, "bench", &func.name)?; - let repetitions = attr.handle_usize("repeat")?.unwrap_or(DEFAULT_REPETITIONS); - attr.finish()?; + let repetitions = repetitions.unwrap_or(DEFAULT_REPETITIONS); + + // Validate parameter count. + if !func.params.is_empty() { + return bad_signature(&func); + } let bench_name = &func.name; let bench_name_str = func.name.to_string(); @@ -43,23 +50,48 @@ pub fn attribute_bench(input_decl: venial::Item) -> ParseResult { let body = &func.body; // Filter out #[bench] itself, but preserve other attributes like #[allow], #[expect], etc. - let other_attributes = retain_attributes_except(&func.attributes, "bench"); + let other_attributes: Vec<_> = retain_attributes_except(&func.attributes, "bench").collect(); - Ok(quote! { - #(#other_attributes)* - pub fn #bench_name() { - for _ in 0..#repetitions { - let __ret: #ret = #body; - crate::common::bench_used(__ret); + let generated_fn = if manual { + // Manual mode: user calls bench_measure() directly. + let ret = func.return_ty; + quote! { + #(#other_attributes)* + // Don't return crate::framework::BenchResult here, to keep user imports working naturally. + pub fn #bench_name() -> #ret { + #body + } + } + } else { + // Automatic mode: framework controls timing. + // Ignore `-> ()`, as no one does that by accident. + // We need `ret` to make sure the type is correct and to avoid unused imports (by IDEs). + let Some(ret) = func.return_ty else { + return bail!( + func, + "#[bench] function must return a value from its computation, to prevent optimizing the operation away" + ); + }; + + quote! { + #(#other_attributes)* + pub fn #bench_name() -> crate::framework::BenchResult { + crate::framework::bench_measure(#repetitions, || { + let __ret: #ret = #body; + __ret // passed onto bench_used() by caller. + }) } } + }; + + Ok(quote! { + #generated_fn ::godot::sys::plugin_add!(crate::framework::__GODOT_BENCH; crate::framework::RustBenchmark { name: #bench_name_str, file: std::file!(), line: std::line!(), function: #bench_name, - repetitions: #repetitions, }); }) } @@ -68,7 +100,12 @@ fn bad_signature(func: &venial::Function) -> Result bail!( func, "#[bench] function must have one of these signatures:\ - \n fn {f}() {{ ... }}", + \n\ + \n(1) #[bench]\ + \n fn {f}() -> T {{ ... }}\ + \n\ + \n(2) #[bench(manual)]\ + \n fn {f}() -> BenchResult {{ ... /* call to bench_measure() */ }}", f = func.name, ) } diff --git a/itest/rust/src/benchmarks/mod.rs b/itest/rust/src/benchmarks/mod.rs index 6d41bec56..f6fb5d00a 100644 --- a/itest/rust/src/benchmarks/mod.rs +++ b/itest/rust/src/benchmarks/mod.rs @@ -16,7 +16,7 @@ use godot::obj::{Gd, InstanceId, NewAlloc, NewGd, Singleton}; use godot::prelude::{varray, Callable, RustCallable, Variant}; use godot::register::GodotClass; -use crate::framework::bench; +use crate::framework::{bench, bench_measure, BenchResult}; mod color; @@ -114,16 +114,18 @@ fn packed_array_from_iter_unknown_size() -> PackedInt32Array { })) } -#[bench(repeat = 25)] -fn call_callv_rust_fn() -> Variant { +#[bench(manual)] +fn call_callv_rust_fn() -> BenchResult { let callable = Callable::from_fn("RustFunction", |_| ()); - callable.callv(&varray![]) + + bench_measure(25, || callable.callv(&varray![])) } -#[bench(repeat = 25)] -fn call_callv_custom() -> Variant { +#[bench(manual)] +fn call_callv_custom() -> BenchResult { let callable = Callable::from_custom(MyRustCallable {}); - callable.callv(&varray![]) + + bench_measure(25, || callable.callv(&varray![])) } // ---------------------------------------------------------------------------------------------------------------------------------------------- diff --git a/itest/rust/src/common.rs b/itest/rust/src/common.rs index eb211efe2..ebfb1061e 100644 --- a/itest/rust/src/common.rs +++ b/itest/rust/src/common.rs @@ -21,13 +21,3 @@ where assert_eq!(value, back); } - -/// Signal to the compiler that a value is used (to avoid optimization). -pub fn bench_used(value: T) { - // The following check would be used to prevent `()` arguments, ensuring that a value from the bench is actually going into the blackbox. - // However, we run into this issue, despite no array being used: https://github.com/rust-lang/rust/issues/43408. - // error[E0401]: can't use generic parameters from outer function - // sys::static_assert!(std::mem::size_of::() != 0, "returned unit value in benchmark; make sure to use a real value"); - - std::hint::black_box(value); -} diff --git a/itest/rust/src/framework/bencher.rs b/itest/rust/src/framework/bencher.rs index 49e428e04..4e9c6fac4 100644 --- a/itest/rust/src/framework/bencher.rs +++ b/itest/rust/src/framework/bencher.rs @@ -17,13 +17,16 @@ // Instead, we focus on min (fastest run) and median -- even median may vary quite a bit between runs; but it gives an idea of the distribution. // See also https://easyperf.net/blog/2019/12/30/Comparing-performance-measurements#average-median-minimum. +use std::any::TypeId; use std::time::{Duration, Instant}; const WARMUP_RUNS: usize = 200; const TEST_RUNS: usize = 501; // uneven, so median need not be interpolated. const METRIC_COUNT: usize = 2; -pub struct BenchResult { +pub type BenchResult = Result; + +pub struct BenchMeasurement { pub stats: [Duration; METRIC_COUNT], } @@ -31,25 +34,42 @@ pub fn metrics() -> [&'static str; METRIC_COUNT] { ["min", "median"] } -pub fn run_benchmark(code: fn(), inner_repetitions: usize) -> BenchResult { +/// Measures the timing of the passed closure (repeated `inner_repetitions` times). +/// +/// Used by both `#[bench]` automatic mode (generated by macro) and `#[bench(manual)]` (called explicitly). +/// +/// Returns `Err(String)` if there is something wrong with the benchmark setup. +pub fn bench_measure(inner_repetitions: usize, work: impl Fn() -> R) -> BenchResult { + // Runtime check: ensure the closure doesn't return (). + if TypeId::of::() == TypeId::of::<()>() { + return Err("bench_measure(): closure must return non-unit type to prevent the computation from being optimized away".to_string()); + } + + // Warmup phase. for _ in 0..WARMUP_RUNS { - code(); + for _ in 0..inner_repetitions { + let _ = work(); + } } + // Measurement phase. let mut times = Vec::with_capacity(TEST_RUNS); for _ in 0..TEST_RUNS { let start = Instant::now(); - code(); + for _ in 0..inner_repetitions { + let fruits_of_labor = work(); + bench_used(fruits_of_labor); + } let duration = start.elapsed(); times.push(duration / inner_repetitions as u32); } times.sort(); - calculate_stats(times) + Ok(calculate_stats(times)) } -fn calculate_stats(times: Vec) -> BenchResult { +fn calculate_stats(times: Vec) -> BenchMeasurement { // See top of file for rationale. /*let mean = { @@ -72,7 +92,17 @@ fn calculate_stats(times: Vec) -> BenchResult { let min = times[0]; let median = times[TEST_RUNS / 2]; - BenchResult { + BenchMeasurement { stats: [min, median], } } + +/// Signal to the compiler that a value is used (to avoid optimization). +fn bench_used(value: T) { + // The following check would be used to prevent `()` arguments, ensuring that a value from the bench is actually going into the blackbox. + // However, we run into this issue, despite no array being used: https://github.com/rust-lang/rust/issues/43408. + // error[E0401]: can't use generic parameters from outer function + // sys::static_assert!(std::mem::size_of::() != 0, "returned unit value in benchmark; make sure to use a real value"); + + std::hint::black_box(value); +} diff --git a/itest/rust/src/framework/mod.rs b/itest/rust/src/framework/mod.rs index 422794e21..52684eeb3 100644 --- a/itest/rust/src/framework/mod.rs +++ b/itest/rust/src/framework/mod.rs @@ -158,8 +158,7 @@ pub struct RustBenchmark { pub file: &'static str, #[allow(dead_code)] pub line: u32, - pub function: fn(), - pub repetitions: usize, + pub function: fn() -> BenchResult, } pub fn passes_filter(filters: &[String], test_name: &str) -> bool { diff --git a/itest/rust/src/framework/runner.rs b/itest/rust/src/framework/runner.rs index 7c4e894ff..54f07857e 100644 --- a/itest/rust/src/framework/runner.rs +++ b/itest/rust/src/framework/runner.rs @@ -376,7 +376,7 @@ impl IntegrationTests { print_bench_pre(bench.name, bench.file, last_file.as_deref()); last_file = Some(bench.file.to_string()); - let result = bencher::run_benchmark(bench.function, bench.repetitions); + let result = (bench.function)(); print_bench_post(result); } } @@ -546,9 +546,17 @@ fn print_bench_pre(benchmark: &str, bench_file: &str, last_file: Option<&str>) { } fn print_bench_post(result: BenchResult) { - for stat in result.stats.iter() { - print!(" {:>10.3}μs", stat.as_nanos() as f64 / 1000.0); + match result { + Ok(measured) => { + for stat in measured.stats.iter() { + print!(" {:>10.3}μs", stat.as_nanos() as f64 / 1000.0); + } + } + Err(msg) => { + print!(" {FMT_RED}ERROR: {msg}{FMT_END}"); + } } + println!(); } From 3832736b5142b41476ca6d8a672177abdcd180f7 Mon Sep 17 00:00:00 2001 From: Jan Haller Date: Sat, 1 Nov 2025 23:26:37 +0100 Subject: [PATCH 52/54] Add `Array::functional_ops()` --- .../src/special_cases/special_cases.rs | 9 +- godot-core/src/builtin/collections/array.rs | 69 +++--- .../collections/array_functional_ops.rs | 213 ++++++++++++++++++ godot-core/src/builtin/collections/mod.rs | 2 + godot-core/src/builtin/mod.rs | 1 + godot-core/src/builtin/string/mod.rs | 14 +- .../src/builtin/string/string_macros.rs | 2 +- godot-ffi/src/toolbox.rs | 14 ++ .../builtin_tests/containers/array_test.rs | 95 +++++++- 9 files changed, 361 insertions(+), 58 deletions(-) create mode 100644 godot-core/src/builtin/collections/array_functional_ops.rs diff --git a/godot-codegen/src/special_cases/special_cases.rs b/godot-codegen/src/special_cases/special_cases.rs index 1d515177d..af9358e72 100644 --- a/godot-codegen/src/special_cases/special_cases.rs +++ b/godot-codegen/src/special_cases/special_cases.rs @@ -801,6 +801,7 @@ pub fn is_builtin_method_deleted(_class_name: &TyName, method: &JsonBuiltinMetho /// Returns some generic type – such as `GenericArray` representing `Array` – if method is marked as generic, `None` otherwise. /// /// Usually required to initialize the return value and cache its type (see also https://github.com/godot-rust/gdext/pull/1357). +#[rustfmt::skip] pub fn builtin_method_generic_ret( class_name: &TyName, method: &JsonBuiltinMethod, @@ -809,9 +810,11 @@ pub fn builtin_method_generic_ret( class_name.rust_ty.to_string().as_str(), method.name.as_str(), ) { - ("Array", "duplicate") | ("Array", "slice") => { - Some(FnReturn::with_generic_builtin(RustTy::GenericArray)) - } + | ("Array", "duplicate") + | ("Array", "slice") + | ("Array", "filter") + + => Some(FnReturn::with_generic_builtin(RustTy::GenericArray)), _ => None, } } diff --git a/godot-core/src/builtin/collections/array.rs b/godot-core/src/builtin/collections/array.rs index ba4df6481..561b93534 100644 --- a/godot-core/src/builtin/collections/array.rs +++ b/godot-core/src/builtin/collections/array.rs @@ -12,6 +12,7 @@ use std::{cmp, fmt}; use godot_ffi as sys; use sys::{ffi_methods, interface_fn, GodotFfi}; +use crate::builtin::iter::ArrayFunctionalOps; use crate::builtin::*; use crate::meta; use crate::meta::error::{ConvertError, FromGodotError, FromVariantError}; @@ -178,7 +179,7 @@ pub struct Array { } /// Guard that can only call immutable methods on the array. -struct ImmutableInnerArray<'a> { +pub(super) struct ImmutableInnerArray<'a> { inner: inner::InnerArray<'a>, } @@ -608,6 +609,13 @@ impl Array { } } + /// Access to Godot's functional-programming APIs based on callables. + /// + /// Exposes Godot array methods such as `filter()`, `map()`, `reduce()` and many more. See return type docs. + pub fn functional_ops(&self) -> ArrayFunctionalOps<'_, T> { + ArrayFunctionalOps::new(self) + } + /// 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. @@ -672,6 +680,8 @@ impl Array { /// /// Calling `bsearch` on an unsorted array results in unspecified behavior. Consider using `sort()` to ensure the sorting /// order is compatible with your callable's ordering. + /// + /// See also: [`bsearch_by()`][Self::bsearch_by], [`functional_ops().bsearch_custom()`][ArrayFunctionalOps::bsearch_custom]. pub fn bsearch(&self, value: impl AsArg) -> usize { meta::arg_into_ref!(value: T); @@ -682,13 +692,15 @@ impl Array { /// /// The comparator function should return an ordering that indicates whether its argument is `Less`, `Equal` or `Greater` the desired value. /// For example, for an ascending-ordered array, a simple predicate searching for a constant value would be `|elem| elem.cmp(&4)`. - /// See also [`slice::binary_search_by()`]. + /// This follows the design of [`slice::binary_search_by()`]. /// /// If the value is found, returns `Ok(index)` with its index. Otherwise, returns `Err(index)`, where `index` is the insertion index /// that would maintain sorting order. /// - /// Calling `bsearch_by` on an unsorted array results in unspecified behavior. Consider using [`sort_by()`] to ensure - /// the sorting order is compatible with your callable's ordering. + /// Calling `bsearch_by` on an unsorted array results in unspecified behavior. Consider using [`sort_unstable_by()`][Self::sort_unstable_by] + /// to ensure the sorting order is compatible with your callable's ordering. + /// + /// See also: [`bsearch()`][Self::bsearch], [`functional_ops().bsearch_custom()`][ArrayFunctionalOps::bsearch_custom]. pub fn bsearch_by(&self, mut func: F) -> Result where F: FnMut(&T) -> cmp::Ordering + 'static, @@ -712,7 +724,7 @@ impl Array { let debug_name = std::any::type_name::(); let index = Callable::with_scoped_fn(debug_name, godot_comparator, |pred| { - self.bsearch_custom(ignored_value, pred) + self.functional_ops().bsearch_custom(ignored_value, pred) }); if let Some(value_at_index) = self.get(index) { @@ -724,22 +736,9 @@ impl Array { Err(index) } - /// Finds the index of a value in a sorted array using binary search, with `Callable` custom predicate. - /// - /// The callable `pred` takes two elements `(a, b)` and should return if `a < b` (strictly less). - /// For a type-safe version, check out [`bsearch_by()`][Self::bsearch_by]. - /// - /// If the value is not present in the array, returns the insertion index that would maintain sorting order. - /// - /// Calling `bsearch_custom` on an unsorted array results in unspecified behavior. Consider using `sort_custom()` to ensure - /// the sorting order is compatible with your callable's ordering. + #[deprecated = "Moved to `functional_ops().bsearch_custom()`"] pub fn bsearch_custom(&self, value: impl AsArg, pred: &Callable) -> usize { - meta::arg_into_ref!(value: T); - - to_usize( - self.as_inner() - .bsearch_custom(&value.to_variant(), pred, true), - ) + self.functional_ops().bsearch_custom(value, pred) } /// Reverses the order of the elements in the array. @@ -753,9 +752,11 @@ impl Array { /// Sorts the array. /// /// 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`. For most variant types, + /// This means that values considered equal may have their order changed when using `sort_unstable()`. For most variant types, /// this distinction should not matter though. /// + /// See also: [`sort_unstable_by()`][Self::sort_unstable_by], [`sort_unstable_custom()`][Self::sort_unstable_custom]. + /// /// _Godot equivalent: `Array.sort()`_ #[doc(alias = "sort")] pub fn sort_unstable(&mut self) { @@ -771,8 +772,10 @@ impl Array { /// elements themselves would be achieved with `|a, b| a.cmp(b)`. /// /// 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_by`. For most variant types, + /// This means that values considered equal may have their order changed when using `sort_unstable_by()`. For most variant types, /// this distinction should not matter though. + /// + /// See also: [`sort_unstable()`][Self::sort_unstable], [`sort_unstable_custom()`][Self::sort_unstable_custom]. pub fn sort_unstable_by(&mut self, mut func: F) where F: FnMut(&T, &T) -> cmp::Ordering, @@ -795,14 +798,14 @@ impl Array { /// Sorts the array, using type-unsafe `Callable` comparator. /// - /// For a type-safe variant of this method, use [`sort_unstable_by()`][Self::sort_unstable_by]. - /// /// The callable expects two parameters `(lhs, rhs)` and should return a bool `lhs < rhs`. /// /// 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_custom`.For most variant types, + /// This means that values considered equal may have their order changed when using `sort_unstable_custom()`. For most variant types, /// this distinction should not matter though. /// + /// Type-safe alternatives: [`sort_unstable()`][Self::sort_unstable] , [`sort_unstable_by()`][Self::sort_unstable_by]. + /// /// _Godot equivalent: `Array.sort_custom()`_ #[doc(alias = "sort_custom")] pub fn sort_unstable_custom(&mut self, func: &Callable) { @@ -940,7 +943,7 @@ impl Array { inner::InnerArray::from_outer_typed(self) } - fn as_inner(&self) -> ImmutableInnerArray<'_> { + pub(super) fn as_inner(&self) -> ImmutableInnerArray<'_> { ImmutableInnerArray { // SAFETY: We can only read from the array. inner: unsafe { self.as_inner_mut() }, @@ -1634,21 +1637,25 @@ macro_rules! varray { /// This macro creates a [slice](https://doc.rust-lang.org/std/primitive.slice.html) of `Variant` values. /// /// # Examples -/// Variable number of arguments: +/// ## Variable number of arguments /// ```no_run /// # use godot::prelude::*; /// let slice: &[Variant] = vslice![42, "hello", true]; -/// /// let concat: GString = godot::global::str(slice); /// ``` -/// _(In practice, you might want to use [`godot_str!`][crate::global::godot_str] instead of `str()`.)_ +/// _In practice, you might want to use [`godot_str!`][crate::global::godot_str] instead of `str()`._ /// -/// Dynamic function call via reflection. NIL can still be passed inside `vslice!`, just use `Variant::nil()`. +/// ## Dynamic function call via reflection +/// NIL can still be passed inside `vslice!`, just use `Variant::nil()`. /// ```no_run /// # use godot::prelude::*; /// # fn some_object() -> Gd { unimplemented!() } /// let mut obj: Gd = some_object(); -/// obj.call("some_method", vslice![Vector2i::new(1, 2), Variant::nil()]); +/// +/// obj.call("some_method", vslice![ +/// Vector2i::new(1, 2), +/// Variant::nil(), +/// ]); /// ``` /// /// # See also diff --git a/godot-core/src/builtin/collections/array_functional_ops.rs b/godot-core/src/builtin/collections/array_functional_ops.rs new file mode 100644 index 000000000..12d062e2c --- /dev/null +++ b/godot-core/src/builtin/collections/array_functional_ops.rs @@ -0,0 +1,213 @@ +/* + * Copyright (c) godot-rust; Bromeon and contributors. + * 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::builtin::{to_usize, Array, Callable, Variant, VariantArray}; +use crate::meta::{ArrayElement, AsArg}; +use crate::{meta, sys}; + +/// Immutable, functional-programming operations for `Array`, based on Godot callables. +/// +/// Returned by [`Array::functional_ops()`]. +/// +/// These methods exist to provide parity with Godot, e.g. when porting GDScript code to Rust. However, they come with several disadvantages +/// compared to Rust's [iterator adapters](https://doc.rust-lang.org/stable/core/iter/index.html#adapters): +/// - Not type-safe: callables are dynamically typed, so you need to double-check signatures. Godot may misinterpret returned values +/// (e.g. predicates apply to any "truthy" values, not just booleans). +/// - Slower: dispatching through callables is typically more costly than iterating over variants, especially since every call involves multiple +/// variant conversions, too. Combining multiple operations like `filter().map()` is very expensive due to intermediate allocations. +/// - Less composable/flexible: Godot's `map()` always returns an untyped array, even if the input is typed and unchanged by the mapping. +/// Rust's `collect()` on the other hand gives you control over the output type. Chaining iterators can apply multiple transformations lazily. +/// +/// In many cases, it is thus better to use [`Array::iter_shared()`] combined with iterator adapters. Check the individual method docs of +/// this struct for concrete alternatives. +pub struct ArrayFunctionalOps<'a, T: ArrayElement> { + array: &'a Array, +} + +impl<'a, T: ArrayElement> ArrayFunctionalOps<'a, T> { + pub(super) fn new(owner: &'a Array) -> Self { + Self { array: owner } + } + + /// Returns a new array containing only the elements for which the callable returns a truthy value. + /// + /// **Rust alternatives:** [`Iterator::filter()`]. + /// + /// The callable has signature `fn(T) -> bool`. + /// + /// # Example + /// ```no_run + /// # use godot::prelude::*; + /// let array = array![1, 2, 3, 4, 5]; + /// let even = array.functional_ops().filter(&Callable::from_fn("is_even", |args| { + /// args[0].to::() % 2 == 0 + /// })); + /// assert_eq!(even, array![2, 4]); + /// ``` + #[must_use] + pub fn filter(&self, callable: &Callable) -> Array { + // SAFETY: filter() returns array of same type as self. + unsafe { self.array.as_inner().filter(callable) } + } + + /// Returns a new untyped array with each element transformed by the callable. + /// + /// **Rust alternatives:** [`Iterator::map()`]. + /// + /// The callable has signature `fn(T) -> Variant`. Since the transformation can change the element type, this method returns + /// a `VariantArray` (untyped array). + /// + /// # Example + /// ```no_run + /// # use godot::prelude::*; + /// let array = array![1.1, 1.5, 1.9]; + /// let rounded = array.functional_ops().map(&Callable::from_fn("round", |args| { + /// args[0].to::().round() as i64 + /// })); + /// assert_eq!(rounded, varray![1, 2, 2]); + /// ``` + #[must_use] + pub fn map(&self, callable: &Callable) -> VariantArray { + // SAFETY: map() returns an untyped array. + unsafe { self.array.as_inner().map(callable) } + } + + /// Reduces the array to a single value by iteratively applying the callable. + /// + /// **Rust alternatives:** [`Iterator::fold()`] or [`Iterator::reduce()`]. + /// + /// The callable takes two arguments: the accumulator and the current element. + /// It returns the new accumulator value. The process starts with `initial` as the accumulator. + /// + /// # Example + /// ```no_run + /// # use godot::prelude::*; + /// let array = array![1, 2, 3, 4]; + /// let sum = array.functional_ops().reduce( + /// &Callable::from_fn("sum", |args| { + /// args[0].to::() + args[1].to::() + /// }), + /// &0.to_variant() + /// ); + /// assert_eq!(sum, 10.to_variant()); + /// ``` + #[must_use] + pub fn reduce(&self, callable: &Callable, initial: &Variant) -> Variant { + self.array.as_inner().reduce(callable, initial) + } + + /// Returns `true` if the callable returns a truthy value for at least one element. + /// + /// **Rust alternatives:** [`Iterator::any()`]. + /// + /// The callable has signature `fn(element) -> bool`. + /// + /// # Example + /// ```no_run + /// # use godot::prelude::*; + /// let array = array![1, 2, 3, 4]; + /// let any_even = array.functional_ops().any(&Callable::from_fn("is_even", |args| { + /// args[0].to::() % 2 == 0 + /// })); + /// assert!(any_even); + /// ``` + pub fn any(&self, callable: &Callable) -> bool { + self.array.as_inner().any(callable) + } + + /// Returns `true` if the callable returns a truthy value for all elements. + /// + /// **Rust alternatives:** [`Iterator::all()`]. + /// + /// The callable has signature `fn(element) -> bool`. + /// + /// # Example + /// ```no_run + /// # use godot::prelude::*; + /// let array = array![2, 4, 6]; + /// let all_even = array.functional_ops().all(&Callable::from_fn("is_even", |args| { + /// args[0].to::() % 2 == 0 + /// })); + /// assert!(all_even); + /// ``` + pub fn all(&self, callable: &Callable) -> bool { + self.array.as_inner().all(callable) + } + + /// Finds the index of the first element matching a custom predicate. + /// + /// **Rust alternatives:** [`Iterator::position()`]. + /// + /// The callable has signature `fn(element) -> bool`. + /// + /// Returns the index of the first element for which the callable returns a truthy value, starting from `from`. + /// If no element matches, returns `None`. + /// + /// # Example + /// ```no_run + /// # use godot::prelude::*; + /// let array = array![1, 2, 3, 4, 5]; + /// let is_even = Callable::from_fn("is_even", |args| { + /// args[0].to::() % 2 == 0 + /// }); + /// assert_eq!(array.functional_ops().find_custom(&is_even, None), Some(1)); // value 2 + /// assert_eq!(array.functional_ops().find_custom(&is_even, Some(2)), Some(3)); // value 4 + /// ``` + #[cfg(since_api = "4.4")] + pub fn find_custom(&self, callable: &Callable, from: Option) -> Option { + let from = from.map(|i| i as i64).unwrap_or(0); + let found_index = self.array.as_inner().find_custom(callable, from); + + sys::found_to_option(found_index) + } + + /// Finds the index of the last element matching a custom predicate, searching backwards. + /// + /// **Rust alternatives:** [`Iterator::rposition()`]. + /// + /// The callable has signature `fn(element) -> bool`. + /// + /// Returns the index of the last element for which the callable returns a truthy value, searching backwards from `from`. + /// If no element matches, returns `None`. + /// + /// # Example + /// ```no_run + /// # use godot::prelude::*; + /// let array = array![1, 2, 3, 4, 5]; + /// let is_even = Callable::from_fn("is_even", |args| { + /// args[0].to::() % 2 == 0 + /// }); + /// assert_eq!(array.functional_ops().rfind_custom(&is_even, None), Some(3)); // value 4 + /// assert_eq!(array.functional_ops().rfind_custom(&is_even, Some(2)), Some(1)); // value 2 + /// ``` + #[cfg(since_api = "4.4")] + pub fn rfind_custom(&self, callable: &Callable, from: Option) -> Option { + let from = from.map(|i| i as i64).unwrap_or(-1); + let found_index = self.array.as_inner().rfind_custom(callable, from); + + sys::found_to_option(found_index) + } + + /// Finds the index of a value in a sorted array using binary search, with `Callable` custom predicate. + /// + /// The callable `pred` takes two elements `(a, b)` and should return if `a < b` (strictly less). + /// For a type-safe version, check out [`Array::bsearch_by()`]. + /// + /// If the value is not present in the array, returns the insertion index that would maintain sorting order. + /// + /// Calling `bsearch_custom()` on an unsorted array results in unspecified behavior. Consider using [`Array::sort_unstable_custom()`] + /// to ensure the sorting order is compatible with your callable's ordering. + pub fn bsearch_custom(&self, value: impl AsArg, pred: &Callable) -> usize { + meta::arg_into_ref!(value: T); + + to_usize( + self.array + .as_inner() + .bsearch_custom(&value.to_variant(), pred, true), + ) + } +} diff --git a/godot-core/src/builtin/collections/mod.rs b/godot-core/src/builtin/collections/mod.rs index d28046dc9..5c6fade32 100644 --- a/godot-core/src/builtin/collections/mod.rs +++ b/godot-core/src/builtin/collections/mod.rs @@ -6,6 +6,7 @@ */ mod array; +mod array_functional_ops; mod dictionary; mod extend_buffer; mod packed_array; @@ -21,6 +22,7 @@ pub(crate) mod containers { // Re-export in godot::builtin::iter. #[rustfmt::skip] // Individual lines. pub(crate) mod iterators { + pub use super::array_functional_ops::ArrayFunctionalOps; pub use super::array::Iter as ArrayIter; pub use super::dictionary::Iter as DictIter; pub use super::dictionary::Keys as DictKeys; diff --git a/godot-core/src/builtin/mod.rs b/godot-core/src/builtin/mod.rs index 61227c6d2..0a97797af 100644 --- a/godot-core/src/builtin/mod.rs +++ b/godot-core/src/builtin/mod.rs @@ -65,6 +65,7 @@ pub use __prelude_reexport::*; pub mod math; /// Iterator types for arrays and dictionaries. +// Might rename this to `collections` or so. pub mod iter { pub use super::collections::iterators::*; } diff --git a/godot-core/src/builtin/string/mod.rs b/godot-core/src/builtin/string/mod.rs index aaab61279..5c10f5141 100644 --- a/godot-core/src/builtin/string/mod.rs +++ b/godot-core/src/builtin/string/mod.rs @@ -66,6 +66,7 @@ pub enum Encoding { } // ---------------------------------------------------------------------------------------------------------------------------------------------- +// Utilities fn populated_or_none(s: GString) -> Option { if s.is_empty() { @@ -75,19 +76,6 @@ fn populated_or_none(s: GString) -> Option { } } -fn found_to_option(index: i64) -> Option { - if index == -1 { - None - } else { - // If this fails, then likely because we overlooked a negative value. - let index_usize = index - .try_into() - .unwrap_or_else(|_| panic!("unexpected index {index} returned from Godot function")); - - Some(index_usize) - } -} - // ---------------------------------------------------------------------------------------------------------------------------------------------- // Padding, alignment and precision support diff --git a/godot-core/src/builtin/string/string_macros.rs b/godot-core/src/builtin/string/string_macros.rs index 73035e004..f5fb5f403 100644 --- a/godot-core/src/builtin/string/string_macros.rs +++ b/godot-core/src/builtin/string/string_macros.rs @@ -399,7 +399,7 @@ macro_rules! impl_shared_string_api { } }; - super::found_to_option(godot_found) + sys::found_to_option(godot_found) } } diff --git a/godot-ffi/src/toolbox.rs b/godot-ffi/src/toolbox.rs index 0b8c11f2a..b6c6f613e 100644 --- a/godot-ffi/src/toolbox.rs +++ b/godot-ffi/src/toolbox.rs @@ -195,6 +195,20 @@ pub fn i64_to_ordering(value: i64) -> std::cmp::Ordering { } } +/// Converts a Godot "found" index `Option`, where -1 is mapped to `None`. +pub fn found_to_option(index: i64) -> Option { + if index == -1 { + None + } else { + // If this fails, then likely because we overlooked a negative value. + let index_usize = index + .try_into() + .unwrap_or_else(|_| panic!("unexpected index {index} returned from Godot function")); + + Some(index_usize) + } +} + /* pub fn unqualified_type_name() -> &'static str { let type_name = std::any::type_name::(); diff --git a/itest/rust/src/builtin_tests/containers/array_test.rs b/itest/rust/src/builtin_tests/containers/array_test.rs index 7e8e76fbe..94c55bc8e 100644 --- a/itest/rust/src/builtin_tests/containers/array_test.rs +++ b/itest/rust/src/builtin_tests/containers/array_test.rs @@ -554,18 +554,11 @@ fn array_bsearch_by() { } #[itest] -fn array_bsearch_custom() { +fn array_fops_bsearch_custom() { let a = array![5, 4, 2, 1]; let func = backwards_sort_callable(); - assert_eq!(a.bsearch_custom(1, &func), 3); - assert_eq!(a.bsearch_custom(3, &func), 2); -} - -fn backwards_sort_callable() -> Callable { - // No &[&Variant] explicit type in arguments. - Callable::from_fn("sort backwards", |args| { - args[0].to::() > args[1].to::() - }) + assert_eq!(a.functional_ops().bsearch_custom(1, &func), 3); + assert_eq!(a.functional_ops().bsearch_custom(3, &func), 2); } #[itest] @@ -696,6 +689,88 @@ fn array_inner_type() { assert_eq!(subarray.element_type(), primary.element_type()); } +#[itest] +fn array_fops_filter() { + let is_even = is_even_callable(); + + let array = array![1, 2, 3, 4, 5, 6]; + assert_eq!(array.functional_ops().filter(&is_even), array![2, 4, 6]); +} + +#[itest] +fn array_fops_map() { + let f = Callable::from_fn("round", |args| args[0].to::().round() as i64); + + let array = array![0.7, 1.0, 1.3, 1.6]; + let result = array.functional_ops().map(&f); + + assert_eq!(result, varray![1, 1, 1, 2]); +} + +#[itest] +fn array_fops_reduce() { + let f = Callable::from_fn("sum", |args| args[0].to::() + args[1].to::()); + + let array = array![1, 2, 3, 4]; + let result = array.functional_ops().reduce(&f, &0.to_variant()); + + assert_eq!(result.to::(), 10); +} + +#[itest] +fn array_fops_any() { + let is_even = is_even_callable(); + + assert!(array![1, 2, 3].functional_ops().any(&is_even)); + assert!(!array![1, 3, 5].functional_ops().any(&is_even)); +} + +#[itest] +fn array_fops_all() { + let is_even = is_even_callable(); + + assert!(!array![1, 2, 3].functional_ops().all(&is_even)); + assert!(array![2, 4, 6].functional_ops().all(&is_even)); +} + +#[itest] +#[cfg(since_api = "4.4")] +fn array_fops_find_custom() { + let is_even = is_even_callable(); + + let array = array![1, 2, 3, 4, 5]; + assert_eq!(array.functional_ops().find_custom(&is_even, None), Some(1)); + + let array = array![1, 3, 5]; + assert_eq!(array.functional_ops().find_custom(&is_even, None), None); +} + +#[itest] +#[cfg(since_api = "4.4")] +fn array_fops_rfind_custom() { + let is_even = is_even_callable(); + + let array = array![1, 2, 3, 4, 5]; + assert_eq!(array.functional_ops().rfind_custom(&is_even, None), Some(3)); + + let array = array![1, 3, 5]; + assert_eq!(array.functional_ops().rfind_custom(&is_even, None), None); +} + +// ---------------------------------------------------------------------------------------------------------------------------------------------- +// Helper functions for creating callables. + +fn backwards_sort_callable() -> Callable { + // No &[&Variant] explicit type in arguments. + Callable::from_fn("sort backwards", |args| { + args[0].to::() > args[1].to::() + }) +} + +fn is_even_callable() -> Callable { + Callable::from_fn("is even", |args| args[0].to::() % 2 == 0) +} + // ---------------------------------------------------------------------------------------------------------------------------------------------- // Class definitions From c82104f51f1bca9380acf195e3f7270ae4467111 Mon Sep 17 00:00:00 2001 From: Yarvin Date: Sun, 2 Nov 2025 07:54:29 +0100 Subject: [PATCH 53/54] Update `runtime_version` to use non-deprecated get_godot_version pointer. --- godot-ffi/src/interface_init.rs | 16 ++++++++-------- godot-ffi/src/lib.rs | 24 +++++++++++++++++++++--- godot-ffi/src/toolbox.rs | 2 +- 3 files changed, 30 insertions(+), 12 deletions(-) diff --git a/godot-ffi/src/interface_init.rs b/godot-ffi/src/interface_init.rs index 6ea170e0e..b7b69ed71 100644 --- a/godot-ffi/src/interface_init.rs +++ b/godot-ffi/src/interface_init.rs @@ -71,7 +71,7 @@ pub fn ensure_static_runtime_compatibility( let minor = unsafe { data_ptr.offset(1).read() }; if minor == 0 { // SAFETY: at this point it's reasonably safe to say that we are indeed dealing with that version struct; read the whole. - let data_ptr = get_proc_address as *const sys::GDExtensionGodotVersion; + let data_ptr = get_proc_address as *const sys::GodotSysVersion; let runtime_version_str = unsafe { read_version_string(&data_ptr.read()) }; panic!( @@ -114,7 +114,7 @@ pub fn ensure_static_runtime_compatibility( pub unsafe fn runtime_version( get_proc_address: sys::GDExtensionInterfaceGetProcAddress, -) -> sys::GDExtensionGodotVersion { +) -> sys::GodotSysVersion { let get_proc_address = get_proc_address.expect("get_proc_address unexpectedly null"); runtime_version_inner(get_proc_address) @@ -125,17 +125,17 @@ unsafe fn runtime_version_inner( get_proc_address: unsafe extern "C" fn( *const std::ffi::c_char, ) -> sys::GDExtensionInterfaceFunctionPtr, -) -> sys::GDExtensionGodotVersion { +) -> sys::GodotSysVersion { // SAFETY: `self.0` is a valid `get_proc_address` pointer. - let get_godot_version = unsafe { get_proc_address(sys::c_str(b"get_godot_version\0")) }; //.expect("get_godot_version unexpectedly null"); + let get_godot_version = unsafe { get_proc_address(sys::c_str(sys::GET_GODOT_VERSION_SYS_STR)) }; //.expect("get_godot_version unexpectedly null"); - // SAFETY: `sys::GDExtensionInterfaceGetGodotVersion` is an `Option` of an `unsafe extern "C"` function pointer. + // SAFETY: `GDExtensionInterfaceGetGodotVersion` is an `Option` of an `unsafe extern "C"` function pointer. let get_godot_version = - crate::unsafe_cast_fn_ptr!(get_godot_version as sys::GDExtensionInterfaceGetGodotVersion); + crate::unsafe_cast_fn_ptr!(get_godot_version as sys::GetGodotSysVersion); - let mut version = std::mem::MaybeUninit::::zeroed(); + let mut version = std::mem::MaybeUninit::::zeroed(); - // SAFETY: `get_proc_address` with "get_godot_version" does return a valid `sys::GDExtensionInterfaceGetGodotVersion` pointer, and since we have a valid + // SAFETY: `get_proc_address` with "get_godot_version" does return a valid `GDExtensionInterfaceGetGodotVersion` pointer, and since we have a valid // `get_proc_address` pointer then it must be callable. unsafe { get_godot_version(version.as_mut_ptr()) }; diff --git a/godot-ffi/src/lib.rs b/godot-ffi/src/lib.rs index 2dd2a5c37..9a866cefa 100644 --- a/godot-ffi/src/lib.rs +++ b/godot-ffi/src/lib.rs @@ -111,10 +111,28 @@ use binding::{ #[cfg(not(wasm_nothreads))] static MAIN_THREAD_ID: ManualInitCell = ManualInitCell::new(); +#[cfg(before_api = "4.5")] +mod version_symbols { + + pub type GodotSysVersion = super::GDExtensionGodotVersion; + pub type GetGodotSysVersion = super::GDExtensionInterfaceGetGodotVersion; + pub const GET_GODOT_VERSION_SYS_STR: &[u8] = b"get_godot_version\0"; +} + +#[cfg(since_api = "4.5")] +mod version_symbols { + pub type GodotSysVersion = super::GDExtensionGodotVersion2; + + pub type GetGodotSysVersion = super::GDExtensionInterfaceGetGodotVersion2; + pub const GET_GODOT_VERSION_SYS_STR: &[u8] = b"get_godot_version2\0"; +} + +use version_symbols::*; + // ---------------------------------------------------------------------------------------------------------------------------------------------- pub struct GdextRuntimeMetadata { - godot_version: GDExtensionGodotVersion, + godot_version: GodotSysVersion, } impl GdextRuntimeMetadata { @@ -122,7 +140,7 @@ impl GdextRuntimeMetadata { /// /// - The `string` field of `godot_version` must not be written to while this struct exists. /// - The `string` field of `godot_version` must be safe to read from while this struct exists. - pub unsafe fn new(godot_version: GDExtensionGodotVersion) -> Self { + pub unsafe fn new(godot_version: GodotSysVersion) -> Self { Self { godot_version } } } @@ -264,7 +282,7 @@ fn safeguards_level_string() -> &'static str { } } -fn print_preamble(version: GDExtensionGodotVersion) { +fn print_preamble(version: GodotSysVersion) { let api_version: &'static str = GdextBuild::godot_static_version_string(); let runtime_version = read_version_string(&version); let safeguards_level = safeguards_level_string(); diff --git a/godot-ffi/src/toolbox.rs b/godot-ffi/src/toolbox.rs index 0b8c11f2a..e79088eea 100644 --- a/godot-ffi/src/toolbox.rs +++ b/godot-ffi/src/toolbox.rs @@ -402,7 +402,7 @@ pub(crate) fn load_utility_function( }) } -pub(crate) fn read_version_string(version_ptr: &sys::GDExtensionGodotVersion) -> String { +pub(crate) fn read_version_string(version_ptr: &sys::GodotSysVersion) -> String { let char_ptr = version_ptr.string; // SAFETY: GDExtensionGodotVersion has the (manually upheld) invariant of a valid string field. From a4641a531229c6134cca3823cf2005d8119ecd15 Mon Sep 17 00:00:00 2001 From: Yarvin Date: Mon, 3 Nov 2025 20:37:06 +0100 Subject: [PATCH 54/54] Special version of cell.rs/guard.rs for goodot-rapier (very experimental) based on previous Lilyz work --- godot-cell/src/cell.rs | 29 ++++++++++-------- godot-cell/src/guards.rs | 66 ++++++++++++++++++---------------------- 2 files changed, 46 insertions(+), 49 deletions(-) diff --git a/godot-cell/src/cell.rs b/godot-cell/src/cell.rs index 3885751ba..7c525e6cc 100644 --- a/godot-cell/src/cell.rs +++ b/godot-cell/src/cell.rs @@ -10,7 +10,6 @@ use std::error::Error; use std::marker::PhantomPinned; use std::pin::Pin; use std::ptr::NonNull; -use std::sync::Mutex; use crate::borrow_state::BorrowState; use crate::guards::{InaccessibleGuard, MutGuard, RefGuard}; @@ -77,7 +76,7 @@ impl GdCell { #[derive(Debug)] pub(crate) struct GdCellInner { /// The mutable state of this cell. - pub(crate) state: Mutex>, + pub(crate) state: UnsafeCell>, /// The actual value we're handing out references to, uses `UnsafeCell` as we're passing out `&mut` /// references to its contents even when we only have a `&` reference to the cell. value: UnsafeCell, @@ -89,12 +88,12 @@ impl GdCellInner { /// Creates a new cell storing `value`. pub fn new(value: T) -> Pin> { let cell = Box::pin(Self { - state: Mutex::new(CellState::new()), + state: UnsafeCell::new(CellState::new()), value: UnsafeCell::new(value), _pin: PhantomPinned, }); - cell.state.lock().unwrap().initialize_ptr(&cell.value); + unsafe { (&mut *cell.state.get()).initialize_ptr(&cell.value) } cell } @@ -103,20 +102,26 @@ impl GdCellInner { /// /// Fails if an accessible mutable reference exists. pub fn borrow(self: Pin<&Self>) -> Result, Box> { - let mut state = self.state.lock().unwrap(); - state.borrow_state.increment_shared()?; + { + let state = unsafe { &mut *self.state.get() }; + state.borrow_state.increment_shared()?; + } + + let state = unsafe { &*self.state.get() }; + let value = state.get_ptr(); // SAFETY: `increment_shared` succeeded, therefore there cannot currently be any accessible mutable // references. - unsafe { Ok(RefGuard::new(&self.get_ref().state, state.get_ptr())) } + unsafe { Ok(RefGuard::new(self.state.get(), value)) } } /// Returns a new mutable reference to the contents of the cell. /// /// Fails if an accessible mutable reference exists, or a shared reference exists. pub fn borrow_mut(self: Pin<&Self>) -> Result, Box> { - let mut state = self.state.lock().unwrap(); + let state = unsafe { &mut *self.state.get() }; state.borrow_state.increment_mut()?; + let count = state.borrow_state.mut_count(); let value = state.get_ptr(); @@ -131,7 +136,7 @@ impl GdCellInner { // If `make_inaccessible` is called and succeeds, then a mutable reference from this guard is passed // in. In which case, we cannot use this guard again until the resulting inaccessible guard is // dropped. - unsafe { Ok(MutGuard::new(&self.get_ref().state, count, value)) } + unsafe { Ok(MutGuard::new(state, count, value)) } } /// Make the current mutable borrow inaccessible, thus freeing the value up to be reborrowed again. @@ -144,7 +149,7 @@ impl GdCellInner { self: Pin<&'cell Self>, current_ref: &'val mut T, ) -> Result, Box> { - InaccessibleGuard::new(&self.get_ref().state, current_ref) + InaccessibleGuard::new(self.state.get(), current_ref) } /// Returns `true` if there are any mutable or shared references, regardless of whether the mutable @@ -157,14 +162,14 @@ impl GdCellInner { /// cell hands out a new borrow before it is destroyed. So we still need to ensure that this cannot /// happen at the same time. pub fn is_currently_bound(self: Pin<&Self>) -> bool { - let state = self.state.lock().unwrap(); + let state = unsafe { &*self.state.get() }; state.borrow_state.shared_count() > 0 || state.borrow_state.mut_count() > 0 } /// Similar to [`Self::is_currently_bound`] but only counts mutable references and ignores shared references. pub(crate) fn is_currently_mutably_bound(self: Pin<&Self>) -> bool { - let state = self.state.lock().unwrap(); + let state = unsafe { &*self.state.get() }; state.borrow_state.mut_count() > 0 } diff --git a/godot-cell/src/guards.rs b/godot-cell/src/guards.rs index c38661c70..ddd90e354 100644 --- a/godot-cell/src/guards.rs +++ b/godot-cell/src/guards.rs @@ -4,10 +4,9 @@ * 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::marker::PhantomData; use std::ops::{Deref, DerefMut}; use std::ptr::NonNull; -use std::sync::{Mutex, MutexGuard}; use crate::cell::CellState; @@ -19,10 +18,11 @@ use crate::cell::CellState; #[derive(Debug)] pub struct RefGuard<'a, T> { /// The current state of borrows to the borrowed value. - state: &'a Mutex>, + state: *mut CellState, /// A pointer to the borrowed value. value: NonNull, + _phantoms: PhantomData<&'a ()>, } impl<'a, T> RefGuard<'a, T> { @@ -40,8 +40,12 @@ impl<'a, T> RefGuard<'a, T> { /// /// These conditions ensure that it is safe to call [`as_ref()`](NonNull::as_ref) on `value` for as long /// as the returned guard exists. - pub(crate) unsafe fn new(state: &'a Mutex>, value: NonNull) -> Self { - Self { state, value } + pub(crate) unsafe fn new(state: *mut CellState, value: NonNull) -> Self { + Self { + state, + value, + _phantoms: PhantomData, + } } } @@ -56,8 +60,7 @@ impl Deref for RefGuard<'_, T> { impl Drop for RefGuard<'_, T> { fn drop(&mut self) { - self.state - .lock() + unsafe { self.state.as_mut() } .unwrap() .borrow_state .decrement_shared() @@ -74,7 +77,7 @@ impl Drop for RefGuard<'_, T> { /// reference handed out by this guard. #[derive(Debug)] pub struct MutGuard<'a, T> { - state: &'a Mutex>, + state: &'a mut CellState, count: usize, value: NonNull, } @@ -109,11 +112,7 @@ impl<'a, T> MutGuard<'a, T> { /// prevent any new references from being made. /// - When it is made inaccessible, [`GdCell`](super::GdCell) will also ensure that any new references /// are derived from this guard's `value` pointer, thus preventing `value` from being invalidated. - pub(crate) unsafe fn new( - state: &'a Mutex>, - count: usize, - value: NonNull, - ) -> Self { + pub(crate) unsafe fn new(state: &'a mut CellState, count: usize, value: NonNull) -> Self { Self { state, count, @@ -126,7 +125,7 @@ impl Deref for MutGuard<'_, T> { type Target = T; fn deref(&self) -> &Self::Target { - let count = self.state.lock().unwrap().borrow_state.mut_count(); + let count = self.state.borrow_state.mut_count(); // This is just a best-effort error check. It should never be triggered. assert_eq!( self.count, @@ -152,7 +151,7 @@ impl Deref for MutGuard<'_, T> { impl DerefMut for MutGuard<'_, T> { fn deref_mut(&mut self) -> &mut Self::Target { - let count = self.state.lock().unwrap().borrow_state.mut_count(); + let count = self.state.borrow_state.mut_count(); // This is just a best-effort error check. It should never be triggered. assert_eq!( self.count, @@ -179,12 +178,7 @@ impl DerefMut for MutGuard<'_, T> { impl Drop for MutGuard<'_, T> { fn drop(&mut self) { - self.state - .lock() - .unwrap() - .borrow_state - .decrement_mut() - .unwrap(); + self.state.borrow_state.decrement_mut().unwrap(); } } @@ -199,9 +193,10 @@ impl Drop for MutGuard<'_, T> { /// is dropped, it resets the state to what it was before, as if this guard never existed. #[derive(Debug)] pub struct InaccessibleGuard<'a, T> { - state: &'a Mutex>, + state: *mut CellState, stack_depth: usize, prev_ptr: NonNull, + _phantoms: PhantomData<&'a ()>, } impl<'a, T> InaccessibleGuard<'a, T> { @@ -216,15 +211,15 @@ impl<'a, T> InaccessibleGuard<'a, T> { /// - There are any shared references. /// - `new_ref` is not equal to the pointer in `state`. pub(crate) fn new<'b>( - state: &'a Mutex>, + state: *mut CellState, new_ref: &'b mut T, ) -> Result> where 'a: 'b, { - let mut guard = state.lock().unwrap(); + let cell_state = unsafe { state.as_mut() }.unwrap(); - let current_ptr = guard.get_ptr(); + let current_ptr = cell_state.get_ptr(); let new_ptr = NonNull::from(new_ref); if current_ptr != new_ptr { @@ -232,23 +227,21 @@ impl<'a, T> InaccessibleGuard<'a, T> { return Err("wrong reference passed in".into()); } - guard.borrow_state.set_inaccessible()?; - let prev_ptr = guard.get_ptr(); - let stack_depth = guard.push_ptr(new_ptr); + cell_state.borrow_state.set_inaccessible()?; + let prev_ptr = cell_state.get_ptr(); + let stack_depth = cell_state.push_ptr(new_ptr); Ok(Self { state, stack_depth, prev_ptr, + _phantoms: PhantomData, }) } /// Single implementation of drop-logic for use in both drop implementations. - fn perform_drop( - mut state: MutexGuard<'_, CellState>, - prev_ptr: NonNull, - stack_depth: usize, - ) { + fn perform_drop(state: *mut CellState, prev_ptr: NonNull, stack_depth: usize) { + let state = unsafe { state.as_mut() }.unwrap(); if state.stack_depth != stack_depth { state .borrow_state @@ -266,11 +259,11 @@ impl<'a, T> InaccessibleGuard<'a, T> { #[doc(hidden)] pub fn try_drop(self) -> Result<(), std::mem::ManuallyDrop> { let manual = std::mem::ManuallyDrop::new(self); - let state = manual.state.lock().unwrap(); + let state = unsafe { manual.state.as_mut() }.unwrap(); if !state.borrow_state.may_unset_inaccessible() || state.stack_depth != manual.stack_depth { return Err(manual); } - Self::perform_drop(state, manual.prev_ptr, manual.stack_depth); + Self::perform_drop(manual.state, manual.prev_ptr, manual.stack_depth); Ok(()) } @@ -280,7 +273,6 @@ impl Drop for InaccessibleGuard<'_, T> { fn drop(&mut self) { // Default behavior of drop-logic simply panics and poisons the cell on failure. This is appropriate // for single-threaded code where no errors should happen here. - let state = self.state.lock().unwrap(); - Self::perform_drop(state, self.prev_ptr, self.stack_depth); + Self::perform_drop(self.state, self.prev_ptr, self.stack_depth); } }