diff --git a/godot-core/src/obj/as_object_arg.rs b/godot-core/src/obj/as_object_arg.rs index 459206b81..d2b5ac6f6 100644 --- a/godot-core/src/obj/as_object_arg.rs +++ b/godot-core/src/obj/as_object_arg.rs @@ -18,15 +18,18 @@ use std::ptr; /// This trait is implemented for the following types: /// - [`Gd`] and `&Gd`, to pass objects. Subclasses of `T` are explicitly supported. /// - [`Option>`] and `Option<&Gd>`, to pass optional objects. `None` is mapped to a null argument. +/// - [`NullArg`], to pass `null` arguments without using `Option`. /// +/// # Nullability ///
-/// The GDExtension API does not provide information about nullability of its function parameters. It is up to you to verify that the arguments -/// you pass are only null when this is allowed. Doing this wrong _should_ be safe, but can lead to the function call failing. +/// The GDExtension API does not inform about nullability of its function parameters. It is up to you to verify that the arguments you pass +/// are only null when this is allowed. Doing this wrong should be safe, but can lead to the function call failing. ///
pub trait AsObjectArg where T: GodotClass + Bounds, { + #[doc(hidden)] fn as_object_arg(&self) -> ObjectArg; } @@ -50,15 +53,52 @@ where } } -// impl AsObjectArg for Option -// where -// T: GodotClass + Bounds, -// U: AsObjectArg, -// { -// fn as_object_arg(&self) -> ObjectArg { -// self.as_ref().map_or_else(ObjectArg::null, AsObjectArg::as_object_arg) -// } -// } +impl AsObjectArg for Option +where + T: GodotClass + Bounds, + U: AsObjectArg, +{ + fn as_object_arg(&self) -> ObjectArg { + self.as_ref() + .map_or_else(ObjectArg::null, AsObjectArg::as_object_arg) + } +} + +impl AsObjectArg for NullArg +where + T: GodotClass + Bounds, +{ + fn as_object_arg(&self) -> ObjectArg { + ObjectArg::null() + } +} + +// ---------------------------------------------------------------------------------------------------------------------------------------------- + +/// Represents `null` when passing an object argument to Godot. +/// +/// This can be used whenever a Godot signature accepts [`AsObjectArg`]. +/// Using `NullArg` is equivalent to passing `Option::>::None`, but less wordy. +/// +/// This expression is only intended for function argument lists. To work with objects that can be null, use `Option>` instead. +/// +/// For APIs that accept `Variant`, you can pass [`Variant::nil()`] instead. +/// +/// # Nullability +///
+/// The GDExtension API does not inform about nullability of its function parameters. It is up to you to verify that the arguments you pass +/// are only null when this is allowed. Doing this wrong should be safe, but can lead to the function call failing. +///
+/// +/// # Example +/// ```no_run +/// # fn some_shape() -> Gd { unimplemented!() } +/// use godot::prelude::*; +/// use godot_core::classes::GltfPhysicsShape; +/// +/// let mut shape: Gd = some_shape(); +/// shape.set_importer_mesh(NullArg); +pub struct NullArg; // ---------------------------------------------------------------------------------------------------------------------------------------------- diff --git a/godot-core/src/obj/gd.rs b/godot-core/src/obj/gd.rs index 6e2653549..fd25028ab 100644 --- a/godot-core/src/obj/gd.rs +++ b/godot-core/src/obj/gd.rs @@ -31,7 +31,8 @@ use crate::{classes, out}; /// This smart pointer can only hold _objects_ in the Godot sense: instances of Godot classes (`Node`, `RefCounted`, etc.) /// or user-declared structs (declared with `#[derive(GodotClass)]`). It does **not** hold built-in types (`Vector3`, `Color`, `i32`). /// -/// `Gd` never holds null objects. If you need nullability, use `Option>`. +/// `Gd` never holds null objects. If you need nullability, use `Option>`. To pass null objects to engine APIs, use +/// [`NullArg`][crate::obj::NullArg]. /// /// # Memory management /// diff --git a/godot/src/prelude.rs b/godot/src/prelude.rs index baa4e9495..c688d1f71 100644 --- a/godot/src/prelude.rs +++ b/godot/src/prelude.rs @@ -26,7 +26,7 @@ pub use super::global::{ pub use super::tools::{load, save, try_load, try_save, GFile}; pub use super::init::{gdextension, ExtensionLibrary, InitLevel}; -pub use super::obj::{Base, Gd, GdMut, GdRef, GodotClass, Inherits, InstanceId, OnReady}; +pub use super::obj::{Base, Gd, GdMut, GdRef, GodotClass, Inherits, InstanceId, NullArg, OnReady}; // Make trait methods available. pub use super::obj::EngineBitfield as _; diff --git a/itest/rust/src/object_tests/mod.rs b/itest/rust/src/object_tests/mod.rs index ce25ab3eb..3a9d10d48 100644 --- a/itest/rust/src/object_tests/mod.rs +++ b/itest/rust/src/object_tests/mod.rs @@ -12,6 +12,7 @@ mod dynamic_call_test; #[cfg(since_api = "4.3")] mod get_property_list_test; mod init_level_test; +mod object_arg_test; mod object_swap_test; mod object_test; mod onready_test; diff --git a/itest/rust/src/object_tests/object_arg_test.rs b/itest/rust/src/object_tests/object_arg_test.rs new file mode 100644 index 000000000..7f7fff0f0 --- /dev/null +++ b/itest/rust/src/object_tests/object_arg_test.rs @@ -0,0 +1,102 @@ +/* + * 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::builtin::Variant; +use godot::classes::{ClassDb, Node}; +use godot::global; +use godot::obj::{Gd, NewAlloc, NullArg}; + +use crate::framework::itest; +use crate::object_tests::object_test::{user_refc_instance, RefcPayload}; + +#[itest] +fn object_arg_owned() { + with_objects(|manual, refc| { + let db = ClassDb::singleton(); + let a = db.class_set_property(manual, "name".into(), Variant::from("hello")); + let b = db.class_set_property(refc, "value".into(), Variant::from(-123)); + (a, b) + }); +} + +#[itest] +fn object_arg_borrowed() { + with_objects(|manual, refc| { + let db = ClassDb::singleton(); + let a = db.class_set_property(&manual, "name".into(), Variant::from("hello")); + let b = db.class_set_property(&refc, "value".into(), Variant::from(-123)); + (a, b) + }); +} + +#[itest] +fn object_arg_option_owned() { + with_objects(|manual, refc| { + let db = ClassDb::singleton(); + let a = db.class_set_property(Some(manual), "name".into(), Variant::from("hello")); + let b = db.class_set_property(Some(refc), "value".into(), Variant::from(-123)); + (a, b) + }); +} + +#[itest] +fn object_arg_option_borrowed() { + with_objects(|manual, refc| { + let db = ClassDb::singleton(); + let a = db.class_set_property(Some(&manual), "name".into(), Variant::from("hello")); + let b = db.class_set_property(Some(&refc), "value".into(), Variant::from(-123)); + (a, b) + }); +} + +#[itest] +fn object_arg_option_none() { + let manual: Option> = None; + let refc: Option> = None; + + // Will emit errors but should not crash. + let db = ClassDb::singleton(); + let error = db.class_set_property(manual, "name".into(), Variant::from("hello")); + assert_eq!(error, global::Error::ERR_UNAVAILABLE); + + let error = db.class_set_property(refc, "value".into(), Variant::from(-123)); + assert_eq!(error, global::Error::ERR_UNAVAILABLE); +} + +#[itest] +fn object_arg_null_arg() { + // Will emit errors but should not crash. + let db = ClassDb::singleton(); + let error = db.class_set_property(NullArg, "name".into(), Variant::from("hello")); + assert_eq!(error, global::Error::ERR_UNAVAILABLE); + + let error = db.class_set_property(NullArg, "value".into(), Variant::from(-123)); + assert_eq!(error, global::Error::ERR_UNAVAILABLE); +} + +// ---------------------------------------------------------------------------------------------------------------------------------------------- +// Helpers + +fn with_objects(f: F) +where + F: FnOnce(Gd, Gd) -> (global::Error, global::Error), +{ + let manual = Node::new_alloc(); + let refc = user_refc_instance(); + + let manual2 = manual.clone(); + let refc2 = refc.clone(); + + let (a, b) = f(manual, refc); + + assert_eq!(a, global::Error::OK); + assert_eq!(b, global::Error::OK); + assert_eq!(manual2.get_name(), "hello".into()); + assert_eq!(refc2.bind().value, -123); + + manual2.free(); +} diff --git a/itest/rust/src/object_tests/object_test.rs b/itest/rust/src/object_tests/object_test.rs index 00a073b27..bc843dee7 100644 --- a/itest/rust/src/object_tests/object_test.rs +++ b/itest/rust/src/object_tests/object_test.rs @@ -878,7 +878,7 @@ impl ObjPayload { // ---------------------------------------------------------------------------------------------------------------------------------------------- #[inline(never)] // force to move "out of scope", can trigger potential dangling pointer errors -fn user_refc_instance() -> Gd { +pub(super) fn user_refc_instance() -> Gd { let value: i16 = 17943; let user = RefcPayload { value }; Gd::from_object(user) @@ -886,7 +886,8 @@ fn user_refc_instance() -> Gd { #[derive(GodotClass, Eq, PartialEq, Debug)] pub struct RefcPayload { - value: i16, + #[var] + pub(super) value: i16, } #[godot_api]