Skip to content

Commit 4c2f71d

Browse files
bors[bot]chitoyuu
andauthored
Merge #999
999: Implement automatic `NativeClass` registration via inventory. Implement mix-ins. r=chitoyuu a=chitoyuu ## 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. Close #350. Note that the standalone registration attribute syntax proposed by the original issue isn't implemented, for the limited usefulness -- there are much fewer cases where manual `NativeClass` impls are necessary thanks to all the improvements since the original issue. ## Implement mix-in `#[methods]` blocks Adds the `#[methods(mixin = "Name")]` syntax for declaring mix-in blocks. Mix-in blocks have a many-to-many relationship with `NativeClass` types. Both `impl Type` and `impl Trait for Type` blocks are accepted. The argument name is changed from `as` in the original proposal to `mixin`, because we might still want to keep universal `#[methods]` blocks in the future for ease of use with WASM. `#[methods(mixin)]` makes a lot more sense for a future auto-registered mixin block than `#[methods(as /* as what? */)]`. All mix-in blocks have to be manually registered for gdnative v0.11.x. Some difficulty was encountered when trying to make auto-mixins compatible with existing code. It might still be possible with some tricks like autoref-specialization, but that might be too much effort given that we likely want to re-design much of the hierarchy for 0.12. Close #984. ## Allow `#[register_with]` for `#[monomorphize]` Enables `#[monomorphize]` to take the same standalone `#[register_with]` attribute as `#[derive(NativeClass)]`. This is chosen for short term consistency, but will probably change in a later version w/ #848, which might not still get implemented for a fair bit of time. Co-authored-by: Chitose Yuuzaki <[email protected]>
2 parents f920000 + a73c7ed commit 4c2f71d

38 files changed

+1128
-102
lines changed

.github/workflows/full-ci.yml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -277,20 +277,38 @@ 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'
284+
- rust: stable
285+
godot: "3.5.1-stable"
286+
postfix: ' (inventory)'
287+
# Limiting no-manual-register tests to stable as to not slow down CI too far -- if inventory is
288+
# working across all 3 Rust versions, this is likely to be as well.
289+
build_args: '--features inventory,no-manual-register'
280290
- rust: nightly
281291
godot: "3.5.1-stable"
282292
postfix: ' (nightly)'
283293
- rust: nightly
284294
godot: "3.5.1-stable"
285295
postfix: ' (nightly, ptrcall)'
286296
build_args: '--features ptrcall'
297+
- rust: nightly
298+
godot: "3.5.1-stable"
299+
postfix: ' (nightly, inventory)'
300+
build_args: '--features inventory'
287301
- rust: '1.63'
288302
godot: "3.5.1-stable"
289303
postfix: ' (msrv 1.63)'
290304
- rust: '1.63'
291305
godot: "3.5.1-stable"
292306
postfix: ' (msrv 1.63, ptrcall)'
293307
build_args: '--features ptrcall'
308+
- rust: '1.63'
309+
godot: "3.5.1-stable"
310+
postfix: ' (msrv 1.63, inventory)'
311+
build_args: '--features inventory'
294312

295313
# Test with oldest supported engine version
296314
- rust: stable

check.sh

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,8 @@ function findGodot() {
6262
fi
6363
}
6464

65-
features="gdnative/async,gdnative/serde"
65+
features="gdnative/async,gdnative/serde,gdnative/inventory"
66+
itest_toggled_features="no-manual-register"
6667
cmds=()
6768

6869
for arg in "${args[@]}"; do
@@ -81,6 +82,9 @@ for arg in "${args[@]}"; do
8182
cmds+=("cargo build --manifest-path test/Cargo.toml --features $features")
8283
cmds+=("cp target/debug/*gdnative_test* test/project/lib/")
8384
cmds+=("$godotBin --path test/project")
85+
cmds+=("cargo build --manifest-path test/Cargo.toml --features $features,$itest_toggled_features")
86+
cmds+=("cp target/debug/*gdnative_test* test/project/lib/")
87+
cmds+=("$godotBin --path test/project")
8488
;;
8589
doc)
8690
cmds+=("cargo doc --lib -p gdnative --no-deps --features $features")

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.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,13 +99,20 @@ pub trait NativeClass: Sized + 'static {
9999

