Skip to content

Commit dca4816

Browse files
committed
Implement automatic NativeClass registration via inventory
This adds the optional `inventory` feature, which allows `NativeClass` types to be automatically registered on supported platforms (everything that OSS Godot currently supports except WASM). Run-time diagnostic functions are added to help debug missing registration problems that are highly likely to arise when porting `inventory`-enabled projects to WASM. An internal `cfg_ex` attribute macro is added to help manage cfg conditions.
1 parent f920000 commit dca4816

File tree

25 files changed

+513
-27
lines changed

25 files changed

+513
-27
lines changed

.github/workflows/full-ci.yml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -277,20 +277,32 @@ jobs:
277277
godot: "3.5.1-stable"
278278
postfix: ' (ptrcall)'
279279
build_args: '--features ptrcall'
280+
- rust: stable
281+
godot: "3.5.1-stable"
282+
postfix: ' (inventory)'
283+
build_args: '--features inventory'
280284
- rust: nightly
281285
godot: "3.5.1-stable"
282286
postfix: ' (nightly)'
283287
- rust: nightly
284288
godot: "3.5.1-stable"
285289
postfix: ' (nightly, ptrcall)'
286290
build_args: '--features ptrcall'
291+
- rust: nightly
292+
godot: "3.5.1-stable"
293+
postfix: ' (nightly, inventory)'
294+
build_args: '--features inventory'
287295
- rust: '1.63'
288296
godot: "3.5.1-stable"
289297
postfix: ' (msrv 1.63)'
290298
- rust: '1.63'
291299
godot: "3.5.1-stable"
292300
postfix: ' (msrv 1.63, ptrcall)'
293301
build_args: '--features ptrcall'
302+
- rust: '1.63'
303+
godot: "3.5.1-stable"
304+
postfix: ' (msrv 1.63, inventory)'
305+
build_args: '--features inventory'
294306

295307
# Test with oldest supported engine version
296308
- rust: stable

gdnative-core/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ libc = "0.2"
3030
once_cell = "1"
3131
parking_lot = "0.12"
3232
serde = { version = "1", features = ["derive"], optional = true }
33+
inventory = { version = "0.3", optional = true }
3334

3435
[dev-dependencies]
3536
gdnative = { path = "../gdnative" } # for doc-tests

gdnative-core/src/export/class_registry.rs

Lines changed: 86 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,22 @@
11
use std::any::TypeId;
22
use std::borrow::Cow;
3+
use std::collections::hash_map::Entry;
34
use std::collections::HashMap;
5+
use std::fmt;
46

57
use once_cell::sync::Lazy;
68
use parking_lot::RwLock;
79

810
use crate::export::NativeClass;
11+
use crate::init::InitLevel;
912

1013
static CLASS_REGISTRY: Lazy<RwLock<HashMap<TypeId, ClassInfo>>> =
1114
Lazy::new(|| RwLock::new(HashMap::new()));
1215

16+
#[derive(Clone, Debug)]
1317
pub(crate) struct ClassInfo {
1418
pub name: Cow<'static, str>,
19+
pub init_level: InitLevel,
1520
}
1621

1722
/// Access the [`ClassInfo`] of the class `C`.
@@ -40,12 +45,89 @@ pub(crate) fn class_name_or_default<C: NativeClass>() -> Cow<'static, str> {
4045
class_name::<C>().unwrap_or_else(|| Cow::Borrowed(std::any::type_name::<C>()))
4146
}
4247

