Skip to content

Commit 1fb9642

Browse files
committed
impl GodotFfi for Option<T> when T is pointer sized and nullable #240
Additionally FromVariant and ToVariant are also implemented for Option<Gd<T>> to satisfy all the requirements for ffi and godot_api.
1 parent bc24100 commit 1fb9642

File tree

6 files changed

+250
-5
lines changed

6 files changed

+250
-5
lines changed

godot-core/src/obj/gd.rs

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,9 @@ use std::ptr;
1212
use godot_ffi as sys;
1313
use godot_ffi::VariantType;
1414
use sys::types::OpaqueObject;
15-
use sys::{ffi_methods, interface_fn, static_assert_eq_size, GodotFfi, PtrcallType};
15+
use sys::{
16+
ffi_methods, interface_fn, static_assert_eq_size, GodotFfi, GodotNullablePtr, PtrcallType,
17+
};
1618

1719
use crate::builtin::meta::{ClassName, VariantMetadata};
1820
use crate::builtin::{
@@ -570,6 +572,11 @@ where
570572
}
571573
}
572574

575+
// SAFETY:
576+
// `Gd<T: GodotClass>` will only contain types that inherit from `crate::engine::Object`.
577+
// Godots `Object` in turn is known to be nullable and always a pointer.
578+
unsafe impl<T: GodotClass> GodotNullablePtr for Gd<T> {}
579+
573580
impl<T: GodotClass> Gd<T> {
574581
/// Runs `init_fn` on the address of a pointer (initialized to null). If that pointer is still null after the `init_fn` call,
575582
/// then `None` will be returned; otherwise `Gd::from_obj_sys(ptr)`.
@@ -754,6 +761,25 @@ impl<T: GodotClass> ToVariant for Gd<T> {
754761
}
755762
}
756763

