Skip to content

Commit e1b69d2

Browse files
committed
Adding ClassCodegenLevel::Core
- mostly just mimic'd the setup for server classes added special_case::is_class_level_core - trying to dry up all the special case stuff into special_cases module - added unit testing for init levels. Make sure utilizing the early_core_singletons works. - documented Godot startup sequence in special_cases.rs and tried to reference where each class list was (roughly) coming from - promoted some base types to Core but hestiant to promote the whole list since it's very brittle and later levels are always safe.
1 parent f46e14a commit e1b69d2

File tree

10 files changed

+197
-66
lines changed

10 files changed

+197
-66
lines changed

godot-codegen/src/generator/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ pub fn generate_sys_module_file(sys_gen_path: &Path, submit_fn: &mut SubmitFn) {
4747
let code = quote! {
4848
pub mod table_builtins;
4949
pub mod table_builtins_lifecycle;
50+
pub mod table_core_classes;
5051
pub mod table_servers_classes;
5152
pub mod table_scene_classes;
5253
pub mod table_editor_classes;

godot-codegen/src/models/domain.rs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -805,14 +805,15 @@ impl ToTokens for ModName {
805805
/// At which stage a class function pointer is loaded.
806806
#[derive(Copy, Clone, Eq, PartialEq, Hash)]
807807
pub enum ClassCodegenLevel {
808+
Core,
808809
Servers,
809810
Scene,
810811
Editor,
811812
}
812813

813814
impl ClassCodegenLevel {
814-
pub fn with_tables() -> [Self; 3] {
815-
[Self::Servers, Self::Scene, Self::Editor]
815+
pub fn with_tables() -> [Self; 4] {
816+
[Self::Core, Self::Servers, Self::Scene, Self::Editor]
816817
}
817818

818819
pub fn table_global_getter(self) -> Ident {
@@ -829,6 +830,7 @@ impl ClassCodegenLevel {
829830

830831
pub fn lower(self) -> &'static str {
831832
match self {
833+
Self::Core => "core",
832834
Self::Servers => "servers",
833835
Self::Scene => "scene",
834836
Self::Editor => "editor",
@@ -837,6 +839,7 @@ impl ClassCodegenLevel {
837839

838840
fn upper(self) -> &'static str {
839841
match self {
842+
Self::Core => "Core",
840843
Self::Servers => "Servers",
841844
Self::Scene => "Scene",
842845
Self::Editor => "Editor",
@@ -845,6 +848,7 @@ impl ClassCodegenLevel {
845848

846849
pub fn to_init_level(self) -> TokenStream {
847850
match self {
851+
Self::Core => quote! { crate::init::InitLevel::Core },
848852
Self::Servers => quote! { crate::init::InitLevel::Servers },
849853
Self::Scene => quote! { crate::init::InitLevel::Scene },
850854
Self::Editor => quote! { crate::init::InitLevel::Editor },

godot-codegen/src/special_cases/codegen_special_cases.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,7 @@ const SELECTED_CLASSES: &[&str] = &[
140140
"ClassDB",
141141
"Engine",
142142
"OS",
143+
"ProjectSettings",
143144
//
144145
// Editor plugins
145146
"EditorPlugin",

godot-codegen/src/special_cases/special_cases.rs

Lines changed: 69 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
use proc_macro2::Ident;
3131

3232
use crate::conv::to_enum_type_uncached;
33-
use crate::models::domain::{Enum, RustTy, TyName, VirtualMethodPresence};
33+
use crate::models::domain::{ClassCodegenLevel, Enum, RustTy, TyName, VirtualMethodPresence};
3434
use crate::models::json::{JsonBuiltinMethod, JsonClassMethod, JsonSignal, JsonUtilityFunction};
3535
use crate::special_cases::codegen_special_cases;
3636
use crate::util::option_as_slice;
@@ -985,34 +985,85 @@ pub fn get_derived_virtual_method_presence(class_name: &TyName, godot_method_nam
985985
}
986986
}
987987

988+
/// Initialization order for Godot (see https://github.com/godotengine/godot/blob/master/main/main.cpp)
989+
/// - Main::setup()
990+
/// - register_core_types()
991+
/// - register_early_core_singletons()
992+
/// - initialize_extensions(GDExtension::INITIALIZATION_LEVEL_CORE)
993+
/// - Main::setup2()
994+
/// - register_server_types()
995+
/// - initialize_extensions(GDExtension::INITIALIZATION_LEVEL_SERVERS)
996+
/// - register_core_singletons() ...possibly a bug. Should this be before LEVEL_SERVERS?
997+
/// - register_scene_types()
998+
/// - register_scene_singletons()
999+
/// - initialize_extensions(GDExtension::INITIALIZATION_LEVEL_SCENE)
1000+
/// - IF EDITOR
1001+
/// - register_editor_types()
1002+
/// - initialize_extensions(GDExtension::INITIALIZATION_LEVEL_EDITOR)
1003+
/// - register_server_singletons() ...another weird one.
1004+
/// - Autoloads, etc.
9881005
#[rustfmt::skip]
989-
pub fn is_class_level_server(class_name: &str) -> bool {
990-
// Unclear on if some of these classes should be registered earlier than `Scene`:
991-
// - `RenderData` + `RenderDataExtension`
992-
// - `RenderSceneData` + `RenderSceneDataExtension`
993-
994-
match class_name {
995-
// TODO: These should actually be at level `Core`
996-
| "Object" | "OpenXRExtensionWrapperExtension"
997-
998-
// Declared final (un-inheritable) in Rust, but those are still servers.
999-
| "AudioServer" | "CameraServer" | "NavigationServer2D" | "NavigationServer3D" | "RenderingServer" | "TranslationServer" | "XRServer"
1000-
1001-
// PhysicsServer2D
1006+
pub fn classify_codegen_level(class_name: &str, godot_classification: &str) -> Option<ClassCodegenLevel> {
1007+
let level = match class_name {
1008+
// See register_core_types() in https://github.com/godotengine/godot/blob/master/core/register_core_types.cpp,
1009+
// which is called before Core level is initialized.
1010+
// Currently only promoting super basic classes to Core level since this is a brittle hardcoded list (feel free expand as
1011+
// necessary based on careful evaluation of register_core_types).
1012+
| "Object" | "RefCounted" | "Resource" | "MainLoop" | "GDExtension"
1013+
=> ClassCodegenLevel::Core,
1014+
1015+
// See register_early_core_singletons() in https://github.com/godotengine/godot/blob/master/core/register_core_types.cpp,
1016+
// which is called before Core level is initialized.
1017+
| "ProjectSettings" | "Engine" | "OS" | "Time"
1018+
=> ClassCodegenLevel::Core,
1019+
1020+
// Anything that comes from another extension could be available in Core but since there would be load order dependencies,
1021+
// instead don't allow calls until Server level.
1022+
| "OpenXRExtensionWrapperExtension"
1023+
=> ClassCodegenLevel::Servers,
1024+
1025+
// See register_server_types() in https://github.com/godotengine/godot/blob/master/servers/register_server_types.cpp
10021026
| "PhysicsDirectBodyState2D" | "PhysicsDirectBodyState2DExtension"
10031027
| "PhysicsDirectSpaceState2D" | "PhysicsDirectSpaceState2DExtension"
10041028
| "PhysicsServer2D" | "PhysicsServer2DExtension"
10051029
| "PhysicsServer2DManager"
1006-
1007-
// PhysicsServer3D
10081030
| "PhysicsDirectBodyState3D" | "PhysicsDirectBodyState3DExtension"
10091031
| "PhysicsDirectSpaceState3D" | "PhysicsDirectSpaceState3DExtension"
10101032
| "PhysicsServer3D" | "PhysicsServer3DExtension"
10111033
| "PhysicsServer3DManager"
10121034
| "PhysicsServer3DRenderingServerHandler"
1035+
| "RenderData" | "RenderDataExtension"
1036+
| "RenderSceneData" | "RenderSceneDataExtension"
1037+
=> ClassCodegenLevel::Servers,
1038+
// Declared final (un-inheritable) in Rust, but those are still servers.
1039+
// NOTE: while these _types_ are available at Server level, the singletons themselves are actually not available until _even after_ Editor level.
1040+
| "AudioServer" | "CameraServer" | "NavigationServer2D" | "NavigationServer3D" | "RenderingServer" | "TranslationServer" | "XRServer" | "DisplayServer"
1041+
=> ClassCodegenLevel::Servers,
1042+
1043+
// Work around wrong classification in https://github.com/godotengine/godot/issues/86206.
1044+
// https://github.com/godotengine/godot/issues/103867
1045+
"OpenXRInteractionProfileEditorBase"
1046+
| "OpenXRInteractionProfileEditor"
1047+
| "OpenXRBindingModifierEditor" if cfg!(before_api = "4.5")
1048+
=> ClassCodegenLevel::Editor,
1049+
// https://github.com/godotengine/godot/issues/86206
1050+
"ResourceImporterOggVorbis" | "ResourceImporterMP3" if cfg!(before_api = "4.3")
1051+
=> ClassCodegenLevel::Editor,
10131052

1014-
=> true, _ => false
1015-
}
1053+
_ => {
1054+
// NOTE: Right now, Godot reports everything that's not "editor" as "core" in `extension_api.json`.
1055+
// If it wasn't picked up by the whitelist, and Godot reports it as "core" we will treat it as a scene class.
1056+
match godot_classification {
1057+
"editor" => ClassCodegenLevel::Editor,
1058+
"core" => ClassCodegenLevel::Scene,
1059+
_ => {
1060+
// we don't know this classification
1061+
return None;
1062+
}
1063+
}
1064+
}
1065+
};
1066+
Some(level)
10161067
}
10171068

10181069
/// Whether a generated enum is `pub(crate)`; useful for manual re-exports.

godot-codegen/src/util.rs

Lines changed: 15 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -51,33 +51,24 @@ pub fn make_sname_ptr(identifier: &str) -> TokenStream {
5151
}
5252

5353
pub fn get_api_level(class: &JsonClass) -> ClassCodegenLevel {
54-
// Work around wrong classification in https://github.com/godotengine/godot/issues/86206.
55-
fn override_editor(class_name: &str) -> bool {
56-
match class_name {
57-
// https://github.com/godotengine/godot/issues/103867
58-
"OpenXRInteractionProfileEditorBase"
59-
| "OpenXRInteractionProfileEditor"
60-
| "OpenXRBindingModifierEditor" => cfg!(before_api = "4.5"),
61-
62-
// https://github.com/godotengine/godot/issues/86206
63-
"ResourceImporterOggVorbis" | "ResourceImporterMP3" => cfg!(before_api = "4.3"),
64-
65-
_ => false,
66-
}
54+
// NOTE: We have to use a whitelist of known classes because Godot doesn't separate these out
55+
// beyond "editor" and "core" and some classes are also mis-classified in the JSON depending on the Godot version.
56+
if let Some(forced_classification) =
57+
special_cases::classify_codegen_level(&class.name, &class.api_type)
58+
{
59+
return forced_classification;
6760
}
6861

69-
if special_cases::is_class_level_server(&class.name) {
70-
ClassCodegenLevel::Servers
71-
} else if class.api_type == "editor" || override_editor(&class.name) {
72-
ClassCodegenLevel::Editor
73-
} else if class.api_type == "core" {
74-
ClassCodegenLevel::Scene
75-
} else {
76-
panic!(
77-
"class {} has unknown API type {}",
78-
class.name, class.api_type
79-
)
62+
// Fall back to just trusting the categorization. Presently, Godot only reports "core" and "editor".
63+
for level in ClassCodegenLevel::with_tables() {
64+
if level.lower() == class.api_type {
65+
return level;
66+
}
8067
}
68+
panic!(
69+
"class {} has unknown API type {}",
70+
class.name, class.api_type
71+
);
8172
}
8273

8374
pub fn ident(s: &str) -> Ident {

godot-ffi/src/binding/mod.rs

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@
66
*/
77

88
use crate::{
9-
BuiltinLifecycleTable, BuiltinMethodTable, ClassEditorMethodTable, ClassSceneMethodTable,
10-
ClassServersMethodTable, GDExtensionClassLibraryPtr, GDExtensionInterface,
11-
GdextRuntimeMetadata, ManualInitCell, UtilityFunctionTable,
9+
BuiltinLifecycleTable, BuiltinMethodTable, ClassCoreMethodTable, ClassEditorMethodTable,
10+
ClassSceneMethodTable, ClassServersMethodTable, GDExtensionClassLibraryPtr,
11+
GDExtensionInterface, GdextRuntimeMetadata, ManualInitCell, UtilityFunctionTable,
1212
};
1313

1414
#[cfg(feature = "experimental-threads")]
@@ -34,6 +34,7 @@ pub(crate) struct GodotBinding {
3434
interface: GDExtensionInterface,
3535
library: ClassLibraryPtr,
3636
global_method_table: BuiltinLifecycleTable,
37+
class_core_method_table: ManualInitCell<ClassCoreMethodTable>,
3738
class_server_method_table: ManualInitCell<ClassServersMethodTable>,
3839
class_scene_method_table: ManualInitCell<ClassSceneMethodTable>,
3940
class_editor_method_table: ManualInitCell<ClassEditorMethodTable>,
@@ -56,6 +57,7 @@ impl GodotBinding {
5657
interface,
5758
library: ClassLibraryPtr(library),
5859
global_method_table,
60+
class_core_method_table: ManualInitCell::new(),
5961
class_server_method_table: ManualInitCell::new(),
6062
class_scene_method_table: ManualInitCell::new(),
6163
class_editor_method_table: ManualInitCell::new(),
@@ -148,6 +150,20 @@ pub unsafe fn class_servers_api() -> &'static ClassServersMethodTable {
148150
)
149151
}
150152

153+
/// # Safety
154+
///
155+
/// - The Godot binding must have been initialized before calling this function.
156+
/// - The class core method table must have been initialized before calling this function.
157+
///
158+
/// If "experimental-threads" is not enabled, then this must be called from the same thread that the bindings were initialized from.
159+
#[inline(always)]
160+
pub unsafe fn class_core_api() -> &'static ClassCoreMethodTable {
161+
get_table(
162+
&get_binding().class_core_method_table,
163+
"cannot fetch classes; init level 'Core' not yet loaded",
164+
)
165+
}
166+
151167
/// # Safety
152168
///
153169
/// - The Godot binding must have been initialized before calling this function.
@@ -251,6 +267,20 @@ pub(crate) unsafe fn get_binding() -> &'static GodotBinding {
251267
BindingStorage::get_binding_unchecked()
252268
}
253269

270+
/// # Safety
271+
///
272+
/// - The Godot binding must have been initialized before calling this function.
273+
/// - Must only be called once.
274+
///
275+
/// If "experimental-threads" is not enabled, then this must be called from the same thread that the bindings were initialized from.
276+
pub(crate) unsafe fn initialize_class_core_method_table(table: ClassCoreMethodTable) {
277+
initialize_table(
278+
&get_binding().class_core_method_table,
279+
table,
280+
"classes (Core level)",
281+
)
282+
}
283+
254284
/// # Safety
255285
///
256286
/// - The Godot binding must have been initialized before calling this function.

godot-ffi/src/lib.rs

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ pub use gen::interface::*;
8181
// Method tables
8282
pub use gen::table_builtins::*;
8383
pub use gen::table_builtins_lifecycle::*;
84+
pub use gen::table_core_classes::*;
8485
pub use gen::table_editor_classes::*;
8586
pub use gen::table_scene_classes::*;
8687
pub use gen::table_servers_classes::*;
@@ -101,8 +102,9 @@ mod binding;
101102

102103
pub use binding::*;
103104
use binding::{
104-
initialize_binding, initialize_builtin_method_table, initialize_class_editor_method_table,
105-
initialize_class_scene_method_table, initialize_class_server_method_table, runtime_metadata,
105+
initialize_binding, initialize_builtin_method_table, initialize_class_core_method_table,
106+
initialize_class_editor_method_table, initialize_class_scene_method_table,
107+
initialize_class_server_method_table, runtime_metadata,
106108
};
107109

108110
#[cfg(not(wasm_nothreads))]
@@ -313,9 +315,18 @@ pub unsafe fn load_class_method_table(api_level: InitLevel) {
313315
let (class_count, method_count);
314316
match api_level {
315317
InitLevel::Core => {
316-
// Currently we don't need to do anything in `Core`, this may change in the future.
317-
class_count = 0;
318-
method_count = 0;
318+
// SAFETY: The interface has been initialized and this function hasn't been called before.
319+
unsafe {
320+
#[cfg(feature = "codegen-lazy-fptrs")]
321+
initialize_class_core_method_table(ClassCoreMethodTable::load());
322+
#[cfg(not(feature = "codegen-lazy-fptrs"))]
323+
initialize_class_core_method_table(ClassCoreMethodTable::load(
324+
interface,
325+
&mut string_names,
326+
));
327+
}
328+
class_count = ClassCoreMethodTable::CLASS_COUNT;
329+
method_count = ClassCoreMethodTable::METHOD_COUNT;
319330
}
320331
InitLevel::Servers => {
321332
// SAFETY: The interface has been initialized and this function hasn't been called before.
@@ -386,7 +397,7 @@ pub unsafe fn godot_has_feature(
386397
// Issue a raw C call to OS.has_feature(tag_string).
387398

388399
// SAFETY: Called from main thread, interface has been initialized, and the scene api has been initialized.
389-
let method_bind = unsafe { class_scene_api() }.os__has_feature();
400+
let method_bind = unsafe { class_core_api() }.os__has_feature();
390401

391402
// SAFETY: Called from main thread, and interface has been initialized.
392403
let interface = unsafe { get_interface() };

itest/rust/src/lib.rs

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

8+
use std::sync::Mutex;
9+
810
use godot::init::{gdextension, ExtensionLibrary, InitLevel};
911

1012
mod benchmarks;
@@ -18,10 +20,36 @@ mod register_tests;
1820
// ----------------------------------------------------------------------------------------------------------------------------------------------
1921
// Entry point
2022

23+
static LEVELS_SEEN: Mutex<Vec<InitLevel>> = Mutex::new(Vec::new());
24+
2125
#[gdextension(entry_symbol = itest_init)]
2226
unsafe impl ExtensionLibrary for framework::IntegrationTests {
27+
fn min_level() -> InitLevel {
28+
InitLevel::Core
29+
}
2330
fn on_level_init(level: InitLevel) {
24-
// Testing that we can initialize and use `Object`-derived classes during `Servers` init level. See `object_tests::init_level_test`.
25-
object_tests::initialize_init_level_test(level);
31+
LEVELS_SEEN.lock().unwrap().push(level);
32+
match level {
33+
InitLevel::Core => {
34+
// make sure we can access early core singletons
35+
object_tests::test_early_core_singletons();
36+
}
37+
InitLevel::Servers => {}
38+
InitLevel::Scene => {}
39+
InitLevel::Editor => {}
40+
}
41+
}
42+
}
43+
44+
// Ensure that we saw all the init levels expected.
45+
#[crate::framework::itest]
46+
fn observed_all_init_levels() {
47+
let levels_seen = LEVELS_SEEN.lock().unwrap().clone();
48+
assert_eq!(levels_seen[0], InitLevel::Core);
49+
assert_eq!(levels_seen[1], InitLevel::Servers);
50+
assert_eq!(levels_seen[2], InitLevel::Scene);
51+
// NOTE: some tests don't see editor mode
52+
if let Some(level_3) = levels_seen.get(3) {
53+
assert_eq!(*level_3, InitLevel::Editor);
2654
}
2755
}

0 commit comments

Comments
 (0)