43-
/// Registers the class `C` in the class registry, using a custom name.
44-
/// Returns the old `ClassInfo` if `C` was already added.
48+
/// Registers the class `C` in the class registry, using a custom name at the given level.
49+
/// Returns `Ok(true)` if FFI registration needs to be performed. `Ok(false)` if the class has
50+
/// already been registered on another level.
51+
/// Returns an error with the old `ClassInfo` if a conflicting entry for `C` was already added.
4552
#[inline]
46-
pub(crate) fn register_class_as<C: NativeClass>(name: Cow<'static, str>) -> Option<ClassInfo> {
53+
pub(crate) fn register_class_as<C: NativeClass>(
54+
name: Cow<'static, str>,
55+
init_level: InitLevel,
56+
) -> Result<bool, RegisterError> {
4757
let type_id = TypeId::of::<C>();
48-
CLASS_REGISTRY.write().insert(type_id, ClassInfo { name })
58+
let mut registry = CLASS_REGISTRY.write();
59+
match registry.entry(type_id) {
60+
Entry::Vacant(entry) => {
61+
entry.insert(ClassInfo { name, init_level });
62+
Ok(true)
63+
}
64+
Entry::Occupied(entry) => {
65+
let class_info = entry.get();
66+
let kind = if class_info.name != name {
67+
Some(RegisterErrorKind::ConflictingName)
68+
} else if class_info.init_level.intersects(init_level) {
69+
Some(RegisterErrorKind::AlreadyOnSameLevel)
70+
} else {
71+
None
72+
};
73+
74+
if let Some(kind) = kind {
75+
Err(RegisterError {
76+
class_info: class_info.clone(),
77+
type_name: std::any::type_name::<C>(),
78+
kind,
79+
})
80+
} else {
81+
Ok(false)
82+
}
83+
}
84+
}
85+
}
86+
87+
#[inline]
88+
#[allow(dead_code)] // Currently unused on platforms with inventory support
89+
pub(crate) fn types_with_init_level(allow: InitLevel, deny: InitLevel) -> Vec<Cow<'static, str>> {
90+
let registry = CLASS_REGISTRY.read();
91+
let mut list = registry
92+
.values()
93+
.filter_map(|class_info| {
94+
(class_info.init_level.intersects(allow) && !class_info.init_level.intersects(deny))
95+
.then(|| class_info.name.clone())
96+
})
97+
.collect::<Vec<_>>();
98+
99+
list.sort_unstable();
100+
list
101+
}
102+
103+
#[derive(Debug)]
104+
pub(crate) struct RegisterError {
105+
pub type_name: &'static str,
106+
pub class_info: ClassInfo,
107+
pub kind: RegisterErrorKind,
108+
}
109+
110+
#[derive(Debug)]
111+
pub(crate) enum RegisterErrorKind {
112+
ConflictingName,
113+
AlreadyOnSameLevel,
114+
}
115+
116+
impl fmt::Display for RegisterError {
117+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
118+
match self.kind {
119+
RegisterErrorKind::ConflictingName => {
120+
write!(
121+
f,
122+
"`{}` has already been registered as `{}`",
123+
self.type_name, self.class_info.name
124+
)
125+
}
126+
RegisterErrorKind::AlreadyOnSameLevel => {
127+
write!(f, "`{}` has already been registered", self.type_name)
128+
}
129+
}
130+
}
49131
}
50132

51133
/// Clears the registry

