From 6ae357a4b4bc93e2ad1438c6489b05ecd9f7dd84 Mon Sep 17 00:00:00 2001 From: Jan Haller Date: Sat, 19 Aug 2023 12:35:02 +0200 Subject: [PATCH] Split method table initialization according to different init levels --- godot-codegen/src/central_generator.rs | 246 +++++++++++-------------- godot-codegen/src/class_generator.rs | 67 ++++--- godot-codegen/src/lib.rs | 4 +- godot-codegen/src/special_cases.rs | 68 ++++++- godot-codegen/src/util.rs | 67 ++++++- godot-core/src/init/mod.rs | 6 +- godot-core/src/lib.rs | 1 + godot-ffi/src/lib.rs | 120 ++++++++++-- godot-ffi/src/toolbox.rs | 106 +++++++++-- 9 files changed, 479 insertions(+), 206 deletions(-) diff --git a/godot-codegen/src/central_generator.rs b/godot-codegen/src/central_generator.rs index d7bf4e7b3..831815b5c 100644 --- a/godot-codegen/src/central_generator.rs +++ b/godot-codegen/src/central_generator.rs @@ -9,10 +9,12 @@ use quote::{format_ident, quote, ToTokens}; use std::collections::HashMap; use std::path::Path; +use crate::api_parser::*; use crate::class_generator::{is_builtin_method_excluded, is_class_excluded, is_method_excluded}; -use crate::util::{option_as_slice, to_pascal_case, to_rust_type, to_snake_case}; -use crate::{api_parser::*, SubmitFn}; -use crate::{ident, util, Context}; +use crate::util::{ + option_as_slice, to_pascal_case, to_rust_type, to_snake_case, ClassCodegenLevel, +}; +use crate::{ident, special_cases, util, Context, SubmitFn, TyName}; struct CentralItems { opaque_types: [Vec; 2], @@ -27,11 +29,14 @@ struct CentralItems { godot_version: Header, } -#[derive(Default)] -struct ClassMethodItems { +struct MethodTableInfo { + table_name: Ident, + ctor_parameters: TokenStream, pre_init_code: TokenStream, method_decls: Vec, method_inits: Vec, + class_count: usize, + method_count: usize, } pub struct TypeNames { @@ -79,48 +84,44 @@ pub(crate) fn generate_sys_classes_file( sys_gen_path: &Path, submit_fn: &mut SubmitFn, ) { - let ClassMethodItems { + for api_level in ClassCodegenLevel::with_tables() { + let code = make_class_method_table(api, api_level, ctx); + let filename = api_level.table_file(); + + submit_fn(sys_gen_path.join(filename), code); + } +} + +/// Generate code for a method table based on shared layout. +fn make_method_table(info: MethodTableInfo) -> TokenStream { + let MethodTableInfo { + table_name, + ctor_parameters, pre_init_code, method_decls, method_inits, - } = make_class_method_items(api, ctx); + class_count, + method_count, + } = info; - let code = quote! { - use crate as sys; - - type MethodBind = sys::GDExtensionMethodBindPtr; - - fn unwrap_fn_ptr( - method: MethodBind, - class_name: &str, - method_name: &str, - hash: i64, - ) -> MethodBind { - crate::out!("Load class method {}::{} (hash {})...", class_name, method_name, hash); - if method.is_null() { - panic!( - "Failed to load class method {}::{} (hash {}).\n\ - Make sure gdext and Godot are compatible: https://godot-rust.github.io/book/gdext/advanced/compatibility.html", - class_name, - method_name, - hash - ) - } - - method - } + // Editor table can be empty, if the Godot binary is compiled without editor. + let unused_attr = method_decls + .is_empty() + .then(|| quote! { #[allow(unused_variables)] }); + quote! { #[allow(non_snake_case)] - pub struct ClassMethodTable { + pub struct #table_name { #( #method_decls, )* } - impl ClassMethodTable { - // #[allow]: some classes have no own methods (only inherited ones), so their StringNames are never referenced. - #[allow(unused_variables)] + impl #table_name { + pub const CLASS_COUNT: usize = #class_count; + pub const METHOD_COUNT: usize = #method_count; + + #unused_attr pub fn load( - interface: &crate::GDExtensionInterface, - string_names: &mut crate::StringCache, + #ctor_parameters ) -> Self { #pre_init_code @@ -129,9 +130,7 @@ pub(crate) fn generate_sys_classes_file( } } } - }; - - submit_fn(sys_gen_path.join("classes.rs"), code); + } } pub(crate) fn generate_sys_builtins_file( @@ -143,73 +142,8 @@ pub(crate) fn generate_sys_builtins_file( // TODO merge this and the one in central.rs, to only collect once let builtin_types_map = collect_builtin_types(api); - let ClassMethodItems { - pre_init_code, - method_decls, - method_inits, - } = make_builtin_method_items(api, &builtin_types_map); - - let code = quote! { - use crate as sys; - - // GDExtensionPtrBuiltInMethod - type BuiltinMethodBind = unsafe extern "C" fn( - p_base: sys::GDExtensionTypePtr, - p_args: *const sys::GDExtensionConstTypePtr, - r_return: sys::GDExtensionTypePtr, - p_argument_count: ::std::os::raw::c_int, - ); - - fn unwrap_fn_ptr( - method: sys::GDExtensionPtrBuiltInMethod, - variant_type: &str, - method_name: &str, - hash: i64, - ) -> BuiltinMethodBind { - crate::out!("Load builtin method {}::{} (hash {})...", variant_type, method_name, hash); - method.unwrap_or_else(|| { - panic!( - "Failed to load builtin method {}::{} (hash {}).\n\ - Make sure gdext and Godot are compatible: https://godot-rust.github.io/book/gdext/advanced/compatibility.html", - variant_type, - method_name, - hash - ) - }) - } - - #[allow(non_snake_case)] - pub struct BuiltinMethodTable { - #( #method_decls, )* - } - - impl BuiltinMethodTable { - pub fn load( - interface: &crate::GDExtensionInterface, - string_names: &mut crate::StringCache, - ) -> Self { - #pre_init_code - - Self { - #( #method_inits, )* - } - } - } - }; - - submit_fn(sys_gen_path.join("builtin_classes.rs"), code); -} - -pub(crate) fn generate_sys_mod_file(core_gen_path: &Path, submit_fn: &mut SubmitFn) { - let code = quote! { - pub mod builtin_classes; - pub mod central; - pub mod classes; - pub mod gdextension_interface; - pub mod interface; - }; - - submit_fn(core_gen_path.join("mod.rs"), code); + let code = make_builtin_method_table(api, &builtin_types_map); + submit_fn(sys_gen_path.join("table_builtin_types.rs"), code); } pub(crate) fn generate_core_mod_file(gen_path: &Path, submit_fn: &mut SubmitFn) { @@ -556,40 +490,76 @@ fn make_central_items( result } -fn make_class_method_items(api: &ExtensionApi, ctx: &mut Context) -> ClassMethodItems { - let mut items = ClassMethodItems::default(); - let mut class_inits = Vec::new(); +fn make_class_method_table( + api: &ExtensionApi, + api_level: ClassCodegenLevel, + ctx: &mut Context, +) -> TokenStream { + let mut table = MethodTableInfo { + table_name: api_level.table_struct(), + ctor_parameters: quote! { + interface: &crate::GDExtensionInterface, + string_names: &mut crate::StringCache, + }, + pre_init_code: TokenStream::new(), // late-init, depends on class string names + method_decls: vec![], + method_inits: vec![], + class_count: 0, + method_count: 0, + }; + let mut class_sname_decls = Vec::new(); for class in api.classes.iter() { - if is_class_excluded(&class.name) { + if special_cases::is_class_deleted(&TyName::from_godot(&class.name)) + || is_class_excluded(&class.name) + || util::get_api_level(class) != api_level + { continue; } let class_var = format_ident!("sname_{}", &class.name); let initializer_expr = util::make_sname_ptr(&class.name); - if populate_class_methods(&mut items, class, &class_var, ctx) { - // Only create class variable if there any methods have been added. - class_inits.push(quote! { + let prev_method_count = table.method_count; + populate_class_methods(&mut table, class, &class_var, ctx); + if table.method_count > prev_method_count { + // Only create class variable if any methods have been added. + class_sname_decls.push(quote! { let #class_var = #initializer_expr; }); } + + table.class_count += 1; } - items.pre_init_code = quote! { + table.pre_init_code = quote! { let get_method_bind = interface.classdb_get_method_bind.expect("classdb_get_method_bind absent"); - #( #class_inits )* + #( #class_sname_decls )* }; - items + make_method_table(table) } -fn make_builtin_method_items( +fn make_builtin_method_table( api: &ExtensionApi, builtin_types_map: &HashMap>, -) -> ClassMethodItems { - let mut items = ClassMethodItems::default(); +) -> TokenStream { + let mut table = MethodTableInfo { + table_name: ident("BuiltinMethodTable"), + ctor_parameters: quote! { + interface: &crate::GDExtensionInterface, + string_names: &mut crate::StringCache, + }, + pre_init_code: quote! { + use crate as sys; + let get_builtin_method = interface.variant_get_ptr_builtin_method.expect("variant_get_ptr_builtin_method absent"); + }, + method_decls: vec![], + method_inits: vec![], + class_count: 0, + method_count: 0, + }; for builtin in api.builtin_classes.iter() { println!("builtin: {}", builtin.name); @@ -597,24 +567,21 @@ fn make_builtin_method_items( continue // for Nil }; - populate_builtin_methods(&mut items, builtin, &builtin_type.type_names); + populate_builtin_methods(&mut table, builtin, &builtin_type.type_names); + table.class_count += 1; } - items.pre_init_code = quote! { - let get_builtin_method = interface.variant_get_ptr_builtin_method.expect("variant_get_ptr_builtin_method absent"); - }; - items + make_method_table(table) } /// Returns whether at least 1 method was added. fn populate_class_methods( - items: &mut ClassMethodItems, + table: &mut MethodTableInfo, class: &Class, class_var: &Ident, ctx: &mut Context, -) -> bool { +) { let class_name_str = class.name.as_str(); - let mut methods_added = false; for method in option_as_slice(&class.methods) { if is_method_excluded(method, false, ctx) { @@ -626,16 +593,14 @@ fn populate_class_methods( let method_decl = make_class_method_decl(&method_field); let method_init = make_class_method_init(method, &method_field, class_var, class_name_str); - items.method_decls.push(method_decl); - items.method_inits.push(method_init); - methods_added = true; + table.method_decls.push(method_decl); + table.method_inits.push(method_init); + table.method_count += 1; } - - methods_added } fn populate_builtin_methods( - items: &mut ClassMethodItems, + table: &mut MethodTableInfo, builtin_class: &BuiltinClass, type_name: &TypeNames, ) { @@ -649,14 +614,15 @@ fn populate_builtin_methods( let method_decl = make_builtin_method_decl(method, &method_field); let method_init = make_builtin_method_init(method, &method_field, type_name); - items.method_decls.push(method_decl); - items.method_inits.push(method_init); + table.method_decls.push(method_decl); + table.method_inits.push(method_init); + table.method_count += 1; } } fn make_class_method_decl(method_field: &Ident) -> TokenStream { // Note: varcall/ptrcall is only decided at call time; the method bind is the same for both. - quote! { pub #method_field: MethodBind } + quote! { pub #method_field: crate::ClassMethodBind } } fn make_class_method_init( @@ -680,13 +646,13 @@ fn make_class_method_init( let method_bind = unsafe { get_method_bind(#class_var, #method_sname, #hash) }; - unwrap_fn_ptr(method_bind, #class_name_str, #method_name_str, #hash) + crate::validate_class_method(method_bind, #class_name_str, #method_name_str, #hash) } } } fn make_builtin_method_decl(_method: &BuiltinClassMethod, method_field: &Ident) -> TokenStream { - quote! { pub #method_field: BuiltinMethodBind } + quote! { pub #method_field: crate::BuiltinMethodBind } } fn make_builtin_method_init( @@ -712,7 +678,7 @@ fn make_builtin_method_init( let method_bind = unsafe { get_builtin_method(sys::#variant_type, #method_sname, #hash) }; - unwrap_fn_ptr(method_bind, #variant_type_str, #method_name_str, #hash) + crate::validate_builtin_method(method_bind, #variant_type_str, #method_name_str, #hash) } } } diff --git a/godot-codegen/src/class_generator.rs b/godot-codegen/src/class_generator.rs index f1711007a..9a6faf60a 100644 --- a/godot-codegen/src/class_generator.rs +++ b/godot-codegen/src/class_generator.rs @@ -12,6 +12,7 @@ use std::path::Path; use crate::central_generator::{collect_builtin_types, BuiltinTypeInfo}; use crate::context::NotificationEnum; +use crate::special_cases::is_class_deleted; use crate::util::{ ident, make_string_name, option_as_slice, parse_native_structures_format, safe_ident, to_pascal_case, to_rust_expr, to_rust_type, to_rust_type_abi, to_snake_case, @@ -497,10 +498,17 @@ fn make_class(class: &Class, class_name: &TyName, ctx: &mut Context) -> Generate }; let constructor = make_constructor(class, ctx); + let get_method_table = util::get_api_level(class).table_global_getter(); + let FnDefinitions { functions: methods, builders, - } = make_methods(option_as_slice(&class.methods), class_name, ctx); + } = make_methods( + option_as_slice(&class.methods), + class_name, + &get_method_table, + ctx, + ); let enums = make_enums(option_as_slice(&class.enums), class_name, ctx); let constants = make_constants(option_as_slice(&class.constants), class_name, ctx); @@ -1001,10 +1009,15 @@ fn make_builtin_module_file(classes_and_modules: Vec) -> } } -fn make_methods(methods: &[ClassMethod], class_name: &TyName, ctx: &mut Context) -> FnDefinitions { +fn make_methods( + methods: &[ClassMethod], + class_name: &TyName, + get_method_table: &Ident, + ctx: &mut Context, +) -> FnDefinitions { let definitions = methods .iter() - .map(|method| make_method_definition(method, class_name, ctx)); + .map(|method| make_method_definition(method, class_name, get_method_table, ctx)); FnDefinitions::expand(definitions) } @@ -1100,28 +1113,37 @@ fn is_type_excluded(ty: &str, ctx: &mut Context) -> bool { pub(crate) fn is_method_excluded( method: &ClassMethod, is_virtual_impl: bool, - #[allow(unused_variables)] ctx: &mut Context, + ctx: &mut Context, ) -> bool { - // Currently excluded: - // - // * Private virtual methods are only included in a virtual - // implementation. - - // -- FIXME remove when impl complete - #[cfg(not(feature = "codegen-full"))] - if method - .return_value - .as_ref() - .map_or(false, |ret| is_type_excluded(ret.type_.as_str(), ctx)) - || method.arguments.as_ref().map_or(false, |args| { - args.iter() - .any(|arg| is_type_excluded(arg.type_.as_str(), ctx)) - }) - { + let is_arg_or_return_excluded = |ty: &str, _ctx: &mut Context| { + let class_deleted = is_class_deleted(&TyName::from_godot(ty)); + + #[cfg(not(feature = "codegen-full"))] + { + class_deleted || is_type_excluded(ty, _ctx) + } + #[cfg(feature = "codegen-full")] + { + class_deleted + } + }; + + // Exclude if return type contains an excluded type. + if method.return_value.as_ref().map_or(false, |ret| { + is_arg_or_return_excluded(ret.type_.as_str(), ctx) + }) { + return true; + } + + // Exclude if any argument contains an excluded type. + if method.arguments.as_ref().map_or(false, |args| { + args.iter() + .any(|arg| is_arg_or_return_excluded(arg.type_.as_str(), ctx)) + }) { return true; } - // -- end. + // Virtual methods are not part of the class API itself, but exposed as an accompanying trait. if !is_virtual_impl && method.name.starts_with('_') { return true; } @@ -1149,6 +1171,7 @@ fn is_function_excluded(function: &UtilityFunction, ctx: &mut Context) -> bool { fn make_method_definition( method: &ClassMethod, class_name: &TyName, + get_method_table: &Ident, ctx: &mut Context, ) -> FnDefinition { if is_method_excluded(method, false, ctx) || special_cases::is_deleted(class_name, &method.name) @@ -1187,7 +1210,7 @@ fn make_method_definition( let fn_ptr = util::make_class_method_ptr_name(&class_name.godot_ty, method); let init_code = quote! { - let __method_bind = sys::class_method_table().#fn_ptr; + let __method_bind = sys::#get_method_table().#fn_ptr; let __call_fn = #function_provider; }; diff --git a/godot-codegen/src/lib.rs b/godot-codegen/src/lib.rs index 7cb85b8ad..dd8bd07b9 100644 --- a/godot-codegen/src/lib.rs +++ b/godot-codegen/src/lib.rs @@ -19,7 +19,7 @@ mod tests; use api_parser::{load_extension_api, ExtensionApi}; use central_generator::{ generate_core_central_file, generate_core_mod_file, generate_sys_central_file, - generate_sys_classes_file, generate_sys_mod_file, + generate_sys_classes_file, }; use class_generator::{ generate_builtin_class_files, generate_class_files, generate_native_structures_files, @@ -60,8 +60,6 @@ pub fn generate_sys_files( h_path: &Path, watch: &mut godot_bindings::StopWatch, ) { - generate_sys_mod_file(sys_gen_path, &mut submit_fn); - let (api, build_config) = load_extension_api(watch); let mut ctx = Context::build_from_api(&api); watch.record("build_context"); diff --git a/godot-codegen/src/special_cases.rs b/godot-codegen/src/special_cases.rs index f2d286a9f..3e3ed8b77 100644 --- a/godot-codegen/src/special_cases.rs +++ b/godot-codegen/src/special_cases.rs @@ -38,12 +38,78 @@ pub(crate) fn is_deleted(class_name: &TyName, godot_method_name: &str) -> bool { #[rustfmt::skip] pub(crate) fn is_class_deleted(class_name: &TyName) -> bool { + // Exclude experimental classes for now; possibly feature-gate them in the future. + if is_class_experimental(class_name) { + return true; + } + match class_name.godot_ty.as_str() { - // Thread APIs + // Hardcoded cases that are not accessible. + | "JavaClassWrapper" // only on Android. + | "JavaScriptBridge" // only on WASM. + | "ThemeDB" // lazily loaded; TODO enable this. + + // Thread APIs. | "Thread" | "Mutex" | "Semaphore" + // Internal classes that were removed in https://github.com/godotengine/godot/pull/80852, but are still available for API < 4.2. + | "FramebufferCacheRD" + | "GDScriptEditorTranslationParserPlugin" + | "GDScriptNativeClass" + | "GLTFDocumentExtensionPhysics" + | "GLTFDocumentExtensionTextureWebP" + | "GodotPhysicsServer2D" + | "GodotPhysicsServer3D" + | "IPUnix" + | "MovieWriterMJPEG" + | "MovieWriterPNGWAV" + | "ResourceFormatImporterSaver" + | "UniformSetCacheRD" + + => true, _ => false + } +} + +#[rustfmt::skip] +fn is_class_experimental(class_name: &TyName) -> bool { + // These classes are currently hardcoded, but the information is available in Godot's doc/classes directory. + // The XML file contains a property . + + match class_name.godot_ty.as_str() { + | "GraphEdit" + | "GraphNode" + | "NavigationAgent2D" + | "NavigationAgent3D" + | "NavigationLink2D" + | "NavigationLink3D" + | "NavigationMesh" + | "NavigationMeshSourceGeometryData3D" + | "NavigationObstacle2D" + | "NavigationObstacle3D" + | "NavigationPathQueryParameters2D" + | "NavigationPathQueryParameters3D" + | "NavigationPathQueryResult2D" + | "NavigationPathQueryResult3D" + | "NavigationPolygon" + | "NavigationRegion2D" + | "NavigationRegion3D" + | "NavigationServer2D" + | "NavigationServer3D" + | "ProjectSettings" + | "SkeletonModification2D" + | "SkeletonModification2DCCDIK" + | "SkeletonModification2DFABRIK" + | "SkeletonModification2DJiggle" + | "SkeletonModification2DLookAt" + | "SkeletonModification2DPhysicalBones" + | "SkeletonModification2DStackHolder" + | "SkeletonModification2DTwoBoneIK" + | "SkeletonModificationStack2D" + | "StreamPeerGZIP" + | "TextureRect" + => true, _ => false } } diff --git a/godot-codegen/src/util.rs b/godot-codegen/src/util.rs index 4c646a955..3c14ba92a 100644 --- a/godot-codegen/src/util.rs +++ b/godot-codegen/src/util.rs @@ -4,7 +4,7 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -use crate::api_parser::{BuiltinClassMethod, ClassConstant, ClassMethod, Enum}; +use crate::api_parser::{BuiltinClassMethod, Class, ClassConstant, ClassMethod, Enum}; use crate::central_generator::TypeNames; use crate::special_cases::is_builtin_scalar; use crate::{Context, GodotTy, ModName, RustTy, TyName}; @@ -17,6 +17,53 @@ pub struct NativeStructuresField { pub field_name: String, } +/// At which stage a class function pointer is loaded. +#[derive(Copy, Clone, Eq, PartialEq)] +pub enum ClassCodegenLevel { + Servers, + Scene, + Editor, + + /// Not pre-fetched because Godot does not load them in time. + Lazy, +} + +impl ClassCodegenLevel { + pub fn with_tables() -> [Self; 3] { + [Self::Servers, Self::Scene, Self::Editor] + } + + pub fn table_global_getter(self) -> Ident { + format_ident!("class_{}_api", self.lower()) + } + + pub fn table_file(self) -> String { + format!("table_{}_classes.rs", self.lower()) + } + + pub fn table_struct(self) -> Ident { + format_ident!("Class{}MethodTable", self.upper()) + } + + fn lower(self) -> &'static str { + match self { + Self::Servers => "servers", + Self::Scene => "scene", + Self::Editor => "editor", + Self::Lazy => unreachable!("lazy classes should be deleted at the moment"), + } + } + + fn upper(self) -> &'static str { + match self { + Self::Servers => "Servers", + Self::Scene => "Scene", + Self::Editor => "Editor", + Self::Lazy => unreachable!("lazy classes should be deleted at the moment"), + } + } +} + /// Small utility that turns an optional vector (often encountered as JSON deserialization type) into a slice. pub fn option_as_slice(option: &Option>) -> &[T] { option.as_ref().map_or(&[], Vec::as_slice) @@ -51,6 +98,24 @@ pub fn make_sname_ptr(identifier: &str) -> TokenStream { } } +pub fn get_api_level(class: &Class) -> ClassCodegenLevel { + if class.name == "ThemeDB" { + // registered in C++ register_scene_singletons(), after MODULE_INITIALIZATION_LEVEL_EDITOR happens. + ClassCodegenLevel::Lazy + } else if class.name.ends_with("Server") { + ClassCodegenLevel::Servers + } else if class.api_type == "core" { + ClassCodegenLevel::Scene + } else if class.api_type == "editor" { + ClassCodegenLevel::Editor + } else { + panic!( + "class {} has unknown API type {}", + class.name, class.api_type + ) + } +} + pub fn make_enum_definition(enum_: &Enum) -> TokenStream { // TODO enums which have unique ords could be represented as Rust enums // This would allow exhaustive matches (or at least auto-completed matches + #[non_exhaustive]). But even without #[non_exhaustive], diff --git a/godot-core/src/init/mod.rs b/godot-core/src/init/mod.rs index e7bfd4044..c5f718d79 100644 --- a/godot-core/src/init/mod.rs +++ b/godot-core/src/init/mod.rs @@ -57,7 +57,8 @@ unsafe extern "C" fn ffi_initialize_layer( let level = InitLevel::from_sys(init_level); let ctx = || format!("failed to initialize GDExtension level `{:?}`", level); - crate::private::handle_panic(ctx, || { + // Swallow panics. TODO consider crashing if gdext init fails. + let _ = crate::private::handle_panic(ctx, || { gdext_on_level_init(level); E::on_level_init(level); }); @@ -70,7 +71,8 @@ unsafe extern "C" fn ffi_deinitialize_layer( let level = InitLevel::from_sys(init_level); let ctx = || format!("failed to deinitialize GDExtension level `{:?}`", level); - crate::private::handle_panic(ctx, || { + // Swallow panics. + let _ = crate::private::handle_panic(ctx, || { E::on_level_deinit(level); gdext_on_level_deinit(level); }); diff --git a/godot-core/src/lib.rs b/godot-core/src/lib.rs index e43218219..026eae1f2 100644 --- a/godot-core/src/lib.rs +++ b/godot-core/src/lib.rs @@ -110,6 +110,7 @@ pub mod private { /// Executes `code`. If a panic is thrown, it is caught and an error message is printed to Godot. /// /// Returns `None` if a panic occurred, and `Some(result)` with the result of `code` otherwise. + #[must_use] pub fn handle_panic(error_context: E, code: F) -> Option where E: FnOnce() -> S, diff --git a/godot-ffi/src/lib.rs b/godot-ffi/src/lib.rs index 49c60bfec..6921d0e98 100644 --- a/godot-ffi/src/lib.rs +++ b/godot-ffi/src/lib.rs @@ -17,7 +17,17 @@ deref_nullptr, clippy::redundant_static_lifetimes )] -pub(crate) mod gen; +pub(crate) mod gen { + pub mod table_builtin_types; + pub mod table_servers_classes; + pub mod table_scene_classes; + pub mod table_editor_classes; + + pub mod central; + pub mod gdextension_interface; + pub mod interface; + +} mod compat; mod gdextension_plus; @@ -40,10 +50,16 @@ pub use crate::godot_ffi::{ from_sys_init_or_init_default, GodotFfi, GodotFuncMarshal, GodotNullablePtr, PrimitiveConversionError, PtrcallType, }; + +// Method tables +pub use gen::table_builtin_types::*; +pub use gen::table_editor_classes::*; +pub use gen::table_scene_classes::*; +pub use gen::table_servers_classes::*; + +// Other pub use gdextension_plus::*; -pub use gen::builtin_classes::*; pub use gen::central::*; -pub use gen::classes::*; pub use gen::gdextension_interface::*; pub use gen::interface::*; pub use string_cache::StringCache; @@ -52,11 +68,20 @@ pub use toolbox::*; // ---------------------------------------------------------------------------------------------------------------------------------------------- // API to access Godot via FFI +#[derive(Debug)] +pub enum ClassApiLevel { + Server, + Scene, + Editor, +} + struct GodotBinding { interface: GDExtensionInterface, library: GDExtensionClassLibraryPtr, global_method_table: GlobalMethodTable, - class_method_table: Option, // late-init + class_server_method_table: Option, // late-init + class_scene_method_table: Option, // late-init + class_editor_method_table: Option, // late-init builtin_method_table: BuiltinMethodTable, runtime_metadata: GdextRuntimeMetadata, config: GdextConfig, @@ -123,7 +148,9 @@ pub unsafe fn initialize( interface, global_method_table, builtin_method_table, - class_method_table: None, + class_server_method_table: None, + class_scene_method_table: None, + class_editor_method_table: None, library, runtime_metadata, config, @@ -173,9 +200,40 @@ pub unsafe fn method_table() -> &'static GlobalMethodTable { /// /// The interface must have been initialised with [`initialize`] before calling this function. #[inline(always)] -pub unsafe fn class_method_table() -> &'static ClassMethodTable { - let table = &unwrap_ref_unchecked(&BINDING).class_method_table; - debug_assert!(table.is_some(), "class method table not loaded"); +pub unsafe fn class_servers_api() -> &'static ClassServersMethodTable { + let table = &unwrap_ref_unchecked(&BINDING).class_server_method_table; + debug_assert!( + table.is_some(), + "cannot fetch classes; init level 'Servers' not yet loaded" + ); + + table.as_ref().unwrap_unchecked() +} + +/// # Safety +/// +/// The interface must have been initialised with [`initialize`] before calling this function. +#[inline(always)] +pub unsafe fn class_scene_api() -> &'static ClassSceneMethodTable { + let table = &unwrap_ref_unchecked(&BINDING).class_scene_method_table; + debug_assert!( + table.is_some(), + "cannot fetch classes; init level 'Scene' not yet loaded" + ); + + table.as_ref().unwrap_unchecked() +} + +/// # Safety +/// +/// The interface must have been initialised with [`initialize`] before calling this function. +#[inline(always)] +pub unsafe fn class_editor_api() -> &'static ClassEditorMethodTable { + let table = &unwrap_ref_unchecked(&BINDING).class_editor_method_table; + debug_assert!( + table.is_some(), + "cannot fetch classes; init level 'Editor' not yet loaded" + ); table.as_ref().unwrap_unchecked() } @@ -192,21 +250,49 @@ pub unsafe fn builtin_method_table() -> &'static BuiltinMethodTable { /// /// The interface must have been initialised with [`initialize`] before calling this function. #[inline(always)] -pub unsafe fn load_class_method_table() { +pub unsafe fn load_class_method_table(api_level: ClassApiLevel) { let binding = unwrap_ref_unchecked_mut(&mut BINDING); - out!("Load class method table..."); + out!("Load class method table for level '{:?}'...", api_level); let begin = std::time::Instant::now(); - let class_method_table = { - let mut string_names = StringCache::new(&binding.interface, &binding.global_method_table); - ClassMethodTable::load(&binding.interface, &mut string_names) - }; + let mut string_names = StringCache::new(&binding.interface, &binding.global_method_table); + let (class_count, method_count); + match api_level { + ClassApiLevel::Server => { + binding.class_server_method_table = Some(ClassServersMethodTable::load( + &binding.interface, + &mut string_names, + )); + class_count = ClassServersMethodTable::CLASS_COUNT; + method_count = ClassServersMethodTable::METHOD_COUNT; + } + ClassApiLevel::Scene => { + binding.class_scene_method_table = Some(ClassSceneMethodTable::load( + &binding.interface, + &mut string_names, + )); + class_count = ClassSceneMethodTable::CLASS_COUNT; + method_count = ClassSceneMethodTable::METHOD_COUNT; + } + ClassApiLevel::Editor => { + binding.class_editor_method_table = Some(ClassEditorMethodTable::load( + &binding.interface, + &mut string_names, + )); + class_count = ClassEditorMethodTable::CLASS_COUNT; + method_count = ClassEditorMethodTable::METHOD_COUNT; + } + } let _elapsed = std::time::Instant::now() - begin; - - out!("Loaded class method table in {}s.", _elapsed.as_secs_f64()); - binding.class_method_table = Some(class_method_table); + out!( + "{:?} level: loaded {} classes and {} methods in {}s.", + api_level, + class_count, + method_count, + _elapsed.as_secs_f64() + ); } /// # Safety diff --git a/godot-ffi/src/toolbox.rs b/godot-ffi/src/toolbox.rs index b42403770..60b078255 100644 --- a/godot-ffi/src/toolbox.rs +++ b/godot-ffi/src/toolbox.rs @@ -6,6 +6,8 @@ //! Functions and macros that are not very specific to gdext, but come in handy. +use crate as sys; + // ---------------------------------------------------------------------------------------------------------------------------------------------- // Macros @@ -80,26 +82,6 @@ macro_rules! debug_assert_godot { // ---------------------------------------------------------------------------------------------------------------------------------------------- // Utility functions -/// Combination of `as_ref()` and `unwrap_unchecked()`, but without the case differentiation in -/// the former (thus raw pointer access in release mode) -pub(crate) unsafe fn unwrap_ref_unchecked(opt: &Option) -> &T { - debug_assert_godot!(opt.is_some()); - - match opt { - Some(ref val) => val, - None => std::hint::unreachable_unchecked(), - } -} - -pub(crate) unsafe fn unwrap_ref_unchecked_mut(opt: &mut Option) -> &mut T { - debug_assert_godot!(opt.is_some()); - - match opt { - Some(ref mut val) => val, - None => std::hint::unreachable_unchecked(), - } -} - /// Extract value from box before `into_inner()` is stable #[allow(clippy::boxed_local)] // false positive pub fn unbox(value: Box) -> T { @@ -174,3 +156,87 @@ impl Inner for Option { self.expect(error_msg) } } + +// ---------------------------------------------------------------------------------------------------------------------------------------------- +// Crate-wide support + +// ---------------------------------------------------------------------------------------------------------------------------------------------- +// Utility functions + +/// Combination of `as_ref()` and `unwrap_unchecked()`, but without the case differentiation in +/// the former (thus raw pointer access in release mode) +pub(crate) unsafe fn unwrap_ref_unchecked(opt: &Option) -> &T { + debug_assert_godot!(opt.is_some()); + + match opt { + Some(ref val) => val, + None => std::hint::unreachable_unchecked(), + } +} + +pub(crate) unsafe fn unwrap_ref_unchecked_mut(opt: &mut Option) -> &mut T { + debug_assert_godot!(opt.is_some()); + + match opt { + Some(ref mut val) => val, + None => std::hint::unreachable_unchecked(), + } +} + +pub(crate) type ClassMethodBind = sys::GDExtensionMethodBindPtr; + +pub(crate) fn validate_class_method( + method: ClassMethodBind, + class_name: &str, + method_name: &str, + hash: i64, +) -> ClassMethodBind { + /*crate::out!( + "Load class method {}::{} (hash {})...", + class_name, + method_name, + hash + );*/ + if method.is_null() { + panic!( + "Failed to load class method {}::{} (hash {}).\n\ + Make sure gdext and Godot are compatible: https://godot-rust.github.io/book/gdext/advanced/compatibility.html", + class_name, + method_name, + hash + ) + } + + method +} + +// GDExtensionPtrBuiltInMethod +pub(crate) type BuiltinMethodBind = unsafe extern "C" fn( + p_base: sys::GDExtensionTypePtr, + p_args: *const sys::GDExtensionConstTypePtr, + r_return: sys::GDExtensionTypePtr, + p_argument_count: std::os::raw::c_int, +); + +pub(crate) fn validate_builtin_method( + method: sys::GDExtensionPtrBuiltInMethod, + variant_type: &str, + method_name: &str, + hash: i64, +) -> BuiltinMethodBind { + /*crate::out!( + "Load builtin method {}::{} (hash {})...", + variant_type, + method_name, + hash + );*/ + method.unwrap_or_else(|| { + panic!( + "Failed to load builtin method {}::{} (hash {}).\n\ + Make sure gdext and Godot are compatible: https://godot-rust.github.io/book/gdext/advanced/compatibility.html", + variant_type, + method_name, + hash + ) + }) +}