Skip to content

Commit 9c7c8da

Browse files
bors[bot]toasteater
and
toasteater
authored
Merge #682
682: Construct Godot API objects from class names r=toasteater a=toasteater This adds the `Ref::<T>::by_class_name` constructor that instantiates a sub-class of `T` by its name. If the resulting object cannot be casted to `T`, it is correctly freed depending on whether it is a `Reference`, and `None` is returned. As with `Ref::cast`, there is the caveat that reference-counted classes cannot be casted to `Object`s, since memory management is determined statically. Co-authored-by: toasteater <[email protected]>
2 parents 6dac63a + 9420ac1 commit 9c7c8da

File tree

5 files changed

+155
-24
lines changed

5 files changed

+155
-24
lines changed

.github/workflows/ci.yml

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -227,13 +227,25 @@ jobs:
227227
cd test;
228228
mkdir -p ./project/lib;
229229
cp ../target/debug/libgdnative_test.so ./project/lib/;
230-
"${{ runner.temp }}/godot_bin/Godot_v${GODOT_VER}-${GODOT_REL}_linux_headless.64" --path ./project/;
231-
"${{ runner.temp }}/godot_bin/Godot_v${GODOT_VER}-${GODOT_REL}_linux_headless.64" -e --path ./project/ --run-editor-tests;
230+
"${{ runner.temp }}/godot_bin/Godot_v${GODOT_VER}-${GODOT_REL}_linux_headless.64" --path ./project/ > >(tee "${{ runner.temp }}/stdout.log");
231+
if grep -q "Leaked instance" "${{ runner.temp }}/stdout.log"; then
232+
exit 1;
233+
fi;
234+
"${{ runner.temp }}/godot_bin/Godot_v${GODOT_VER}-${GODOT_REL}_linux_headless.64" -e --path ./project/ --run-editor-tests > >(tee "${{ runner.temp }}/stdout.log");
235+
if grep -q "Leaked instance" "${{ runner.temp }}/stdout.log"; then
236+
exit 1;
237+
fi;
232238
cargo build --features=type_tag_fallback;
233239
mkdir -p ./project/lib;
234240
cp ../target/debug/libgdnative_test.so ./project/lib/;
235-
"${{ runner.temp }}/godot_bin/Godot_v${GODOT_VER}-${GODOT_REL}_linux_headless.64" --path ./project/;
236-
"${{ runner.temp }}/godot_bin/Godot_v${GODOT_VER}-${GODOT_REL}_linux_headless.64" -e --path ./project/ --run-editor-tests;
241+
"${{ runner.temp }}/godot_bin/Godot_v${GODOT_VER}-${GODOT_REL}_linux_headless.64" --path ./project/ > >(tee "${{ runner.temp }}/stdout.log");
242+
if grep -q "Leaked instance" "${{ runner.temp }}/stdout.log"; then
243+
exit 1;
244+
fi;
245+
"${{ runner.temp }}/godot_bin/Godot_v${GODOT_VER}-${GODOT_REL}_linux_headless.64" -e --path ./project/ --run-editor-tests > >(tee "${{ runner.temp }}/stdout.log");
246+
if grep -q "Leaked instance" "${{ runner.temp }}/stdout.log"; then
247+
exit 1;
248+
fi;
237249
238250
# This job doesn't actually test anything, but they're used to tell bors the
239251
# build completed, as there is no practical way to detect when a workflow is

gdnative-core/src/object.rs

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
use std::borrow::Borrow;
2+
use std::ffi::CString;
23
use std::fmt::{self, Debug};
34
use std::hash::{Hash, Hasher};
45
use std::marker::PhantomData;
56
use std::ops::Deref;
67
use std::ptr::NonNull;
78

8-
use crate::private::{get_api, ReferenceCountedClassPlaceholder};
9+
use crate::private::{get_api, ManuallyManagedClassPlaceholder, ReferenceCountedClassPlaceholder};
910
use crate::ref_kind::{ManuallyManaged, RefCounted, RefKind};
1011
use crate::sys;
1112
use crate::thread_access::{
@@ -322,6 +323,27 @@ impl<T: GodotObject + Instanciable> Ref<T, Unique> {
322323
}
323324
}
324325