100100
/// A NativeScript "class" that is statically named. [`NativeClass`] types that implement this
101101
/// trait can be registered using [`InitHandle::add_class`].
102+
///
103+
/// This trait will be renamed to [`Monomorphized`] in a future version since its purpose has
104+
/// grown beyond simply providing a static type name.
102105
pub trait StaticallyNamed: NativeClass {
103106
/// The name of the class.
104107
///
105108
/// This name must be unique for the dynamic library. For generic or library code where this
106109
/// is hard to satisfy, consider using [`InitHandle::add_class_as`] to provide a name
107110
/// at registration time instead.
108111
const CLASS_NAME: &'static str;
112+
113+
/// Function that registers methods specific to this monomorphization.
114+
#[inline]
115+
fn nativeclass_register_monomorphized(_builder: &ClassBuilder<Self>) {}
109116
}
110117

111118
/// Trait used to provide information of Godot-exposed methods of a script class.

gdnative-core/src/export/class_builder.rs

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
use std::any::TypeId;
2+
use std::cell::RefCell;
3+
use std::collections::HashSet;
14
use std::ffi::CString;
25
use std::marker::PhantomData;
36
use std::ptr;
@@ -20,6 +23,7 @@ use crate::private::get_api;
2023
pub struct ClassBuilder<C> {
2124
pub(super) init_handle: *mut libc::c_void,
2225
pub(super) class_name: CString,
26+
mixins: RefCell<HashSet<TypeId, ahash::RandomState>>,
2327
_marker: PhantomData<C>,
2428
}
2529

@@ -28,6 +32,7 @@ impl<C: NativeClass> ClassBuilder<C> {
2832
Self {
2933
init_handle,
3034
class_name,
35+
mixins: RefCell::default(),
3136
_marker: PhantomData,
3237
}
3338
}
@@ -241,4 +246,48 @@ impl<C: NativeClass> ClassBuilder<C> {
241246
);
242247
}
243248
}
249+
250+
/// Add a mixin to the class being registered.
251+
///
252+
/// # Examples
253+
///
254+
/// ```
255+
/// use gdnative::prelude::*;
256+
///
257+
/// #[derive(NativeClass)]
258+
/// #[inherit(Node)]
259+
/// #[register_with(my_register)]
260+
/// #[no_constructor]
261+
/// struct MyType {}
262+
///
263+
/// // This creates a opaque type `MyMixin` in the current scope that implements
264+
/// // the `Mixin` trait. Mixin types have no public interface or stable layout.
265+
/// #[methods(mixin = "MyMixin")]
266+
/// impl MyType {
267+
/// #[method]
268+
/// fn my_method(&self) -> i64 { 42 }
269+
/// }
270+
///
271+
/// fn my_register(builder: &ClassBuilder<MyType>) {
272+
/// builder.mixin::<MyMixin>();
273+
/// }
274+
/// ```
275+
#[inline]
276+
pub fn mixin<M: Mixin<C>>(&self) {
277+
if self.mixins.borrow_mut().insert(TypeId::of::<M>()) {
278+
M::register(self);
279+
}
280+
}
281+
}
282+
283+
/// Trait for mixins, manually registered `#[methods]` blocks that may be applied to multiple types.
284+
///
285+
/// This trait is implemented on generated types by the `#[methods]` proc-macro only, and has no public interface.
286+
/// Use [`ClassBuilder::mixin`] to register mixins to [`NativeClass`] types.
287+
pub trait Mixin<C>: crate::private::mixin::Sealed + 'static
288+
where
289+
C: NativeClass,
290+
{
291+
#[doc(hidden)]
292+
fn register(builder: &ClassBuilder<C>);
244293
}

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
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+
}

0 commit comments

Comments
 (0)