gdnative-core/src/init/diagnostics.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
//! Run-time tracing functions to help debug the init process.
2+
//!
3+
//! The provided functions are designed to convey any issues found through human-readable
4+
//! error output, while programmatically providing only an overall indication of whether
5+
//! any problems were found. This is so that they can be freely improved without compatibility
6+
//! concerns.
7+
8+
mod missing_manual_registration;
9+
mod missing_suggested_diagnostics;
10+
11+
#[doc(inline)]
12+
pub use missing_manual_registration::missing_manual_registration;
13+
14+
#[doc(inline)]
15+
pub use missing_suggested_diagnostics::missing_suggested_diagnostics;
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
use std::sync::atomic::{AtomicBool, Ordering};
2+
3+
use gdnative_impl_proc_macros::cfg_ex;
4+
5+
pub static CHECKED: AtomicBool = AtomicBool::new(false);
6+
7+
/// Checks for any `NativeClass` types that are registered automatically, but not manually.
8+
/// Returns `true` if the test isn't applicable, or if no such types were found.
9+
///
10+
/// Some platforms may not have support for automatic registration. On such platforms, only
11+
/// manually registered classes are visible at run-time.
12+
///
13+
/// Please refer to [the `rust-ctor` README][ctor-repo] for an up-to-date listing of platforms
14+
/// that *do* support automatic registration.
15+
///
16+
/// [ctor-repo]: https://github.com/mmastrac/rust-ctor
17+
#[inline]
18+
pub fn missing_manual_registration() -> bool {
19+
CHECKED.store(true, Ordering::Release);
20+
check_missing_manual_registration()
21+
}
22+
23+
#[cfg_ex(not(all(feature = "inventory", gdnative::inventory_platform_available)))]
24+
fn check_missing_manual_registration() -> bool {
25+
true
26+
}
27+
28+
#[cfg_ex(all(feature = "inventory", gdnative::inventory_platform_available))]
29+
fn check_missing_manual_registration() -> bool {
30+
use crate::init::InitLevel;
31+
32+
let types =
33+
crate::export::class_registry::types_with_init_level(InitLevel::AUTO, InitLevel::USER);
34+
35+
if types.is_empty() {
36+
return true;
37+
}
38+
39+
let mut message = format!(
40+
"gdnative-core: {} NativeScript(s) are not manually registered: ",
41+
types.len()
42+
);
43+
44+
let mut first = true;
45+
for name in types {
46+
if first {
47+
first = false;
48+
} else {
49+
message.push_str(", ");
50+
}
51+
message.push_str(&name);
52+
}
53+
54+
godot_warn!("{message}");
55+
godot_warn!(concat!(
56+
"gdnative-core: Types that are not manually registered will not be available on platforms ",
57+
"where automatic registration is unavailable.",
58+
));
59+
60+
false
61+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
use gdnative_impl_proc_macros::cfg_ex;
2+
3+
/// Checks if all suggested diagnostics have been ran depending on the current platform, at
4+
/// the point of invocation. This is automatically ran as part of the init macro, and do not
5+
/// usually need to be manually invoked.
6+
///
7+
/// Returns `true` in a release build, or if no such diagnostics were found.
8+
#[inline]
9+
pub fn missing_suggested_diagnostics() -> bool {
10+
check_missing_suggested_diagnostics()
11+
}
12+
13+
#[cfg(not(debug_assertions))]
14+
fn check_missing_suggested_diagnostics() -> bool {
15+
true
16+
}
17+
18+
#[cfg(debug_assertions)]
19+
fn check_missing_suggested_diagnostics() -> bool {
20+
check_missing_suggested_diagnostics_inventory_unavailable()
21+
}
22+
23+
#[cfg_ex(all(feature = "inventory", not(gdnative::inventory_platform_available)))]
24+
fn check_missing_suggested_diagnostics_inventory_unavailable() -> bool {
25+
if !super::missing_manual_registration::CHECKED.load(std::sync::atomic::Ordering::Acquire) {
26+
godot_warn!(concat!(
27+
"gdnative-core: `gdnative` was compiled with the `inventory` feature, but the current platform ",
28+
"does not support automatic registration. As such, only manually registered types will be available.\n",
29+
"Call `gdnative::init::diagnostics::missing_manual_registration()` at the end your init callback to "
30+
"suppress this message."
31+
));
32+
33+
false
34+
} else {
35+
true
36+
}
37+
}
38+
39+
#[cfg_ex(not(all(feature = "inventory", not(gdnative::inventory_platform_available))))]
40+
fn check_missing_suggested_diagnostics_inventory_unavailable() -> bool {
41+
true
42+
}

gdnative-core/src/init/init_handle.rs

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,22 +8,24 @@ use std::borrow::Cow;
88
use std::ffi::CString;
99
use std::ptr;
1010

11+
use super::InitLevel;
12+
1113
/// A handle that can register new classes to the engine during initialization.
1214
///
1315
/// See [`godot_nativescript_init`](macro.godot_nativescript_init.html) and
1416
/// [`godot_init`](macro.godot_init.html).
1517
#[derive(Copy, Clone)]
1618
pub struct InitHandle {
17-
#[doc(hidden)]
1819
handle: *mut libc::c_void,
20+
init_level: InitLevel,
1921
}
2022

2123
#[allow(deprecated)] // Remove once init(), register_properties() and register() have been renamed
2224
impl InitHandle {
2325
#[doc(hidden)]
2426
#[inline]
25-
pub unsafe fn new(handle: *mut libc::c_void) -> Self {
26-
InitHandle { handle }
27+
pub unsafe fn new(handle: *mut libc::c_void, init_level: InitLevel) -> Self {
28+
InitHandle { handle, init_level }
2729
}
2830

2931
/// Registers a new class to the engine.
@@ -75,13 +77,14 @@ impl InitHandle {
7577
{
7678
let c_class_name = CString::new(&*name).unwrap();
7779

78-
if let Some(class_info) = class_registry::register_class_as::<C>(name) {
79-
panic!(
80-
"`{type_name}` has already been registered as `{old_name}`",
81-
type_name = std::any::type_name::<C>(),
82-
old_name = class_info.name,
83-
);
84-
}
80+
match class_registry::register_class_as::<C>(name, self.init_level) {
81+
Ok(true) => {}
82+
Ok(false) => return,
83+
Err(e) => {
84+
godot_error!("gdnative-core: ignoring new registration: {e}");
85+
return;
86+
}
87+
};
8588

8689
unsafe {
8790
let base_name = CString::new(C::Base::class_name()).unwrap();

gdnative-core/src/init/macros.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,10 @@ macro_rules! godot_nativescript_init {
5757
}
5858

5959
$crate::private::report_panics("nativescript_init", || {
60-
$callback($crate::init::InitHandle::new(handle));
60+
$crate::init::auto_register($crate::init::InitHandle::new(handle, $crate::init::InitLevel::AUTO));
61+
$callback($crate::init::InitHandle::new(handle, $crate::init::InitLevel::USER));
62+
63+
$crate::init::diagnostics::missing_suggested_diagnostics();
6164
});
6265
}
6366
};

gdnative-core/src/init/mod.rs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,39 @@ mod info;
4040
mod init_handle;
4141
mod macros;
4242

43+
pub mod diagnostics;
44+
4345
pub use info::*;
4446
pub use init_handle::*;
4547

48+
bitflags::bitflags! {
49+
/// Initialization level used to distinguish the source of init actions, such as class registration.
50+
/// Internal API.
51+
#[doc(hidden)]
52+
pub struct InitLevel: u8 {
53+
/// Init level for automatic registration
54+
const AUTO = 1;
55+
/// Init level for user code
56+
const USER = 2;
57+
}
58+
}
59+
60+
#[doc(hidden)]
61+
#[cfg(feature = "inventory")]
62+
#[inline]
63+
pub fn auto_register(init_handle: InitHandle) {
64+
for plugin in inventory::iter::<crate::private::AutoInitPlugin> {
65+
(plugin.f)(init_handle);
66+
}
67+
}
68+
69+
#[doc(hidden)]
70+
#[cfg(not(feature = "inventory"))]
71+
#[inline]
72+
pub fn auto_register(_init_handle: InitHandle) {
73+
// Nothing to do here.
74+
}
75+
4676
pub use crate::{
4777
godot_gdnative_init, godot_gdnative_terminate, godot_init, godot_nativescript_init,
4878
};

0 commit comments

Comments
 (0)