326+
impl<T: GodotObject> Ref<T, Unique> {
327+
/// Creates a new instance of a sub-class of `T` by its class name. Returns `None` if the
328+
/// class does not exist, cannot be constructed, has a different `RefKind` from, or is not
329+
/// a sub-class of `T`.
330+
///
331+
/// The lifetime of the returned object is *not* automatically managed if `T` is a manually-
332+
/// managed type. This means that if `Object` is used as the type parameter, any `Reference`
333+
/// objects created, if returned, will be leaked. As a result, such calls will return `None`.
334+
/// Casting between `Object` and `Reference` is possible on `TRef` and bare references.
335+
#[inline]
336+
pub fn by_class_name(class_name: &str) -> Option<Self> {
337+
unsafe {
338+
// Classes with NUL-bytes in their names can not exist
339+
let class_name = CString::new(class_name).ok()?;
340+
let ctor = (get_api().godot_get_class_constructor)(class_name.as_ptr())?;
341+
let ptr = NonNull::new(ctor() as *mut sys::godot_object)?;
342+
<T::RefKind as RefKindSpec>::impl_from_maybe_ref_counted(ptr)
343+
}
344+
}
345+
}
346+
325347
/// Method for references that can be safely used.
326348
impl<T: GodotObject, Access: ThreadAccess> Ref<T, Access>
327349
where
@@ -1131,6 +1153,11 @@ pub trait RefKindSpec: Sized {
11311153
#[doc(hidden)]
11321154
type PtrWrapper: PtrWrapper;
11331155

1156+
#[doc(hidden)]
1157+
unsafe fn impl_from_maybe_ref_counted<T: GodotObject<RefKind = Self>>(
1158+
ptr: NonNull<sys::godot_object>,
1159+
) -> Option<Ref<T, Unique>>;
1160+
11341161
#[doc(hidden)]
11351162
unsafe fn impl_assume_safe<'a, T: GodotObject<RefKind = Self>>(
11361163
this: &Ref<T, Shared>,
@@ -1159,6 +1186,25 @@ pub trait RefKindSpec: Sized {
11591186
impl RefKindSpec for ManuallyManaged {
11601187
type PtrWrapper = Forget;
11611188

1189+
#[inline(always)]
1190+
unsafe fn impl_from_maybe_ref_counted<T: GodotObject<RefKind = Self>>(
1191+
ptr: NonNull<sys::godot_object>,
1192+
) -> Option<Ref<T, Unique>> {
1193+
if RawObject::<ReferenceCountedClassPlaceholder>::try_from_sys_ref(ptr).is_some() {
1194+
drop(Ref::<ReferenceCountedClassPlaceholder, Unique>::init_from_sys(ptr));
1195+
None
1196+
} else {
1197+
let obj = Ref::<ManuallyManagedClassPlaceholder, Unique>::init_from_sys(ptr);
1198+
1199+
if obj.as_raw().is_class::<T>() {
1200+
Some(obj.cast_unchecked())
1201+
} else {
1202+
obj.free();
1203+
None
1204+
}
1205+
}
1206+
}
1207+
11621208
#[inline(always)]
11631209
unsafe fn impl_assume_safe<'a, T: GodotObject<RefKind = Self>>(
11641210
this: &Ref<T, Shared>,
@@ -1190,6 +1236,24 @@ impl RefKindSpec for ManuallyManaged {
11901236
impl RefKindSpec for RefCounted {
11911237
type PtrWrapper = UnRef;
11921238

1239+
#[inline(always)]
1240+
unsafe fn impl_from_maybe_ref_counted<T: GodotObject<RefKind = Self>>(
1241+
ptr: NonNull<sys::godot_object>,
1242+
) -> Option<Ref<T, Unique>> {
1243+
if RawObject::<ReferenceCountedClassPlaceholder>::try_from_sys_ref(ptr).is_some() {
1244+
let obj = Ref::<ReferenceCountedClassPlaceholder, Unique>::init_from_sys(ptr);
1245+
1246+
if obj.as_raw().is_class::<T>() {
1247+
Some(obj.cast_unchecked())
1248+
} else {
1249+
None
1250+
}
1251+
} else {
1252+
RawObject::<ManuallyManagedClassPlaceholder>::from_sys_ref_unchecked(ptr).free();
1253+
None
1254+
}
1255+
}
1256+
11931257
#[inline(always)]
11941258
unsafe fn impl_assume_safe<'a, T: GodotObject<RefKind = Self>>(
11951259
this: &Ref<T, Shared>,

gdnative-core/src/private.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,7 @@ unsafe impl crate::object::GodotObject for ManuallyManagedClassPlaceholder {
150150
type RefKind = crate::ref_kind::ManuallyManaged;
151151

152152
fn class_name() -> &'static str {
153-
"{placeholder} manually managed object"
153+
"Object"
154154
}
155155
}
156156

@@ -162,7 +162,7 @@ unsafe impl crate::object::GodotObject for ReferenceCountedClassPlaceholder {
162162
type RefKind = crate::ref_kind::RefCounted;
163163

164164
fn class_name() -> &'static str {
165-
"{placeholder} reference counted object"
165+
"Reference"
166166
}
167167
}
168168

test/src/lib.rs

Lines changed: 3 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
#![allow(clippy::blacklisted_name)]
22

3-
use gdnative::api;
43
use gdnative::prelude::*;
54

5+
mod test_constructor;
66
mod test_derive;
77
mod test_free_ub;
88
mod test_register;
@@ -54,13 +54,13 @@ pub extern "C" fn run_tests(
5454
status &= gdnative::core_types::test_vector3_array_access();
5555
status &= gdnative::core_types::test_vector3_array_debug();
5656

57-
status &= test_constructor();
5857
status &= test_underscore_method_binding();
5958
status &= test_rust_class_construction();
6059
status &= test_from_instance_id();
6160

6261
status &= test_derive::run_tests();
6362
status &= test_free_ub::run_tests();
63+
status &= test_constructor::run_tests();
6464
status &= test_register::run_tests();
6565
status &= test_return_leak::run_tests();
6666
status &= test_variant_call_args::run_tests();
@@ -70,21 +70,6 @@ pub extern "C" fn run_tests(
7070
gdnative::core_types::Variant::from_bool(status).forget()
7171
}
7272

73-
fn test_constructor() -> bool {
74-
println!(" -- test_constructor");
75-
76-
// Just create an object and call a method as a sanity check for the
77-
// generated constructors.
78-
let lib = api::GDNativeLibrary::new();
79-
let _ = lib.is_singleton();
80-
81-
let path = api::Path2D::new();
82-
let _ = path.z_index();
83-
path.free();
84-
85-
true
86-
}
87-
8873
fn test_underscore_method_binding() -> bool {
8974
println!(" -- test_underscore_method_binding");
9075

@@ -269,6 +254,7 @@ fn init(handle: InitHandle) {
269254

270255
test_derive::register(handle);
271256
test_free_ub::register(handle);
257+
test_constructor::register(handle);
272258
test_register::register(handle);
273259
test_return_leak::register(handle);
274260
test_variant_call_args::register(handle);

test/src/test_constructor.rs

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
use gdnative::api;
2+
use gdnative::prelude::*;
3+
4+
pub(crate) fn run_tests() -> bool {
5+
let mut status = true;
6+
7+
status &= test_constructor();
8+
status &= test_from_class_name();
9+
10+
status
11+
}
12+
13+
pub(crate) fn register(_handle: InitHandle) {}
14+
15+
fn test_constructor() -> bool {
16+
println!(" -- test_constructor");
17+
18+
// Just create an object and call a method as a sanity check for the
19+
// generated constructors.
20+
let lib = api::GDNativeLibrary::new();
21+
let _ = lib.is_singleton();
22+
23+
let path = api::Path2D::new();
24+
let _ = path.z_index();
25+
path.free();
26+
27+
true
28+
}
29+
30+
fn test_from_class_name() -> bool {
31+
println!(" -- test_from_class_name");
32+
33+
let ok = std::panic::catch_unwind(|| {
34+
// Since this method is restricted to Godot types, there is no way we can detect
35+
// here whether any invalid objects are leaked. Instead, the CI script is modified
36+
// to look at stdout for any reported leaks.
37+
38+
let node = Ref::<Node, _>::by_class_name("Node2D").unwrap();
39+
assert_eq!("Node2D", node.get_class().to_string());
40+
let node = node.cast::<Node2D>().unwrap();
41+
assert_eq!("Node2D", node.get_class().to_string());
42+
let _ = node.position();
43+
node.free();
44+
45+
let shader = Ref::<Reference, _>::by_class_name("Shader").unwrap();
46+
assert_eq!("Shader", &shader.get_class().to_string());
47+
let shader = shader.cast::<Shader>().unwrap();
48+
assert_eq!("Shader", &shader.get_class().to_string());
49+
50+
let none = Ref::<Object, _>::by_class_name("Shader");
51+
assert!(none.is_none());
52+
53+
let none = Ref::<Node2D, _>::by_class_name("Spatial");
54+
assert!(none.is_none());
55+
56+
let none = Ref::<Shader, _>::by_class_name("AudioEffectReverb");
57+
assert!(none.is_none());
58+
59+
let none = Ref::<Object, _>::by_class_name("ClassThatDoesNotExistProbably");
60+
assert!(none.is_none());
61+
})
62+
.is_ok();
63+
64+
if !ok {
65+
gdnative::godot_error!(" !! Test test_from_class_name failed");
66+
}
67+
68+
ok
69+
}

0 commit comments

Comments
 (0)