Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions godot-bindings/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,14 @@ api-custom = ["dep:bindgen", "dep:regex", "dep:which"]
api-custom-json = ["dep:nanoserde", "dep:bindgen", "dep:regex", "dep:which"]
api-custom-extheader = []

debug-checks-fast-unsafe = []
debug-checks-balanced = []
debug-checks-paranoid = []

release-checks-fast-unsafe = []
release-checks-balanced = []
release-checks-paranoid = []

[dependencies]
gdextension-api = { workspace = true }

Expand Down
12 changes: 12 additions & 0 deletions godot-bindings/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,16 @@ fn main() {
// ]]

assert!(count <= 1, "ERROR: at most one `api-*` feature can be enabled");

let mut debug_checks_count=0;
if cfg!(feature="debug-checks-fast-unsafe") {debug_checks_count+=1;}
if cfg!(feature="debug-checks-balanced") {debug_checks_count+=1;}
if cfg!(feature="debug-checks-paranoid") {debug_checks_count+=1;}
assert!(debug_checks_count<=1,"ERROR: at most one `debug-checks-*` feature can be enabled");

let mut release_checks_count=0;
if cfg!(feature="release-checks-fast-unsafe") {release_checks_count+=1;}
if cfg!(feature="release-checks-balanced") {release_checks_count+=1;}
if cfg!(feature="release-checks-paranoid") {release_checks_count+=1;}
assert!(release_checks_count<=1,"ERROR: at most one `release-checks-*` feature can be enabled");
}
25 changes: 25 additions & 0 deletions godot-bindings/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -267,3 +267,28 @@ 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 };
if cfg!(debug_assertions) {
if cfg!(feature = "debug-checks-fast-unsafe") {
checks_level = 0;
} else if cfg!(feature = "debug-checks-balanced") {
checks_level = 1;
} else if cfg!(feature = "debug-checks-paranoid") {
checks_level = 2;
}
} else if cfg!(feature = "release-checks-fast-unsafe") {
checks_level = 0;
} else if cfg!(feature = "release-checks-balanced") {
checks_level = 1;
} else if cfg!(feature = "release-checks-paranoid") {
checks_level = 2;
}

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}""#);
}
}
1 change: 1 addition & 0 deletions godot-codegen/src/generator/native_structures.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
8 changes: 8 additions & 0 deletions godot-core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,14 @@ api-4-3 = ["godot-ffi/api-4-3"]
api-4-4 = ["godot-ffi/api-4-4"]
# ]]

debug-checks-fast-unsafe = ["godot-ffi/debug-checks-fast-unsafe"]
debug-checks-balanced = ["godot-ffi/debug-checks-balanced"]
debug-checks-paranoid = ["godot-ffi/debug-checks-paranoid"]

release-checks-fast-unsafe = ["godot-ffi/release-checks-fast-unsafe"]
release-checks-balanced = ["godot-ffi/release-checks-balanced"]
release-checks-paranoid = ["godot-ffi/release-checks-paranoid"]

[dependencies]
godot-ffi = { path = "../godot-ffi", version = "=0.3.5" }

Expand Down
1 change: 1 addition & 0 deletions godot-core/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,5 @@ fn main() {

godot_bindings::emit_godot_version_cfg();
godot_bindings::emit_wasm_nothreads_cfg();
godot_bindings::emit_checks_mode();
}
6 changes: 3 additions & 3 deletions godot-core/src/builtin/collections/array.rs
Original file line number Diff line number Diff line change
Expand Up @@ -951,7 +951,7 @@ impl<T: ArrayElement> Array<T> {
std::mem::transmute::<&Array<T>, &Array<U>>(self)
}

#[cfg(debug_assertions)]
#[cfg(checks_at_least = "paranoid")]
pub(crate) fn debug_validate_elements(&self) -> Result<(), ConvertError> {
// SAFETY: every element is internally represented as Variant.
let canonical_array = unsafe { self.assume_type_ref::<Variant>() };
Expand All @@ -973,7 +973,7 @@ impl<T: ArrayElement> Array<T> {
}

// 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_elements(&self) -> Result<(), ConvertError> {
Ok(())
}
Expand Down Expand Up @@ -1192,7 +1192,7 @@ impl<T: ArrayElement> Clone for Array<T> {
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 {
Expand Down
11 changes: 6 additions & 5 deletions godot-core/src/classes/class_runtime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::ClassName;
use crate::obj::{bounds, Bounds, Gd, GodotClass, InstanceId, RawGd};
use crate::sys;
Expand Down Expand Up @@ -177,6 +177,7 @@ where
}
}

#[cfg(checks_at_least = "balanced")]
pub(crate) fn ensure_object_alive(
instance_id: InstanceId,
old_object_ptr: sys::GDExtensionObjectPtr,
Expand All @@ -197,7 +198,7 @@ pub(crate) fn ensure_object_alive(
);
}

#[cfg(debug_assertions)]
#[cfg(checks_at_least = "paranoid")]
pub(crate) fn ensure_object_inherits(derived: ClassName, base: ClassName, instance_id: InstanceId) {
if derived == base
|| base == Object::class_name() // for Object base, anything inherits by definition
Expand All @@ -212,7 +213,7 @@ pub(crate) fn ensure_object_inherits(derived: ClassName, base: ClassName, instan
)
}

#[cfg(debug_assertions)]
#[cfg(checks_at_least = "paranoid")]
pub(crate) fn ensure_binding_not_null<T>(binding: sys::GDExtensionClassInstancePtr)
where
T: GodotClass + Bounds<Declarer = bounds::DeclUser>,
Expand Down Expand Up @@ -240,7 +241,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: ClassName, base: ClassName) -> bool {
use std::collections::HashSet;

Expand Down
4 changes: 2 additions & 2 deletions godot-core/src/meta/error/convert_error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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: ArrayTypeInfo, value: i64 },

