Skip to content

Commit 08edc1f

Browse files
authored
Merge pull request #1664 from godot-rust/qol/validate-registrations
Validate class registrations against `ClassDB`
2 parents ad5fa7c + e055940 commit 08edc1f

10 files changed

Lines changed: 457 additions & 99 deletions

File tree

godot-core/src/init/mod.rs

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,9 @@ unsafe extern "C" fn startup_func<E: ExtensionLibrary>() {
125125

126126
// Now that editor UI is ready, display all warnings/error collected so far.
127127
sys::print_deferred_startup_messages();
128+
129+
// If any of the errors were fatal, exit here.
130+
abort_if_startup_fatal();
128131
}
129132

130133
#[cfg(since_api = "4.5")]
@@ -345,6 +348,49 @@ unsafe fn gdext_on_level_init(level: InitLevel, _userdata: &InitUserData) {
345348
CURRENT_INIT_LEVEL.store(Some(level));
346349
}
347350

351+
/// Aborts the process if a fatal startup error was collected (depending on runtime target).
352+
///
353+
/// Behavior:
354+
/// - An _interactive_ editor is not aborted, so the developer can read the errors and hot-reload a fix.
355+
/// - A _headless_ editor (CI, `--export-release`, `--import`) has no one to read them and aborts like a game run.
356+
/// - A _game_ runs `SceneTree::quit()` (frames may still run with the broken class set, possibly causing follow-up errors)
357+
/// - A game without a `SceneTree` (e.g. a custom `MainLoop`) calls `process::exit()`.
358+
#[cfg(since_api = "4.5")]
359+
fn abort_if_startup_fatal() {
360+
if !sys::take_startup_fatal() {
361+
return;
362+
}
363+
364+
// Interactive editor: keep running, the developer can read the errors and hot-reload a fix.
365+
if is_editor_hint() && !is_headless() {
366+
return;
367+
}
368+
369+
// Exit code within Godot's recommended 0..=125 range for `SceneTree::quit()`.
370+
let exit_code = 111;
371+
if let Some(main_loop) = classes::Engine::singleton().get_main_loop()
372+
&& let Ok(mut scene_tree) = main_loop.try_cast::<classes::SceneTree>()
373+
{
374+
scene_tree.quit_ex().exit_code(exit_code).done();
375+
} else {
376+
std::process::exit(exit_code);
377+
}
378+
}
379+
380+
/// Whether Godot runs without a visual display -- `--headless`, `--display-driver headless`, or an export/import run, which force it.
381+
#[cfg(since_api = "4.5")]
382+
fn is_headless() -> bool {
383+
// Absent singleton or a failing call -> headless. Dynamic call for minimal codegen.
384+
if let Some(mut display_server) = classes::Engine::singleton().get_singleton("DisplayServer")
385+
&& let Ok(name) = display_server.try_call("get_name", &[])
386+
&& let Ok(name) = name.try_to::<GString>()
387+
{
388+
name == "headless"
389+
} else {
390+
true
391+
}
392+
}
393+
348394
/// Tasks needed to be done by gdext internally upon unloading an initialization level. Called after user code.
349395
fn gdext_on_level_deinit(level: InitLevel) {
350396
if level == InitLevel::Editor {

godot-core/src/private.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ mod reexport_pub {
3333
pub use crate::obj::rpc::priv_re_export::*;
3434
pub use crate::obj::rtti::ObjectRtti;
3535
pub use crate::registry::callbacks;
36+
pub use crate::registry::reg_validation::validate_signal;
3637
pub use crate::registry::shard::{
3738
ClassShard, DynTraitImpl, ErasedDynGd, ErasedRegisterFn, ITraitImpl, InherentImpl,
3839
ShardItem, Struct,

godot-core/src/registry/class.rs

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@ use crate::meta::ClassId;
1616
use crate::meta::error::FromGodotError;
1717
use crate::obj::{DynGd, Gd, GodotClass, Singleton, cap};
1818
use crate::private::{ClassShard, ShardItem};
19-
use crate::registry::callbacks;
2019
use crate::registry::shard::{DynTraitImpl, ErasedRegisterFn, ITraitImpl, InherentImpl, Struct};
20+
use crate::registry::{callbacks, reg_validation};
2121
use crate::{godot_error, godot_warn, sys};
2222

2323
/// Returns a lock to a global map of loaded classes, by initialization level.
@@ -585,14 +585,14 @@ fn fill_into<T>(dst: &mut Option<T>, src: Option<T>) -> Result<(), ()> {
585585
fn register_class_raw(mut info: ClassRegistrationInfo) {
586586
// Some metadata like dynify fns are already emptied at this point. Only consider registrations for Godot.
587587

588-
// First register class...
589-
validate_class_constraints(&info);
590-
591588
let class_name = info.class_name;
592589
let parent_class_name = info
593590
.parent_class_name
594591
.expect("class defined (parent_class_name)");
595592

593+
// First register class...
594+
precheck_class_registration(&info, parent_class_name);
595+
596596
// Register virtual functions -- if the user provided some via #[godot_api], take those; otherwise, use the
597597
// ones generated alongside #[derive(GodotClass)]. The latter can also be null, if no OnReady is provided.
598598
if info.godot_params.get_virtual_func.is_none() {
@@ -622,14 +622,14 @@ fn register_class_raw(mut info: ClassRegistrationInfo) {
622622
ptr::addr_of!(info.godot_params),
623623
);
624624

625-
// ...then see if it worked.
626-
// This is necessary because the above registration does not report errors (apart from console output).
625+
// ...then see if it worked. The registration above does not report errors (apart from console output), and precheck may miss something.
627626
let tag = interface_fn!(classdb_get_class_tag)(class_name.string_sys());
628627
tag.is_null()
629628
};
630629

631630
// Do not panic here; otherwise lock is poisoned and the whole extension becomes unusable.
632631
// This can happen during hot reload if a class changes base type in an incompatible way (e.g. RefCounted -> Node).
632+
// Also, the pre-validation only runs under strict safeguards, there may be registrations that Godot rejects but godot-rust doesn't catch.
633633
if registration_failed {
634634
godot_error!(
635635
"Failed to register class `{class_name}`; check preceding Godot stderr messages."
@@ -661,8 +661,11 @@ fn register_class_raw(mut info: ClassRegistrationInfo) {
661661
}
662662
}
663663

664-
fn validate_class_constraints(_class: &ClassRegistrationInfo) {
665-
// TODO: if we add builder API, the proc-macro checks in parse_struct_attributes() etc. should be duplicated here.
664+
/// Validates a class registration before it is handed to Godot, which reports errors only to stderr.
665+
fn precheck_class_registration(class_info: &ClassRegistrationInfo, parent_class_id: ClassId) {
666+
// TODO(v0.7): if we add builder API, the proc-macro checks in parse_struct_attributes() etc. should be duplicated here.
667+
668+
reg_validation::validate_class(class_info.class_name, parent_class_id);
666669
}
667670

668671
fn unregister_class_raw(class: LoadedClass) {

godot-core/src/registry/constant.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ impl IntegerConstant {
3131
}
3232

3333
fn register(&self, class_name: ClassId, enum_name: &StringName, is_bitfield: bool) {
34+
crate::registry::reg_validation::validate_constant(class_name, &self.name);
35+
3436
unsafe {
3537
interface_fn!(classdb_register_extension_class_integer_constant)(
3638
sys::get_library(),

godot-core/src/registry/godot_register_wrappers.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,13 @@ fn register_var_or_export_inner(
8484
let getter_name = StringName::from(getter_name);
8585
let setter_name = StringName::from(setter_name);
8686

87+
crate::registry::reg_validation::validate_property(
88+
class_name,
89+
&info.property_name,
90+
&getter_name,
91+
&setter_name,
92+
);
93+
8794
let property_info_sys = info.property_sys();
8895

8996
unsafe {

godot-core/src/registry/method.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,9 @@ impl ClassMethodInfo {
153153
}
154154

155155
fn register_nonvirtual_class_method(&self, method_info_sys: sys::GDExtensionClassMethodInfo) {
156+
// Only for non-virtual methods. Godot keeps virtual methods in a separate map, which isn't exposed through ClassDB.
157+
crate::registry::reg_validation::validate_method(self.class_id, &self.method_name);
158+
156159
// SAFETY: The lifetime of the data we use here is at least as long as this function's scope. So we can
157160
// safely call this function without issue.
158161
//

godot-core/src/registry/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ pub mod constant;
3030
pub mod info;
3131
pub mod method;
3232
pub mod property;
33+
pub mod reg_validation;
3334
pub mod shard;
3435

3536
// RpcConfig uses MultiplayerPeer::TransferMode and MultiplayerApi::RpcMode, which are only enabled in `codegen-full` feature.

0 commit comments

Comments
 (0)