From a8eb3da78450da856d9029c79c920fbc56c9c132 Mon Sep 17 00:00:00 2001 From: Luo Zhihao Date: Wed, 24 Sep 2025 23:50:39 +0800 Subject: [PATCH] Add cargo features to configure safety checks --- godot-bindings/Cargo.toml | 3 ++ godot-bindings/build.rs | 2 +- godot-bindings/src/lib.rs | 18 ++++++++++ .../src/generator/native_structures.rs | 1 + godot-core/Cargo.toml | 3 ++ godot-core/build.rs | 1 + godot-core/src/builtin/collections/array.rs | 6 ++-- godot-core/src/classes/class_runtime.rs | 11 +++--- godot-core/src/meta/error/convert_error.rs | 4 +-- godot-core/src/meta/signature.rs | 2 ++ godot-core/src/obj/base.rs | 36 ++++++++++--------- godot-core/src/obj/casts.rs | 7 ++-- godot-core/src/obj/gd.rs | 4 ++- godot-core/src/obj/raw_gd.rs | 14 +++++--- godot-core/src/obj/rtti.rs | 6 ++-- godot-core/src/storage/mod.rs | 8 ++--- godot-ffi/Cargo.toml | 4 +++ godot-ffi/build.rs | 1 + godot-ffi/src/lib.rs | 12 +++++-- godot/Cargo.toml | 4 +++ 20 files changed, 102 insertions(+), 45 deletions(-) diff --git a/godot-bindings/Cargo.toml b/godot-bindings/Cargo.toml index aa359d94d..a12c5ca07 100644 --- a/godot-bindings/Cargo.toml +++ b/godot-bindings/Cargo.toml @@ -32,6 +32,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 = [] + [dependencies] gdextension-api = { workspace = true } diff --git a/godot-bindings/build.rs b/godot-bindings/build.rs index 0a9ad0ba9..7eb15475f 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..e4141da29 100644 --- a/godot-bindings/src/lib.rs +++ b/godot-bindings/src/lib.rs @@ -267,3 +267,21 @@ 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") { + checks_level = 0; + } + + for checks in check_modes.iter().take(checks_level + 1) { + println!(r#"cargo:rustc-check-cfg=cfg(checks_at_least, values("{checks}"))"#); + println!(r#"cargo:rustc-cfg=checks_at_least="{checks}""#); + } +} 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 0de1b2d2c..b6c8da55c 100644 --- a/godot-core/Cargo.toml +++ b/godot-core/Cargo.toml @@ -37,6 +37,9 @@ api-4-3 = ["godot-ffi/api-4-3"] api-4-4 = ["godot-ffi/api-4-4"] # ]] +debug-checks-balanced = ["godot-ffi/debug-checks-balanced"] +release-checks-fast = ["godot-ffi/release-checks-fast"] + [dependencies] godot-ffi = { path = "../godot-ffi", version = "=0.3.5" } 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 cdbd5ba5d..31da04a84 100644 --- a/godot-core/src/builtin/collections/array.rs +++ b/godot-core/src/builtin/collections/array.rs @@ -979,7 +979,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::() }; @@ -1001,7 +1001,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(()) } @@ -1244,7 +1244,7 @@ impl Clone for Array { let copy = unsafe { self.clone_unchecked() }; // Double-check copy's runtime type in Debug mode. - if cfg!(debug_assertions) { + if cfg!(checks_at_least = "paranoid") { 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 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 110cb3aa8..4f4b5a6f5 100644 --- a/godot-core/src/meta/error/convert_error.rs +++ b/godot-core/src/meta/error/convert_error.rs @@ -186,7 +186,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, @@ -231,7 +231,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 b55f88028..32918b9d3 100644 --- a/godot-core/src/meta/signature.rs +++ b/godot-core/src/meta/signature.rs @@ -144,6 +144,7 @@ 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. + #[cfg(checks_at_least = "balanced")] if let Some(instance_id) = maybe_instance_id { crate::classes::ensure_object_alive(instance_id, object_ptr, &call_ctx); } @@ -304,6 +305,7 @@ impl Signature { let call_ctx = CallContext::outbound(class_name, method_name); // $crate::out!("out_class_ptrcall: {call_ctx}"); + #[cfg(checks_at_least = "balanced")] 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 d13f7969b..0c0d45054 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), @@ -186,7 +188,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()" @@ -258,7 +260,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(), @@ -275,7 +277,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()" @@ -306,7 +308,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, @@ -318,7 +320,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 } @@ -326,7 +328,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()" @@ -343,7 +345,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 282839f86..16582cc94 100644 --- a/godot-core/src/obj/gd.rs +++ b/godot-core/src/obj/gd.rs @@ -769,7 +769,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()); } diff --git a/godot-core/src/obj/raw_gd.rs b/godot-core/src/obj/raw_gd.rs index ef7d6a913..88608bd23 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"); @@ -383,10 +383,14 @@ impl RawGd { /// Verify that the object is non-null and alive. In Debug 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); + + let instance_id = self.check_dynamic_type(&call_ctx); - 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()`. @@ -509,7 +513,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..acc60d400 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,7 +36,7 @@ impl ObjectRtti { Self { instance_id, - #[cfg(debug_assertions)] + #[cfg(checks_at_least = "paranoid")] class_name: T::class_id(), } } @@ -47,7 +47,7 @@ impl ObjectRtti { /// In Debug mode, if the object is not of type `T` or derived. #[inline] pub fn check_type(&self) -> InstanceId { - #[cfg(debug_assertions)] + #[cfg(checks_at_least = "paranoid")] crate::classes::ensure_object_inherits(self.class_name, T::class_id(), self.instance_id); self.instance_id 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 bbbcabfbc..a85dd6d70 100644 --- a/godot-ffi/Cargo.toml +++ b/godot-ffi/Cargo.toml @@ -29,6 +29,10 @@ api-4-3 = ["godot-bindings/api-4-3"] api-4-4 = ["godot-bindings/api-4-4"] # ]] + +debug-checks-balanced = ["godot-bindings/debug-checks-balanced"] +release-checks-fast = ["godot-bindings/release-checks-fast"] + [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 a1e2e4156..edcc856f9 100644 --- a/godot-ffi/src/lib.rs +++ b/godot-ffi/src/lib.rs @@ -290,8 +290,16 @@ pub unsafe fn deinitialize() { 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 = 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!(); + }; + 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 04bfa4517..54ae18e6e 100644 --- a/godot/Cargo.toml +++ b/godot/Cargo.toml @@ -38,6 +38,10 @@ api-4-3 = ["godot-core/api-4-3"] api-4-4 = ["godot-core/api-4-4"] # ]] + +debug-checks-balanced = ["godot-core/debug-checks-balanced"] +release-checks-fast = ["godot-core/release-checks-fast"] + default = ["__codegen-full"] # Private features, they are under no stability guarantee