/// InvalidEnum is also used by bitfields.
Expand Down Expand Up @@ -247,7 +247,7 @@ impl fmt::Display for FromGodotError {
"expected array of class {exp_class}, got array of class {act_class}"
)
}
#[cfg(debug_assertions)]
#[cfg(checks_at_least = "paranoid")]
Self::BadArrayTypeInt { expected, value } => {
write!(
f,
Expand Down
2 changes: 2 additions & 0 deletions godot-core/src/meta/signature.rs
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ impl<Params: OutParamTuple, Ret: FromGodot> Signature<Params, Ret> {
//$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);
}
Expand Down Expand Up @@ -300,6 +301,7 @@ impl<Params: OutParamTuple, Ret: FromGodot> Signature<Params, Ret> {
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);
}
Expand Down
34 changes: 18 additions & 16 deletions godot-core/src/obj/base.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -27,7 +27,7 @@ thread_local! {
}

/// Represents the initialization state of a `Base<T>` 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()`).
Expand All @@ -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)
Expand Down Expand Up @@ -82,7 +82,7 @@ pub struct Base<T: GodotClass> {
/// Tracks the initialization state of this `Base<T>` in Debug mode.
///
/// Rc allows to "copy-construct" the base from an existing one, while still affecting the user-instance through the original `Base<T>`.
#[cfg(debug_assertions)]
#[cfg(checks_at_least = "paranoid")]
init_state: Rc<Cell<InitState>>,
}

Expand All @@ -95,13 +95,14 @@ impl<T: GodotClass> Base<T> {
/// `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<T>` is in use, that constitutes a logic error, not a safety issue.
pub(crate) unsafe fn from_base(base: &Base<T>) -> Base<T> {
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),
}
}
Expand All @@ -114,7 +115,8 @@ impl<T: GodotClass> Base<T> {
/// `gd` must be alive at the time of invocation. If it is destroyed while the returned `Base<T>` is in use, that constitutes a logic
/// error, not a safety issue.
pub(crate) unsafe fn from_script_gd(gd: &Gd<T>) -> 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)
Expand All @@ -141,15 +143,15 @@ impl<T: GodotClass> Base<T> {
base_from_obj!(obj, InitState::ObjectConstructing)
}

#[cfg(debug_assertions)]
#[cfg(checks_at_least = "paranoid")]
fn from_obj(obj: Gd<T>, init_state: InitState) -> Self {
Self {
obj: ManuallyDrop::new(obj),
init_state: Rc::new(Cell::new(init_state)),
}
}

#[cfg(not(debug_assertions))]
#[cfg(not(checks_at_least = "paranoid"))]
fn from_obj(obj: Gd<T>) -> Self {
Self {
obj: ManuallyDrop::new(obj),
Expand Down Expand Up @@ -187,7 +189,7 @@ impl<T: GodotClass> Base<T> {
/// If called outside an initialization function, or for ref-counted objects on a non-main thread.
#[cfg(since_api = "4.2")]
pub fn to_init_gd(&self) -> Gd<T> {
#[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()"
Expand Down Expand Up @@ -253,7 +255,7 @@ impl<T: GodotClass> Base<T> {

/// Finalizes the initialization of this `Base<T>` and returns whether
pub(crate) fn mark_initialized(&mut self) {
#[cfg(debug_assertions)]
#[cfg(checks_at_least = "paranoid")]
{
assert_eq!(
self.init_state.get(),
Expand All @@ -270,7 +272,7 @@ impl<T: GodotClass> Base<T> {
/// Returns a [`Gd`] referencing the base object, assuming the derived object is fully constructed.
#[doc(hidden)]
pub fn __fully_constructed_gd(&self) -> Gd<T> {
#[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()"
Expand Down Expand Up @@ -301,7 +303,7 @@ impl<T: GodotClass> Base<T> {

/// Returns a [`Gd`] referencing the base object, for use in script contexts only.
pub(crate) fn to_script_gd(&self) -> Gd<T> {
#[cfg(debug_assertions)]
#[cfg(checks_at_least = "paranoid")]
assert_eq!(
self.init_state.get(),
InitState::Script,
Expand All @@ -312,15 +314,15 @@ impl<T: GodotClass> Base<T> {
}

/// Returns `true` if this `Base<T>` 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
}

/// Returns a [`Gd`] referencing the base object, assuming the derived object is fully constructed.
#[doc(hidden)]
pub fn __constructed_gd(&self) -> Gd<T> {
#[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()"
Expand Down
7 changes: 5 additions & 2 deletions godot-core/src/obj/casts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,14 +48,15 @@ impl<T: GodotClass, U: GodotClass> CastSuccess<T, U> {
}

/// Access shared reference to destination, without consuming object.
#[cfg(debug_assertions)]
#[cfg(checks_at_least = "paranoid")]
pub fn as_dest_ref(&self) -> &RawGd<U> {
self.check_validity();
&self.dest
}

/// Access exclusive reference to destination, without consuming object.
pub fn as_dest_mut(&mut self) -> &mut RawGd<U> {
#[cfg(checks_at_least = "paranoid")]
self.check_validity();
&mut self.dest
}
Expand All @@ -70,13 +71,15 @@ impl<T: GodotClass, U: GodotClass> CastSuccess<T, U> {
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());
}
}
4 changes: 3 additions & 1 deletion godot-core/src/obj/gd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -698,7 +698,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());
}

Expand Down
Loading
Loading