diff --git a/godot-core/src/obj/gd.rs b/godot-core/src/obj/gd.rs index eb13884a9..81a2cc9aa 100644 --- a/godot-core/src/obj/gd.rs +++ b/godot-core/src/obj/gd.rs @@ -12,7 +12,9 @@ use std::ptr; use godot_ffi as sys; use godot_ffi::VariantType; use sys::types::OpaqueObject; -use sys::{ffi_methods, interface_fn, static_assert_eq_size, GodotFfi, PtrcallType}; +use sys::{ + ffi_methods, interface_fn, static_assert_eq_size, GodotFfi, GodotNullablePtr, PtrcallType, +}; use crate::builtin::meta::{ClassName, VariantMetadata}; use crate::builtin::{ @@ -569,6 +571,11 @@ where } } +// SAFETY: +// `Gd` will only contain types that inherit from `crate::engine::Object`. +// Godots `Object in turn is known to be nullable and always a pointer. +unsafe impl GodotNullablePtr for Gd {} + impl Gd { /// Runs `init_fn` on the address of a pointer (initialized to null). If that pointer is still null after the `init_fn` call, /// then `None` will be returned; otherwise `Gd::from_obj_sys(ptr)`. @@ -706,6 +713,25 @@ impl ToVariant for Gd { } } +impl ToVariant for Option> { + fn to_variant(&self) -> Variant { + match self { + Some(gd) => gd.to_variant(), + None => Variant::nil(), + } + } +} + +impl FromVariant for Option> { + fn try_from_variant(variant: &Variant) -> Result { + if variant.is_nil() { + Ok(None) + } else { + Gd::try_from_variant(variant).map(Some) + } + } +} + impl PartialEq for Gd { /// ⚠️ Returns whether two `Gd` pointers point to the same object. /// diff --git a/godot-ffi/src/godot_ffi.rs b/godot-ffi/src/godot_ffi.rs index 127bc2af6..edc6c85c9 100644 --- a/godot-ffi/src/godot_ffi.rs +++ b/godot-ffi/src/godot_ffi.rs @@ -4,8 +4,8 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -use crate as sys; -use std::fmt::Debug; +use crate::{self as sys, ptr_then}; +use std::{fmt::Debug, ptr}; /// Adds methods to convert from and to Godot FFI pointers. /// See [crate::ffi_methods] for ergonomic implementation. @@ -94,6 +94,53 @@ pub unsafe trait GodotFfi { unsafe fn move_return_ptr(self, dst: sys::GDExtensionTypePtr, call_type: PtrcallType); } +/// Marks a type as having a nullable counterpart in Godot. +/// +/// This trait primarily exists to implement GodotFfi for `Option>`, which is not possible +/// due to Rusts orphan rule. The rule also enforces better API design, though. `godot_ffi` should +/// not concern itself with the details of how Godot types work and merely defines the FFI abstraction. +/// By having a marker trait for nullable types, we can provide a generic implementation for +/// compatible types, without knowing their definition. +/// +/// # Safety +/// +/// The type has to have a pointer-sized counterpart in Godot, which needs to be nullable. +/// So far, this only applies to class types (Object hierarchy). +pub unsafe trait GodotNullablePtr: GodotFfi {} + +unsafe impl GodotFfi for Option +where + T: GodotNullablePtr, +{ + fn sys(&self) -> sys::GDExtensionTypePtr { + match self { + Some(value) => value.sys(), + None => ptr::null_mut() as sys::GDExtensionTypePtr, + } + } + + unsafe fn from_sys(ptr: sys::GDExtensionTypePtr) -> Self { + ptr_then(ptr, |ptr| T::from_sys(ptr)) + } + + unsafe fn from_sys_init(init_fn: impl FnOnce(sys::GDExtensionTypePtr)) -> Self { + let mut raw = std::mem::MaybeUninit::uninit(); + init_fn(raw.as_mut_ptr() as sys::GDExtensionTypePtr); + + Self::from_sys(raw.assume_init()) + } + + unsafe fn from_arg_ptr(ptr: sys::GDExtensionTypePtr, call_type: PtrcallType) -> Self { + ptr_then(ptr, |ptr| T::from_arg_ptr(ptr, call_type)) + } + + unsafe fn move_return_ptr(self, ptr: sys::GDExtensionTypePtr, call_type: PtrcallType) { + if let Some(value) = self { + value.move_return_ptr(ptr, call_type) + } + } +} + /// An indication of what type of pointer call is being made. #[derive(Default, Copy, Clone, Eq, PartialEq, Debug)] pub enum PtrcallType { diff --git a/godot-ffi/src/lib.rs b/godot-ffi/src/lib.rs index fa94018cb..be7747df1 100644 --- a/godot-ffi/src/lib.rs +++ b/godot-ffi/src/lib.rs @@ -28,7 +28,7 @@ mod plugins; #[doc(hidden)] pub use paste; -pub use crate::godot_ffi::{GodotFfi, GodotFuncMarshal, PtrcallType}; +pub use crate::godot_ffi::{GodotFfi, GodotFuncMarshal, GodotNullablePtr, PtrcallType}; pub use gen::central::*; pub use gen::gdextension_interface::*; diff --git a/itest/godot/ManualFfiTests.gd b/itest/godot/ManualFfiTests.gd index 398f2684e..936eb0a75 100644 --- a/itest/godot/ManualFfiTests.gd +++ b/itest/godot/ManualFfiTests.gd @@ -149,4 +149,88 @@ func test_refcounted_as_object_return_from_user_func_ptrcall(): func test_custom_constructor(): var obj = CustomConstructor.construct_object(42) assert_eq(obj.val, 42) - obj.free() \ No newline at end of file + obj.free() + +func test_option_refcounted_none_varcall(): + var ffi := OptionFfiTest.new() + + var from_rust: Variant = ffi.return_option_refcounted_none() + assert_that(ffi.accept_option_refcounted_none(from_rust), "ffi.accept_option_refcounted_none(from_rust)") + + var from_gdscript: Variant = null + var mirrored: Variant = ffi.mirror_option_refcounted(from_gdscript) + assert_eq(mirrored, from_gdscript, "mirrored == from_gdscript") + +func test_option_refcounted_none_ptrcall(): + var ffi := OptionFfiTest.new() + + var from_rust: Object = ffi.return_option_refcounted_none() + assert_that(ffi.accept_option_refcounted_none(from_rust), "ffi.accept_option_refcounted_none(from_rust)") + + var from_gdscript: Object = null + var mirrored: Object = ffi.mirror_option_refcounted(from_gdscript) + assert_eq(mirrored, from_gdscript, "mirrored == from_gdscript") + +func test_option_refcounted_some_varcall(): + var ffi := OptionFfiTest.new() + + var from_rust: Variant = ffi.return_option_refcounted_some() + assert_that(ffi.accept_option_refcounted_some(from_rust), "ffi.accept_option_refcounted_some(from_rust)") + + var from_gdscript: Variant = RefCounted.new() + var mirrored: Variant = ffi.mirror_option_refcounted(from_gdscript) + assert_eq(mirrored, from_gdscript, "mirrored == from_gdscript") + +func test_option_refcounted_some_ptrcall(): + var ffi := OptionFfiTest.new() + + var from_rust: Object = ffi.return_option_refcounted_some() + assert_that(ffi.accept_option_refcounted_some(from_rust), "ffi.accept_option_refcounted_some(from_rust)") + + var from_gdscript: Object = RefCounted.new() + var mirrored: Object = ffi.mirror_option_refcounted(from_gdscript) + assert_eq(mirrored, from_gdscript, "mirrored == from_gdscript") + +func test_option_node_none_varcall(): + var ffi := OptionFfiTest.new() + + var from_rust: Variant = ffi.return_option_node_none() + assert_that(ffi.accept_option_node_none(from_rust), "ffi.accept_option_node_none(from_rust)") + + var from_gdscript: Variant = null + var mirrored: Variant = ffi.mirror_option_node(from_gdscript) + assert_eq(mirrored, from_gdscript, "mirrored == from_gdscript") + +func test_option_node_none_ptrcall(): + var ffi := OptionFfiTest.new() + + var from_rust: Node = ffi.return_option_node_none() + assert_that(ffi.accept_option_node_none(from_rust), "ffi.accept_option_node_none(from_rust)") + + var from_gdscript: Node = null + var mirrored: Node = ffi.mirror_option_node(from_gdscript) + assert_eq(mirrored, from_gdscript, "mirrored == from_gdscript") + +func test_option_node_some_varcall(): + var ffi := OptionFfiTest.new() + + var from_rust: Variant = ffi.return_option_node_some() + assert_that(ffi.accept_option_node_some(from_rust), "ffi.accept_option_node_some(from_rust)") + + var from_gdscript: Variant = Node.new() + var mirrored: Variant = ffi.mirror_option_node(from_gdscript) + assert_eq(mirrored, from_gdscript, "mirrored == from_gdscript") + from_gdscript.free() + from_rust.free() + +func test_option_node_some_ptrcall(): + var ffi := OptionFfiTest.new() + + var from_rust: Node = ffi.return_option_node_some() + assert_that(ffi.accept_option_node_some(from_rust), "ffi.accept_option_node_some(from_rust)") + + var from_gdscript: Node = Node.new() + var mirrored: Node = ffi.mirror_option_node(from_gdscript) + assert_eq(mirrored, from_gdscript, "mirrored == from_gdscript") + from_gdscript.free() + from_rust.free() \ No newline at end of file diff --git a/itest/rust/src/lib.rs b/itest/rust/src/lib.rs index 652528406..55f8eb8c0 100644 --- a/itest/rust/src/lib.rs +++ b/itest/rust/src/lib.rs @@ -23,6 +23,7 @@ mod gdscript_ffi_test; mod init_test; mod node_test; mod object_test; +mod option_ffi_test; mod packed_array_test; mod projection_test; mod quaternion_test; diff --git a/itest/rust/src/option_ffi_test.rs b/itest/rust/src/option_ffi_test.rs new file mode 100644 index 000000000..46ea2402e --- /dev/null +++ b/itest/rust/src/option_ffi_test.rs @@ -0,0 +1,87 @@ +/* + * 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::prelude::{godot_api, Gd, GodotClass, Node, Object, RefCounted}; +use godot::sys::GodotFfi; + +use crate::itest; + +#[itest] +fn option_some_sys_conversion() { + let v = Some(Object::new_alloc()); + let ptr = v.sys(); + + let v2 = unsafe { Option::>::from_sys(ptr) }; + assert_eq!(v2, v); + + v.unwrap().free(); +} + +#[itest] +fn option_none_sys_conversion() { + let v = None; + let ptr = v.sys(); + + let v2 = unsafe { Option::>::from_sys(ptr) }; + assert_eq!(v2, v); +} + +#[derive(GodotClass, Debug)] +#[class(base = RefCounted, init)] +struct OptionFfiTest; + +#[godot_api] +impl OptionFfiTest { + #[func] + fn return_option_refcounted_none(&self) -> Option> { + None + } + + #[func] + fn accept_option_refcounted_none(&self, value: Option>) -> bool { + value.is_none() + } + + #[func] + fn return_option_refcounted_some(&self) -> Option> { + Some(RefCounted::new()) + } + + #[func] + fn accept_option_refcounted_some(&self, value: Option>) -> bool { + value.is_some() + } + + #[func] + fn mirror_option_refcounted(&self, value: Option>) -> Option> { + value + } + + #[func] + fn return_option_node_none(&self) -> Option> { + None + } + + #[func] + fn accept_option_node_none(&self, value: Option>) -> bool { + value.is_none() + } + + #[func] + fn return_option_node_some(&self) -> Option> { + Some(Node::new_alloc()) + } + + #[func] + fn accept_option_node_some(&self, value: Option>) -> bool { + value.is_some() + } + + #[func] + fn mirror_option_node(&self, value: Option>) -> Option> { + value + } +}