Skip to content

Commit

Permalink
NullArg + tests + documentation
Browse files Browse the repository at this point in the history
  • Loading branch information
Bromeon committed Jul 20, 2024
1 parent 6e007d0 commit 1517613
Show file tree
Hide file tree
Showing 6 changed files with 160 additions and 15 deletions.
62 changes: 51 additions & 11 deletions godot-core/src/obj/as_object_arg.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,18 @@ use std::ptr;
/// This trait is implemented for the following types:
/// - [`Gd<T>`] and `&Gd<T>`, to pass objects. Subclasses of `T` are explicitly supported.
/// - [`Option<Gd<T>>`] and `Option<&Gd<T>>`, to pass optional objects. `None` is mapped to a null argument.
/// - [`NullArg`], to pass `null` arguments without using `Option`.
///
/// # Nullability
/// <div class="warning">
/// 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.
/// </div>
pub trait AsObjectArg<T>
where
T: GodotClass + Bounds<Declarer = bounds::DeclEngine>,
{
#[doc(hidden)]
fn as_object_arg(&self) -> ObjectArg<T>;
}

Expand All @@ -50,15 +53,52 @@ where
}
}

// impl<T, U> AsObjectArg<T> for Option<U>
// where
// T: GodotClass + Bounds<Declarer = bounds::DeclEngine>,
// U: AsObjectArg<T>,
// {
// fn as_object_arg(&self) -> ObjectArg<T> {
// self.as_ref().map_or_else(ObjectArg::null, AsObjectArg::as_object_arg)
// }
// }
impl<T, U> AsObjectArg<T> for Option<U>
where
T: GodotClass + Bounds<Declarer = bounds::DeclEngine>,
U: AsObjectArg<T>,
{
fn as_object_arg(&self) -> ObjectArg<T> {
self.as_ref()
.map_or_else(ObjectArg::null, AsObjectArg::as_object_arg)
}
}

impl<T> AsObjectArg<T> for NullArg
where
T: GodotClass + Bounds<Declarer = bounds::DeclEngine>,
{
fn as_object_arg(&self) -> ObjectArg<T> {
ObjectArg::null()
}
}

// ----------------------------------------------------------------------------------------------------------------------------------------------

/// Represents `null` when passing an object argument to Godot.
///
/// This can be used whenever a Godot signature accepts [`AsObjectArg<T>`].
/// Using `NullArg` is equivalent to passing `Option::<Gd<T>>::None`, but less wordy.
///
/// This expression is only intended for function argument lists. To work with objects that can be null, use `Option<Gd<T>>` instead.
///
/// For APIs that accept `Variant`, you can pass [`Variant::nil()`] instead.
///
/// # Nullability
/// <div class="warning">
/// 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.
/// </div>
///
/// # Example
/// ```no_run
/// # fn some_shape() -> Gd<GltfPhysicsShape> { unimplemented!() }
/// use godot::prelude::*;
/// use godot_core::classes::GltfPhysicsShape;
///
/// let mut shape: Gd<GltfPhysicsShape> = some_shape();
/// shape.set_importer_mesh(NullArg);
pub struct NullArg;

// ----------------------------------------------------------------------------------------------------------------------------------------------

Expand Down
3 changes: 2 additions & 1 deletion godot-core/src/obj/gd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<T>` never holds null objects. If you need nullability, use `Option<Gd<T>>`.
/// `Gd<T>` never holds null objects. If you need nullability, use `Option<Gd<T>>`. To pass null objects to engine APIs, use
/// [`NullArg`][crate::obj::NullArg].
///
/// # Memory management
///
Expand Down
2 changes: 1 addition & 1 deletion godot/src/prelude.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 _;
Expand Down
1 change: 1 addition & 0 deletions itest/rust/src/object_tests/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
102 changes: 102 additions & 0 deletions itest/rust/src/object_tests/object_arg_test.rs
Original file line number Diff line number Diff line change
@@ -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<Gd<Node>> = None;
let refc: Option<Gd<RefcPayload>> = 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: F)
where
F: FnOnce(Gd<Node>, Gd<RefcPayload>) -> (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();
}
5 changes: 3 additions & 2 deletions itest/rust/src/object_tests/object_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -878,15 +878,16 @@ impl ObjPayload {
// ----------------------------------------------------------------------------------------------------------------------------------------------

#[inline(never)] // force to move "out of scope", can trigger potential dangling pointer errors
fn user_refc_instance() -> Gd<RefcPayload> {
pub(super) fn user_refc_instance() -> Gd<RefcPayload> {
let value: i16 = 17943;
let user = RefcPayload { value };
Gd::from_object(user)
}

#[derive(GodotClass, Eq, PartialEq, Debug)]
pub struct RefcPayload {
value: i16,
#[var]
pub(super) value: i16,
}

#[godot_api]
Expand Down

0 comments on commit 1517613

Please sign in to comment.