764+
impl<T: GodotClass> ToVariant for Option<Gd<T>> {
765+
fn to_variant(&self) -> Variant {
766+
match self {
767+
Some(gd) => gd.to_variant(),
768+
None => Variant::nil(),
769+
}
770+
}
771+
}
772+
773+
impl<T: GodotClass> FromVariant for Option<Gd<T>> {
774+
fn try_from_variant(variant: &Variant) -> Result<Self, VariantConversionError> {
775+
if variant.is_nil() {
776+
Ok(None)
777+
} else {
778+
Gd::try_from_variant(variant).map(Some)
779+
}
780+
}
781+
}
782+
757783
impl<T: GodotClass> PartialEq for Gd<T> {
758784
/// ⚠️ Returns whether two `Gd` pointers point to the same object.
759785
///

godot-ffi/src/godot_ffi.rs

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@
44
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
55
*/
66

7-
use crate as sys;
8-
use std::fmt::Debug;
7+
use crate::{self as sys, ptr_then};
8+
use std::{fmt::Debug, ptr};
99

1010
/// Adds methods to convert from and to Godot FFI pointers.
1111
/// See [crate::ffi_methods] for ergonomic implementation.
@@ -96,6 +96,53 @@ pub unsafe trait GodotFfi {
9696
unsafe fn move_return_ptr(self, dst: sys::GDExtensionTypePtr, call_type: PtrcallType);
9797
}
9898

99+
/// Marks a type as having a nullable counterpart in Godot.
100+
///
101+
/// This trait primarily exists to implement GodotFfi for `Option<Gd<T>>`, which is not possible
102+
/// due to Rusts orphan rule. The rule also enforces better API design, though. `godot_ffi` should
103+
/// not concern itself with the details of how Godot types work and merely defines the FFI abstraction.
104+
/// By having a marker trait for nullable types, we can provide a generic implementation for
105+
/// compatible types, without knowing their definition.
106+
///
107+
/// # Safety
108+
///
109+
/// The type has to have a pointer-sized counterpart in Godot, which needs to be nullable.
110+
/// So far, this only applies to class types (Object hierarchy).
111+
pub unsafe trait GodotNullablePtr: GodotFfi {}
112+
113+
unsafe impl<T> GodotFfi for Option<T>
114+
where
115+
T: GodotNullablePtr,
116+
{
117+
fn sys(&self) -> sys::GDExtensionTypePtr {
118+
match self {
119+
Some(value) => value.sys(),
120+
None => ptr::null_mut() as sys::GDExtensionTypePtr,
121+
}
122+
}
123+
124+
unsafe fn from_sys(ptr: sys::GDExtensionTypePtr) -> Self {
125+
ptr_then(ptr, |ptr| T::from_sys(ptr))
126+
}
127+
128+
unsafe fn from_sys_init(init_fn: impl FnOnce(sys::GDExtensionUninitializedTypePtr)) -> Self {
129+
let mut raw = std::mem::MaybeUninit::uninit();
130+
init_fn(raw.as_mut_ptr() as sys::GDExtensionUninitializedTypePtr);
131+
132+
Self::from_sys(raw.assume_init())
133+
}
134+
135+
unsafe fn from_arg_ptr(ptr: sys::GDExtensionTypePtr, call_type: PtrcallType) -> Self {
136+
ptr_then(ptr, |ptr| T::from_arg_ptr(ptr, call_type))
137+
}
138+
139+
unsafe fn move_return_ptr(self, ptr: sys::GDExtensionTypePtr, call_type: PtrcallType) {
140+
if let Some(value) = self {
141+
value.move_return_ptr(ptr, call_type)
142+
}
143+
}
144+
}
145+
99146
/// An indication of what type of pointer call is being made.
100147
#[derive(Default, Copy, Clone, Eq, PartialEq, Debug)]
101148
pub enum PtrcallType {

godot-ffi/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ use std::ffi::CStr;
3232
#[doc(hidden)]
3333
pub use paste;
3434

35-
pub use crate::godot_ffi::{GodotFfi, GodotFuncMarshal, PtrcallType};
35+
pub use crate::godot_ffi::{GodotFfi, GodotFuncMarshal, GodotNullablePtr, PtrcallType};
3636
pub use gen::central::*;
3737
pub use gen::gdextension_interface::*;
3838
pub use gen::interface::*;

itest/godot/ManualFfiTests.gd

Lines changed: 85 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,4 +149,88 @@ func test_refcounted_as_object_return_from_user_func_ptrcall():
149149
func test_custom_constructor():
150150
var obj = CustomConstructor.construct_object(42)
151151
assert_eq(obj.val, 42)
152-
obj.free()
152+
obj.free()
153+
154+
func test_option_refcounted_none_varcall():
155+
var ffi := OptionFfiTest.new()
156+
157+
var from_rust: Variant = ffi.return_option_refcounted_none()
158+
assert_that(ffi.accept_option_refcounted_none(from_rust), "ffi.accept_option_refcounted_none(from_rust)")
159+
160+
var from_gdscript: Variant = null
161+
var mirrored: Variant = ffi.mirror_option_refcounted(from_gdscript)
162+
assert_eq(mirrored, from_gdscript, "mirrored == from_gdscript")
163+
164+
func test_option_refcounted_none_ptrcall():
165+
var ffi := OptionFfiTest.new()
166+
167+
var from_rust: Object = ffi.return_option_refcounted_none()
168+
assert_that(ffi.accept_option_refcounted_none(from_rust), "ffi.accept_option_refcounted_none(from_rust)")
169+
170+
var from_gdscript: Object = null
171+
var mirrored: Object = ffi.mirror_option_refcounted(from_gdscript)
172+
assert_eq(mirrored, from_gdscript, "mirrored == from_gdscript")
173+
174+
func test_option_refcounted_some_varcall():
175+
var ffi := OptionFfiTest.new()
176+
177+
var from_rust: Variant = ffi.return_option_refcounted_some()
178+
assert_that(ffi.accept_option_refcounted_some(from_rust), "ffi.accept_option_refcounted_some(from_rust)")
179+
180+
var from_gdscript: Variant = RefCounted.new()
181+
var mirrored: Variant = ffi.mirror_option_refcounted(from_gdscript)
182+
assert_eq(mirrored, from_gdscript, "mirrored == from_gdscript")
183+
184+
func test_option_refcounted_some_ptrcall():
185+
var ffi := OptionFfiTest.new()
186+
187+
var from_rust: Object = ffi.return_option_refcounted_some()
188+
assert_that(ffi.accept_option_refcounted_some(from_rust), "ffi.accept_option_refcounted_some(from_rust)")
189+
190+
var from_gdscript: Object = RefCounted.new()
191+
var mirrored: Object = ffi.mirror_option_refcounted(from_gdscript)
192+
assert_eq(mirrored, from_gdscript, "mirrored == from_gdscript")
193+
194+
func test_option_node_none_varcall():
195+
var ffi := OptionFfiTest.new()
196+
197+
var from_rust: Variant = ffi.return_option_node_none()
198+
assert_that(ffi.accept_option_node_none(from_rust), "ffi.accept_option_node_none(from_rust)")
199+
200+
var from_gdscript: Variant = null
201+
var mirrored: Variant = ffi.mirror_option_node(from_gdscript)
202+
assert_eq(mirrored, from_gdscript, "mirrored == from_gdscript")
203+
204+
func test_option_node_none_ptrcall():
205+
var ffi := OptionFfiTest.new()
206+
207+
var from_rust: Node = ffi.return_option_node_none()
208+
assert_that(ffi.accept_option_node_none(from_rust), "ffi.accept_option_node_none(from_rust)")
209+
210+
var from_gdscript: Node = null
211+
var mirrored: Node = ffi.mirror_option_node(from_gdscript)
212+
assert_eq(mirrored, from_gdscript, "mirrored == from_gdscript")
213+
214+
func test_option_node_some_varcall():
215+
var ffi := OptionFfiTest.new()
216+
217+
var from_rust: Variant = ffi.return_option_node_some()
218+
assert_that(ffi.accept_option_node_some(from_rust), "ffi.accept_option_node_some(from_rust)")
219+
220+
var from_gdscript: Variant = Node.new()
221+
var mirrored: Variant = ffi.mirror_option_node(from_gdscript)
222+
assert_eq(mirrored, from_gdscript, "mirrored == from_gdscript")
223+
from_gdscript.free()
224+
from_rust.free()
225+
226+
func test_option_node_some_ptrcall():
227+
var ffi := OptionFfiTest.new()
228+
229+
var from_rust: Node = ffi.return_option_node_some()
230+
assert_that(ffi.accept_option_node_some(from_rust), "ffi.accept_option_node_some(from_rust)")
231+
232+
var from_gdscript: Node = Node.new()
233+
var mirrored: Node = ffi.mirror_option_node(from_gdscript)
234+
assert_eq(mirrored, from_gdscript, "mirrored == from_gdscript")
235+
from_gdscript.free()
236+
from_rust.free()

itest/rust/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ mod init_test;
2525
mod native_structures_test;
2626
mod node_test;
2727
mod object_test;
28+
mod option_ffi_test;
2829
mod packed_array_test;
2930
mod projection_test;
3031
mod quaternion_test;

itest/rust/src/option_ffi_test.rs

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
/*
2+
* This Source Code Form is subject to the terms of the Mozilla Public
3+
* License, v. 2.0. If a copy of the MPL was not distributed with this
4+
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
5+
*/
6+
7+
use godot::prelude::{godot_api, Gd, GodotClass, Node, Object, RefCounted};
8+
use godot::sys::GodotFfi;
9+
10+
use crate::itest;
11+
12+
#[itest]
13+
fn option_some_sys_conversion() {
14+
let v = Some(Object::new_alloc());
15+
let ptr = v.sys();
16+
17+
let v2 = unsafe { Option::<Gd<Object>>::from_sys(ptr) };
18+
assert_eq!(v2, v);
19+
20+
v.unwrap().free();
21+
}
22+
23+
#[itest]
24+
fn option_none_sys_conversion() {
25+
let v = None;
26+
let ptr = v.sys();
27+
28+
let v2 = unsafe { Option::<Gd<Object>>::from_sys(ptr) };
29+
assert_eq!(v2, v);
30+
}
31+
32+
#[derive(GodotClass, Debug)]
33+
#[class(base = RefCounted, init)]
34+
struct OptionFfiTest;
35+
36+
#[godot_api]
37+
impl OptionFfiTest {
38+
#[func]
39+
fn return_option_refcounted_none(&self) -> Option<Gd<RefCounted>> {
40+
None
41+
}
42+
43+
#[func]
44+
fn accept_option_refcounted_none(&self, value: Option<Gd<RefCounted>>) -> bool {
45+
value.is_none()
46+
}
47+
48+
#[func]
49+
fn return_option_refcounted_some(&self) -> Option<Gd<RefCounted>> {
50+
Some(RefCounted::new())
51+
}
52+
53+
#[func]
54+
fn accept_option_refcounted_some(&self, value: Option<Gd<RefCounted>>) -> bool {
55+
value.is_some()
56+
}
57+
58+
#[func]
59+
fn mirror_option_refcounted(&self, value: Option<Gd<RefCounted>>) -> Option<Gd<RefCounted>> {
60+
value
61+
}
62+
63+
#[func]
64+
fn return_option_node_none(&self) -> Option<Gd<Node>> {
65+
None
66+
}
67+
68+
#[func]
69+
fn accept_option_node_none(&self, value: Option<Gd<Node>>) -> bool {
70+
value.is_none()
71+
}
72+
73+
#[func]
74+
fn return_option_node_some(&self) -> Option<Gd<Node>> {
75+
Some(Node::new_alloc())
76+
}
77+
78+
#[func]
79+
fn accept_option_node_some(&self, value: Option<Gd<Node>>) -> bool {
80+
value.is_some()
81+
}
82+
83+
#[func]
84+
fn mirror_option_node(&self, value: Option<Gd<Node>>) -> Option<Gd<Node>> {
85+
value
86+
}
87+
}

0 commit comments

Comments
 (0)