From 1fc5a9b7b8a423be5413f245e73b2e9c6e94ee0a Mon Sep 17 00:00:00 2001 From: Jan Haller Date: Sun, 10 Sep 2023 21:41:46 +0200 Subject: [PATCH] Class and builtin function tables now use giant arrays instead of named fields Should reduce the amount of generated code and thus compile time, while providing the same functionality. This is not yet done for utility functions and builtin lifecycle methods, as those have a higher chance of being used via names, and they are not that big in comparison. --- godot-codegen/src/central_generator.rs | 292 +++++++++++++++++++------ godot-codegen/src/class_generator.rs | 84 ++++--- godot-codegen/src/context.rs | 152 ++++++++++--- godot-codegen/src/lib.rs | 7 +- godot-codegen/src/special_cases.rs | 31 ++- godot-codegen/src/util.rs | 97 ++++++-- godot-core/src/lib.rs | 3 + godot-ffi/src/toolbox.rs | 98 ++++++--- 8 files changed, 568 insertions(+), 196 deletions(-) diff --git a/godot-codegen/src/central_generator.rs b/godot-codegen/src/central_generator.rs index 92b526614..be3cd8620 100644 --- a/godot-codegen/src/central_generator.rs +++ b/godot-codegen/src/central_generator.rs @@ -5,13 +5,15 @@ */ use proc_macro2::{Ident, Literal, TokenStream}; -use quote::{format_ident, quote, ToTokens}; +use quote::{format_ident, quote, ToTokens, TokenStreamExt}; use std::collections::HashMap; +use std::hash::Hasher; use std::path::Path; use crate::api_parser::*; use crate::util::{ - option_as_slice, to_pascal_case, to_rust_type, to_snake_case, ClassCodegenLevel, + make_builtin_method_ptr_name, make_class_method_ptr_name, option_as_slice, to_pascal_case, + to_rust_type, to_snake_case, ClassCodegenLevel, MethodTableKey, }; use crate::{codegen_special_cases, ident, special_cases, util, Context, SubmitFn, TyName}; @@ -26,7 +28,7 @@ struct CentralItems { godot_version: Header, } -struct MethodTableInfo { +struct NamedMethodTable { table_name: Ident, imports: TokenStream, ctor_parameters: TokenStream, @@ -37,6 +39,40 @@ struct MethodTableInfo { method_count: usize, } +struct IndexedMethodTable { + table_name: Ident, + imports: TokenStream, + ctor_parameters: TokenStream, + pre_init_code: TokenStream, + fptr_type: TokenStream, + method_inits: Vec, + named_accessors: Vec, + class_count: usize, + method_count: usize, +} + +// ---------------------------------------------------------------------------------------------------------------------------------------------- + +struct MethodInit { + method_init: TokenStream, + index: usize, +} + +impl ToTokens for MethodInit { + fn to_tokens(&self, tokens: &mut TokenStream) { + self.method_init.to_tokens(tokens); + } +} + +// ---------------------------------------------------------------------------------------------------------------------------------------------- + +struct AccessorMethod { + name: Ident, + index: usize, +} + +// ---------------------------------------------------------------------------------------------------------------------------------------------- + pub struct TypeNames { /// Name in JSON: "int" or "PackedVector2Array" pub json_builtin_name: String, @@ -51,6 +87,22 @@ pub struct TypeNames { pub sys_variant_type: Ident, } +impl Eq for TypeNames {} + +impl PartialEq for TypeNames { + fn eq(&self, other: &Self) -> bool { + self.json_builtin_name == other.json_builtin_name + } +} + +impl std::hash::Hash for TypeNames { + fn hash(&self, state: &mut H) { + self.json_builtin_name.hash(state); + } +} + +// ---------------------------------------------------------------------------------------------------------------------------------------------- + /// Allows collecting all builtin TypeNames before generating methods pub(crate) struct BuiltinTypeInfo<'a> { pub value: i32, @@ -63,6 +115,8 @@ pub(crate) struct BuiltinTypeInfo<'a> { pub operators: Option<&'a Vec>, } +// ---------------------------------------------------------------------------------------------------------------------------------------------- + pub(crate) struct BuiltinTypeMap<'a> { map: HashMap>, } @@ -86,6 +140,8 @@ impl<'a> BuiltinTypeMap<'a> { } } +// ---------------------------------------------------------------------------------------------------------------------------------------------- + pub(crate) fn generate_sys_central_file( api: &ExtensionApi, ctx: &mut Context, @@ -102,9 +158,9 @@ pub(crate) fn generate_sys_central_file( pub(crate) fn generate_sys_classes_file( api: &ExtensionApi, - ctx: &mut Context, sys_gen_path: &Path, watch: &mut godot_bindings::StopWatch, + ctx: &mut Context, submit_fn: &mut SubmitFn, ) { for api_level in ClassCodegenLevel::with_tables() { @@ -118,11 +174,11 @@ pub(crate) fn generate_sys_classes_file( pub(crate) fn generate_sys_utilities_file( api: &ExtensionApi, - ctx: &mut Context, sys_gen_path: &Path, + ctx: &mut Context, submit_fn: &mut SubmitFn, ) { - let mut table = MethodTableInfo { + let mut table = NamedMethodTable { table_name: ident("UtilityFunctionTable"), imports: quote! {}, ctor_parameters: quote! { @@ -153,25 +209,20 @@ pub(crate) fn generate_sys_utilities_file( }); table.method_inits.push(quote! { - #field: { - let utility_fn = unsafe { - get_utility_fn(string_names.fetch(#fn_name_str), #hash) - }; - crate::validate_utility_function(utility_fn, #fn_name_str, #hash) - }, + #field: crate::load_utility_function(get_utility_fn, string_names, #fn_name_str, #hash), }); table.method_count += 1; } - let code = make_method_table(table); + let code = make_named_method_table(table); submit_fn(sys_gen_path.join("table_utilities.rs"), code); } /// Generate code for a method table based on shared layout. -fn make_method_table(info: MethodTableInfo) -> TokenStream { - let MethodTableInfo { +fn make_named_method_table(info: NamedMethodTable) -> TokenStream { + let NamedMethodTable { table_name, imports, ctor_parameters, @@ -182,11 +233,6 @@ fn make_method_table(info: MethodTableInfo) -> TokenStream { method_count, } = info; - // 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)] }); - // Assumes that both decls and inits already have a trailing comma. // This is necessary because some generators emit multiple lines (statements) per element. quote! { @@ -201,7 +247,6 @@ fn make_method_table(info: MethodTableInfo) -> TokenStream { pub const CLASS_COUNT: usize = #class_count; pub const METHOD_COUNT: usize = #method_count; - #unused_attr pub fn load( #ctor_parameters ) -> Self { @@ -215,13 +260,88 @@ fn make_method_table(info: MethodTableInfo) -> TokenStream { } } +fn make_indexed_method_table(info: IndexedMethodTable) -> TokenStream { + let IndexedMethodTable { + table_name, + imports, + ctor_parameters, + pre_init_code, + fptr_type, + mut method_inits, + named_accessors, + class_count, + method_count, + } = info; + + // Editor table can be empty, if the Godot binary is compiled without editor. + let unused_attr = (method_count == 0).then(|| quote! { #[allow(unused_variables)] }); + let named_method_api = make_named_accessors(&named_accessors, &fptr_type); + + // Make sure methods are complete and in order of index. + assert_eq!( + method_inits.len(), + method_count, + "number of methods does not match count" + ); + method_inits.sort_by_key(|init| init.index); + + if let Some(last) = method_inits.last() { + assert_eq!( + last.index, + method_count - 1, + "last method should have highest index" + ); + } else { + assert_eq!(method_count, 0, "empty method table should have count 0"); + } + + // Assumes that inits already have a trailing comma. + // This is necessary because some generators emit multiple lines (statements) per element. + quote! { + #imports + + pub struct #table_name { + function_pointers: [#fptr_type; #method_count], + } + + impl #table_name { + pub const CLASS_COUNT: usize = #class_count; + pub const METHOD_COUNT: usize = #method_count; + + #unused_attr + pub fn load( + #ctor_parameters + ) -> Self { + #pre_init_code + + Self { + function_pointers: [ + #( #method_inits )* + ] + } + } + + #[inline(always)] + pub fn fptr_by_index(&self, index: usize) -> #fptr_type { + // SAFETY: indices are statically generated and guaranteed to be in range. + unsafe { + *self.function_pointers.get_unchecked(index) + } + } + + #named_method_api + } + } +} + pub(crate) fn generate_sys_builtin_methods_file( api: &ExtensionApi, builtin_types: &BuiltinTypeMap, sys_gen_path: &Path, + ctx: &mut Context, submit_fn: &mut SubmitFn, ) { - let code = make_builtin_method_table(api, builtin_types); + let code = make_builtin_method_table(api, builtin_types, ctx); submit_fn(sys_gen_path.join("table_builtins.rs"), code); } @@ -548,7 +668,7 @@ fn make_central_items( fn make_builtin_lifecycle_table(builtin_types: &BuiltinTypeMap) -> TokenStream { let len = builtin_types.count(); - let mut table = MethodTableInfo { + let mut table = NamedMethodTable { table_name: ident("BuiltinLifecycleTable"), imports: quote! { use crate::{ @@ -588,7 +708,7 @@ fn make_builtin_lifecycle_table(builtin_types: &BuiltinTypeMap) -> TokenStream { table.class_count += 1; } - make_method_table(table) + make_named_method_table(table) } fn make_class_method_table( @@ -596,7 +716,7 @@ fn make_class_method_table( api_level: ClassCodegenLevel, ctx: &mut Context, ) -> TokenStream { - let mut table = MethodTableInfo { + let mut table = IndexedMethodTable { table_name: api_level.table_struct(), imports: TokenStream::new(), ctor_parameters: quote! { @@ -604,15 +724,17 @@ fn make_class_method_table( string_names: &mut crate::StringCache, }, pre_init_code: TokenStream::new(), // late-init, depends on class string names - method_decls: vec![], + fptr_type: quote! { crate::ClassMethodBind }, method_inits: vec![], + named_accessors: vec![], class_count: 0, method_count: 0, }; let mut class_sname_decls = Vec::new(); for class in api.classes.iter() { - if special_cases::is_class_deleted(&TyName::from_godot(&class.name)) + let class_ty = TyName::from_godot(&class.name); + if special_cases::is_class_deleted(&class_ty) || codegen_special_cases::is_class_excluded(&class.name) || util::get_api_level(class) != api_level { @@ -623,7 +745,7 @@ fn make_class_method_table( let initializer_expr = util::make_sname_ptr(&class.name); let prev_method_count = table.method_count; - populate_class_methods(&mut table, class, &class_var, ctx); + populate_class_methods(&mut table, class, &class_ty, &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! { @@ -640,11 +762,32 @@ fn make_class_method_table( #( #class_sname_decls )* }; - make_method_table(table) + make_indexed_method_table(table) } -fn make_builtin_method_table(api: &ExtensionApi, builtin_types: &BuiltinTypeMap) -> TokenStream { - let mut table = MethodTableInfo { +/// For index-based method tables, have select methods exposed by name for internal use. +fn make_named_accessors(accessors: &[AccessorMethod], fptr: &TokenStream) -> TokenStream { + let mut result_api = TokenStream::new(); + + for AccessorMethod { name, index } in accessors { + let code = quote! { + #[inline(always)] + pub fn #name(&self) -> #fptr { + self.fptr_by_index(#index) + } + }; + + result_api.append_all(code.into_iter()); + } + result_api +} + +fn make_builtin_method_table( + api: &ExtensionApi, + builtin_types: &BuiltinTypeMap, + ctx: &mut Context, +) -> TokenStream { + let mut table = IndexedMethodTable { table_name: ident("BuiltinMethodTable"), imports: TokenStream::new(), ctor_parameters: quote! { @@ -655,8 +798,9 @@ fn make_builtin_method_table(api: &ExtensionApi, builtin_types: &BuiltinTypeMap) use crate as sys; let get_builtin_method = interface.variant_get_ptr_builtin_method.expect("variant_get_ptr_builtin_method absent"); }, - method_decls: vec![], + fptr_type: quote! { crate::BuiltinMethodBind }, method_inits: vec![], + named_accessors: vec![], class_count: 0, method_count: 0, }; @@ -667,93 +811,104 @@ fn make_builtin_method_table(api: &ExtensionApi, builtin_types: &BuiltinTypeMap) continue; // for Nil }; - populate_builtin_methods(&mut table, builtin, &builtin_type.type_names); + populate_builtin_methods(&mut table, builtin, &builtin_type.type_names, ctx); table.class_count += 1; } - make_method_table(table) + make_indexed_method_table(table) } -/// Returns whether at least 1 method was added. fn populate_class_methods( - table: &mut MethodTableInfo, + table: &mut IndexedMethodTable, class: &Class, + class_ty: &TyName, class_var: &Ident, ctx: &mut Context, ) { - let class_name_str = class.name.as_str(); - for method in option_as_slice(&class.methods) { - if codegen_special_cases::is_method_excluded(method, false, ctx) { + if special_cases::is_deleted(class_ty, method, ctx) { continue; } - let field = util::make_class_method_ptr_name(&class.name, method); - // Note: varcall/ptrcall is only decided at call time; the method bind is the same for both. - let method_decl = quote! { pub #field: crate::ClassMethodBind, }; - let method_init = make_class_method_init(method, &field, class_var, class_name_str); + let index = ctx.get_table_index(&MethodTableKey::ClassMethod { + api_level: util::get_api_level(class), + class_ty: class_ty.clone(), + method_name: method.name.clone(), + }); + let method_init = make_class_method_init(method, class_var, class_ty); - table.method_decls.push(method_decl); - table.method_inits.push(method_init); + table.method_inits.push(MethodInit { method_init, index }); table.method_count += 1; + + // If requested, add a named accessor for this method. + if special_cases::is_named_accessor_in_table(class_ty, &method.name) { + table.named_accessors.push(AccessorMethod { + name: make_class_method_ptr_name(class_ty, method), + index, + }); + } } } fn populate_builtin_methods( - table: &mut MethodTableInfo, + table: &mut IndexedMethodTable, builtin_class: &BuiltinClass, - type_name: &TypeNames, + builtin_name: &TypeNames, + ctx: &mut Context, ) { for method in option_as_slice(&builtin_class.methods) { - if codegen_special_cases::is_builtin_method_excluded(method) { + let builtin_ty = TyName::from_godot(&builtin_class.name); + if special_cases::is_builtin_deleted(&builtin_ty, method) { continue; } - let field = util::make_builtin_method_ptr_name(type_name, method); + let index = ctx.get_table_index(&MethodTableKey::BuiltinMethod { + builtin_ty: builtin_ty.clone(), + method_name: method.name.clone(), + }); - let method_decl = quote! { pub #field: crate::BuiltinMethodBind, }; - let method_init = make_builtin_method_init(method, &field, type_name); + let method_init = make_builtin_method_init(method, builtin_name, index); - table.method_decls.push(method_decl); - table.method_inits.push(method_init); + table.method_inits.push(MethodInit { method_init, index }); table.method_count += 1; + + // If requested, add a named accessor for this method. + if special_cases::is_named_accessor_in_table(&builtin_ty, &method.name) { + table.named_accessors.push(AccessorMethod { + name: make_builtin_method_ptr_name(&builtin_ty, &method), + index, + }); + } } } fn make_class_method_init( method: &ClassMethod, - field: &Ident, class_var: &Ident, - class_name_str: &str, + class_ty: &TyName, ) -> TokenStream { + let class_name_str = class_ty.godot_ty.as_str(); let method_name_str = method.name.as_str(); - let method_sname = util::make_sname_ptr(method_name_str); let hash = method.hash.unwrap_or_else(|| { panic!( "class method has no hash: {}::{}", - class_name_str, method_name_str + class_ty.godot_ty, method_name_str ) }); quote! { - #field: { - let method_bind = unsafe { - get_method_bind(#class_var, #method_sname, #hash) - }; - crate::validate_class_method(method_bind, #class_name_str, #method_name_str, #hash) - }, + crate::load_class_method(get_method_bind, string_names, #class_var, #class_name_str, #method_name_str, #hash), } } fn make_builtin_method_init( method: &BuiltinClassMethod, - field: &Ident, type_name: &TypeNames, + index: usize, ) -> TokenStream { let method_name_str = method.name.as_str(); - let method_sname = util::make_sname_ptr(method_name_str); let variant_type = &type_name.sys_variant_type; let variant_type_str = &type_name.json_builtin_name; @@ -766,12 +921,7 @@ fn make_builtin_method_init( }); quote! { - #field: { - let method_bind = unsafe { - get_builtin_method(sys::#variant_type, #method_sname, #hash) - }; - crate::validate_builtin_method(method_bind, #variant_type_str, #method_name_str, #hash) - }, + {let _ = #index;crate::load_builtin_method(get_builtin_method, string_names, sys::#variant_type, #variant_type_str, #method_name_str, #hash)}, } } diff --git a/godot-codegen/src/class_generator.rs b/godot-codegen/src/class_generator.rs index e42c22558..0ec57c583 100644 --- a/godot-codegen/src/class_generator.rs +++ b/godot-codegen/src/class_generator.rs @@ -11,12 +11,12 @@ use quote::{format_ident, quote, ToTokens}; use std::path::Path; use crate::api_parser::*; -use crate::central_generator::{collect_builtin_types, BuiltinTypeInfo}; +use crate::central_generator::collect_builtin_types; use crate::context::NotificationEnum; 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, - NativeStructuresField, + to_pascal_case, to_rust_expr, to_rust_type, to_rust_type_abi, to_snake_case, ClassCodegenLevel, + MethodTableKey, NativeStructuresField, }; use crate::{ codegen_special_cases, special_cases, util, Context, GeneratedBuiltin, GeneratedBuiltinModule, @@ -280,19 +280,18 @@ pub(crate) fn generate_builtin_class_files( let mut modules = vec![]; for class in api.builtin_classes.iter() { let module_name = ModName::from_godot(&class.name); - let class_name = TyName::from_godot(&class.name); - let inner_class_name = TyName::from_godot(&format!("Inner{}", class.name)); + let builtin_name = TyName::from_godot(&class.name); + let inner_builtin_name = TyName::from_godot(&format!("Inner{}", class.name)); - if special_cases::is_builtin_type_deleted(&class_name) { + if special_cases::is_builtin_type_deleted(&builtin_name) { continue; } - let type_info = builtin_types_map + let _type_info = builtin_types_map .get(&class.name) .unwrap_or_else(|| panic!("builtin type not found: {}", class.name)); - let generated_class = - make_builtin_class(class, &class_name, &inner_class_name, type_info, ctx); + let generated_class = make_builtin_class(class, &builtin_name, &inner_builtin_name, ctx); let file_contents = generated_class.code; let out_path = gen_path.join(format!("{}.rs", module_name.rust_mod)); @@ -300,7 +299,7 @@ pub(crate) fn generate_builtin_class_files( submit_fn(out_path, file_contents); modules.push(GeneratedBuiltinModule { - class_name: inner_class_name, + class_name: inner_builtin_name, module_name, }); } @@ -494,17 +493,12 @@ 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 api_level = util::get_api_level(class); let FnDefinitions { functions: methods, builders, - } = make_methods( - option_as_slice(&class.methods), - class_name, - &get_method_table, - ctx, - ); + } = make_methods(option_as_slice(&class.methods), class_name, &api_level, 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); @@ -806,9 +800,8 @@ fn workaround_constant_collision(all_constants: &mut Vec<(Ident, i32)>) { fn make_builtin_class( class: &BuiltinClass, - class_name: &TyName, + builtin_name: &TyName, inner_class_name: &TyName, - type_info: &BuiltinTypeInfo, ctx: &mut Context, ) -> GeneratedBuiltin { let outer_class = if let RustTy::BuiltinIdent(ident) = to_rust_type(&class.name, None, ctx) { @@ -830,14 +823,13 @@ fn make_builtin_class( builders, } = make_builtin_methods( option_as_slice(&class.methods), - class_name, + builtin_name, inner_class_name, - type_info, ctx, ); - let enums = make_enums(&class_enums, class_name, ctx); - let special_constructors = make_special_builtin_methods(class_name, ctx); + let enums = make_enums(&class_enums, builtin_name, ctx); + let special_constructors = make_special_builtin_methods(builtin_name, ctx); // mod re_export needed, because class should not appear inside the file module, and we can't re-export private struct as pub let code = quote! { @@ -1017,26 +1009,27 @@ fn make_builtin_module_file(classes_and_modules: Vec) -> fn make_methods( methods: &[ClassMethod], class_name: &TyName, - get_method_table: &Ident, + api_level: &ClassCodegenLevel, ctx: &mut Context, ) -> FnDefinitions { - let definitions = methods - .iter() - .map(|method| make_method_definition(method, class_name, get_method_table, ctx)); + let get_method_table = api_level.table_global_getter(); + + let definitions = methods.iter().map(|method| { + make_method_definition(method, class_name, api_level, &get_method_table, ctx) + }); FnDefinitions::expand(definitions) } fn make_builtin_methods( methods: &[BuiltinClassMethod], - class_name: &TyName, + builtin_name: &TyName, inner_class_name: &TyName, - type_info: &BuiltinTypeInfo, ctx: &mut Context, ) -> FnDefinitions { - let definitions = methods.iter().map(|method| { - make_builtin_method_definition(method, class_name, inner_class_name, type_info, ctx) - }); + let definitions = methods + .iter() + .map(|method| make_builtin_method_definition(method, builtin_name, inner_class_name, ctx)); FnDefinitions::expand(definitions) } @@ -1082,12 +1075,11 @@ fn make_special_builtin_methods(class_name: &TyName, _ctx: &Context) -> TokenStr fn make_method_definition( method: &ClassMethod, class_name: &TyName, + api_level: &ClassCodegenLevel, get_method_table: &Ident, ctx: &mut Context, ) -> FnDefinition { - if codegen_special_cases::is_method_excluded(method, false, ctx) - || special_cases::is_deleted(class_name, &method.name) - { + if special_cases::is_deleted(class_name, method, ctx) { return FnDefinition::none(); } /*if method.map_args(|args| args.is_empty()) { @@ -1119,10 +1111,14 @@ fn make_method_definition( quote! { sys::interface_fn!(object_method_bind_ptrcall) } }; - let fn_ptr = util::make_class_method_ptr_name(&class_name.godot_ty, method); + let table_index = ctx.get_table_index(&MethodTableKey::ClassMethod { + api_level: *api_level, + class_ty: class_name.clone(), + method_name: method.name.clone(), + }); let init_code = quote! { - let __method_bind = sys::#get_method_table().#fn_ptr; + let __method_bind = sys::#get_method_table().fptr_by_index(#table_index); let __call_fn = #function_provider; }; @@ -1156,12 +1152,11 @@ fn make_method_definition( fn make_builtin_method_definition( method: &BuiltinClassMethod, - class_name: &TyName, + builtin_name: &TyName, inner_class_name: &TyName, - type_info: &BuiltinTypeInfo, ctx: &mut Context, ) -> FnDefinition { - if codegen_special_cases::is_builtin_method_excluded(method) { + if special_cases::is_builtin_deleted(builtin_name, method) { return FnDefinition::none(); } @@ -1175,10 +1170,13 @@ fn make_builtin_method_definition( let is_varcall = method.is_vararg; let variant_ffi = is_varcall.then(VariantFfi::type_ptr); - let fn_ptr = util::make_builtin_method_ptr_name(&type_info.type_names, method); + let table_index = ctx.get_table_index(&MethodTableKey::BuiltinMethod { + builtin_ty: builtin_name.clone(), + method_name: method.name.clone(), + }); let init_code = quote! { - let __call_fn = sys::builtin_method_table().#fn_ptr; + let __call_fn = sys::builtin_method_table().fptr_by_index(#table_index); }; let receiver = make_receiver(method.is_static, method.is_const, quote! { self.sys_ptr }); @@ -1191,7 +1189,7 @@ fn make_builtin_method_definition( &FnSignature { function_name: method_name_str, surrounding_class: Some(inner_class_name), - is_private: special_cases::is_private(class_name, &method.name), + is_private: special_cases::is_private(builtin_name, &method.name), is_virtual: false, qualifier: FnQualifier::for_method(method.is_const, method.is_static), diff --git a/godot-codegen/src/context.rs b/godot-codegen/src/context.rs index 974061bfa..85d212c01 100644 --- a/godot-codegen/src/context.rs +++ b/godot-codegen/src/context.rs @@ -4,8 +4,9 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -use crate::api_parser::Class; -use crate::{codegen_special_cases, util, ExtensionApi, GodotTy, RustTy, TyName}; +use crate::api_parser::{BuiltinClass, BuiltinClassMethod, Class, ClassConstant, ClassMethod}; +use crate::util::{option_as_slice, MethodTableKey}; +use crate::{codegen_special_cases, special_cases, util, ExtensionApi, GodotTy, RustTy, TyName}; use proc_macro2::{Ident, TokenStream}; use quote::{format_ident, ToTokens}; use std::collections::{HashMap, HashSet}; @@ -20,6 +21,8 @@ pub(crate) struct Context<'a> { cached_rust_types: HashMap, notifications_by_class: HashMap>, notification_enum_names_by_class: HashMap, + method_table_indices: HashMap, + method_table_next_index: HashMap, } impl<'a> Context<'a> { @@ -34,6 +37,12 @@ impl<'a> Context<'a> { for builtin in api.builtin_classes.iter() { let ty_name = builtin.name.as_str(); ctx.builtin_types.insert(ty_name); + + Self::populate_builtin_class_table_indices( + builtin, + option_as_slice(&builtin.methods), + &mut ctx, + ); } for structure in api.native_structures.iter() { @@ -60,31 +69,17 @@ impl<'a> Context<'a> { } // Populate notification constants (first, only for classes that declare them themselves). - if let Some(constants) = class.constants.as_ref() { - let mut has_notifications = false; - - for constant in constants.iter() { - if let Some(rust_constant) = util::try_to_notification(constant) { - // First time - if !has_notifications { - ctx.notifications_by_class - .insert(class_name.clone(), Vec::new()); - - ctx.notification_enum_names_by_class.insert( - class_name.clone(), - NotificationEnum::for_own_class(&class_name), - ); - - has_notifications = true; - } - - ctx.notifications_by_class - .get_mut(&class_name) - .expect("just inserted constants; must be present") - .push((rust_constant, constant.value)); - } - } - } + Self::populate_notification_constants( + &class_name, + option_as_slice(&class.constants), + &mut ctx, + ); + Self::populate_class_table_indices( + class, + &class_name, + option_as_slice(&class.methods), + &mut ctx, + ); } // Populate remaining notification enum names, by copying the one to nearest base class that has at least 1 notification. @@ -129,13 +124,110 @@ impl<'a> Context<'a> { ctx } + fn populate_notification_constants( + class_name: &TyName, + constants: &[ClassConstant], + ctx: &mut Context, + ) { + let mut has_notifications = false; + for constant in constants.iter() { + if let Some(rust_constant) = util::try_to_notification(constant) { + // First time + if !has_notifications { + ctx.notifications_by_class + .insert(class_name.clone(), Vec::new()); + + ctx.notification_enum_names_by_class.insert( + class_name.clone(), + NotificationEnum::for_own_class(class_name), + ); + + has_notifications = true; + } + + ctx.notifications_by_class + .get_mut(class_name) + .expect("just inserted constants; must be present") + .push((rust_constant, constant.value)); + } + } + } + + fn populate_class_table_indices( + class: &Class, + class_name: &TyName, + methods: &[ClassMethod], + ctx: &mut Context, + ) { + if special_cases::is_class_deleted(class_name) { + return; + } + + for method in methods.iter() { + if special_cases::is_deleted(class_name, method, ctx) { + continue; + } + + let key = MethodTableKey::ClassMethod { + api_level: util::get_api_level(class), + class_ty: class_name.clone(), + method_name: method.name.clone(), + }; + + ctx.register_table_index(key); + } + } + + fn populate_builtin_class_table_indices( + builtin: &BuiltinClass, + methods: &[BuiltinClassMethod], + ctx: &mut Context, + ) { + let builtin_ty = TyName::from_godot(builtin.name.as_str()); + if special_cases::is_builtin_type_deleted(&builtin_ty) { + return; + } + + for method in methods.iter() { + if special_cases::is_builtin_deleted(&builtin_ty, method) { + continue; + } + + let key = MethodTableKey::BuiltinMethod { + builtin_ty: builtin_ty.clone(), + method_name: method.name.clone(), + }; + + ctx.register_table_index(key); + } + } + pub fn get_engine_class(&self, class_name: &TyName) -> &Class { self.engine_classes.get(class_name).unwrap() } - // pub fn is_engine_class(&self, class_name: &str) -> bool { - // self.engine_classes.contains(class_name) - // } + // Private, because initialized in constructor. Ensures deterministic assignment. + fn register_table_index(&mut self, key: MethodTableKey) -> usize { + let key_category = key.category(); + + let next_index = self + .method_table_next_index + .entry(key_category) + .or_insert(0); + + let prev = self.method_table_indices.insert(key, *next_index); + assert!(prev.is_none(), "table index already registered"); + + *next_index += 1; + *next_index + } + + pub fn get_table_index(&self, key: &MethodTableKey) -> usize { + *self + .method_table_indices + .get(key) + .unwrap_or_else(|| panic!("did not register table index for key {:?}", key)) + } /// Checks if this is a builtin type (not `Object`). /// diff --git a/godot-codegen/src/lib.rs b/godot-codegen/src/lib.rs index 44c3e67e7..5a3c7095b 100644 --- a/godot-codegen/src/lib.rs +++ b/godot-codegen/src/lib.rs @@ -72,16 +72,16 @@ pub fn generate_sys_files( watch.record("generate_central_file"); let builtin_types = BuiltinTypeMap::load(&api); - generate_sys_builtin_methods_file(&api, &builtin_types, sys_gen_path, &mut submit_fn); + generate_sys_builtin_methods_file(&api, &builtin_types, sys_gen_path, &mut ctx, &mut submit_fn); watch.record("generate_builtin_methods_file"); generate_sys_builtin_lifecycle_file(&builtin_types, sys_gen_path, &mut submit_fn); watch.record("generate_builtin_lifecycle_file"); - generate_sys_classes_file(&api, &mut ctx, sys_gen_path, watch, &mut submit_fn); + generate_sys_classes_file(&api, sys_gen_path, watch, &mut ctx, &mut submit_fn); // watch records inside the function. - generate_sys_utilities_file(&api, &mut ctx, sys_gen_path, &mut submit_fn); + generate_sys_utilities_file(&api, sys_gen_path, &mut ctx, &mut submit_fn); watch.record("generate_utilities_file"); let is_godot_4_0 = api.header.version_major == 4 && api.header.version_minor == 0; @@ -224,6 +224,7 @@ impl ToTokens for RustTy { // ---------------------------------------------------------------------------------------------------------------------------------------------- /// Contains multiple naming conventions for types (classes, builtin classes, enums). +// TODO(bromeon, 2023-09): see if it makes sense to unify this with TypeNames (which is mostly used in central generator) #[derive(Clone, Eq, PartialEq, Hash)] pub(crate) struct TyName { godot_ty: String, diff --git a/godot-codegen/src/special_cases.rs b/godot-codegen/src/special_cases.rs index 1963e231f..928d97420 100644 --- a/godot-codegen/src/special_cases.rs +++ b/godot-codegen/src/special_cases.rs @@ -17,11 +17,17 @@ #![allow(clippy::match_like_matches_macro)] // if there is only one rule -use crate::TyName; +use crate::api_parser::{BuiltinClassMethod, ClassMethod}; +use crate::Context; +use crate::{codegen_special_cases, TyName}; #[rustfmt::skip] -pub(crate) fn is_deleted(class_name: &TyName, godot_method_name: &str) -> bool { - match (class_name.godot_ty.as_str(), godot_method_name) { +pub(crate) fn is_deleted(class_name: &TyName, method: &ClassMethod, ctx: &mut Context) -> bool { + if codegen_special_cases::is_method_excluded(method, false, ctx){ + return true; + } + + match (class_name.godot_ty.as_str(), method.name.as_str()) { // Already covered by manual APIs //| ("Object", "to_string") | ("Object", "get_instance_id") @@ -135,9 +141,18 @@ fn is_class_experimental(class_name: &TyName) -> bool { } } +/// Whether a method is available in the method table as a named accessor. #[rustfmt::skip] -pub(crate) fn is_private(class_name: &TyName, godot_method_name: &str) -> bool { - match (class_name.godot_ty.as_str(), godot_method_name) { +pub(crate) fn is_named_accessor_in_table(class_or_builtin_ty: &TyName, godot_method_name: &str) -> bool { + // Generated methods made private are typically needed internally and exposed with a different API, + // so make them accessible. + is_private(class_or_builtin_ty, godot_method_name) +} + +/// Whether a class or builtin method should be hidden from the public API. +#[rustfmt::skip] +pub(crate) fn is_private(class_or_builtin_ty: &TyName, godot_method_name: &str) -> bool { + match (class_or_builtin_ty.godot_ty.as_str(), godot_method_name) { // Already covered by manual APIs | ("Object", "to_string") | ("RefCounted", "init_ref") @@ -161,6 +176,12 @@ pub(crate) fn is_excluded_from_default_params(class_name: Option<&TyName>, godot } } +/// True if builtin method is excluded. Does NOT check for type exclusion; use [`is_builtin_type_deleted`] for that. +pub(crate) fn is_builtin_deleted(_class_name: &TyName, method: &BuiltinClassMethod) -> bool { + // Currently only deleted if codegen. + codegen_special_cases::is_builtin_method_excluded(method) +} + /// True if builtin type is excluded (`NIL` or scalars) pub(crate) fn is_builtin_type_deleted(class_name: &TyName) -> bool { let name = class_name.godot_ty.as_str(); diff --git a/godot-codegen/src/util.rs b/godot-codegen/src/util.rs index 8c1903151..7570b231e 100644 --- a/godot-codegen/src/util.rs +++ b/godot-codegen/src/util.rs @@ -7,12 +7,14 @@ use crate::api_parser::{ BuiltinClassMethod, Class, ClassConstant, ClassMethod, Enum, UtilityFunction, }; -use crate::central_generator::TypeNames; use crate::special_cases::is_builtin_scalar; use crate::{Context, GodotTy, ModName, RustTy, TyName}; + use proc_macro2::{Ident, Literal, TokenStream}; use quote::{format_ident, quote, ToTokens}; +use std::fmt; + #[derive(Clone, Debug, PartialEq, Eq)] pub struct NativeStructuresField { pub field_type: String, @@ -20,7 +22,7 @@ pub struct NativeStructuresField { } /// At which stage a class function pointer is loaded. -#[derive(Copy, Clone, Eq, PartialEq)] +#[derive(Copy, Clone, Eq, PartialEq, Hash)] pub enum ClassCodegenLevel { Servers, Scene, @@ -66,28 +68,95 @@ impl ClassCodegenLevel { } } +// ---------------------------------------------------------------------------------------------------------------------------------------------- + +/// Lookup key for indexed method tables. +// Could potentially save a lot of string allocations with lifetimes. +#[derive(Eq, PartialEq, Hash)] +pub(crate) enum MethodTableKey { + ClassMethod { + api_level: ClassCodegenLevel, + class_ty: TyName, + method_name: String, + }, + BuiltinMethod { + builtin_ty: TyName, + method_name: String, + }, + /*BuiltinLifecycleMethod { + builtin_ty: TyName, + method_name: String, + }, + UtilityFunction { + function_name: String, + },*/ +} + +impl MethodTableKey { + /// Maps the method table key to a "category", meaning a distinct method table. + /// + /// Categories have independent address spaces for indices, meaning they begin again at 0 for each new category. + pub fn category(&self) -> String { + match self { + MethodTableKey::ClassMethod { api_level, .. } => format!("class.{}", api_level.lower()), + MethodTableKey::BuiltinMethod { .. } => "builtin".to_string(), + // MethodTableKey::BuiltinLifecycleMethod { .. } => "builtin.lifecycle".to_string(), + // MethodTableKey::UtilityFunction { .. } => "utility".to_string(), + } + } +} + +impl fmt::Debug for MethodTableKey { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + MethodTableKey::ClassMethod { + api_level: _, + class_ty: class_name, + method_name, + } => write!(f, "ClassMethod({}.{})", class_name.godot_ty, method_name), + MethodTableKey::BuiltinMethod { + builtin_ty: variant_type, + method_name, + } => write!( + f, + "BuiltinMethod({}.{})", + variant_type.godot_ty, method_name + ), + /*MethodTableKey::BuiltinLifecycleMethod { + builtin_ty: variant_type, + method_name, + } => write!( + f, + "BuiltinLifecycleMethod({}.{})", + variant_type.godot_ty, method_name + ), + MethodTableKey::UtilityFunction { function_name } => { + write!(f, "UtilityFunction({})", function_name) + }*/ + } + } +} + +// ---------------------------------------------------------------------------------------------------------------------------------------------- + /// Small utility that turns an optional vector (often encountered as JSON deserialization type) into a slice. -pub fn option_as_slice(option: &Option>) -> &[T] { +pub(crate) fn option_as_slice(option: &Option>) -> &[T] { option.as_ref().map_or(&[], Vec::as_slice) } -// pub fn make_class_method_ptr_name(class_name_str: &str, method_name_str: &str) -> Ident { -// format_ident!("{}__{}", class_name_str, method_name_str) -// } - // Use &ClassMethod instead of &str, to make sure it's the original Godot name and no rename. -pub fn make_class_method_ptr_name(class_godot_name: &str, method: &ClassMethod) -> Ident { - format_ident!("{}__{}", class_godot_name, method.name) +pub(crate) fn make_class_method_ptr_name(class_ty: &TyName, method: &ClassMethod) -> Ident { + format_ident!("{}__{}", to_snake_case(&class_ty.godot_ty), method.name) } -pub fn make_builtin_method_ptr_name( - variant_type: &TypeNames, +pub(crate) fn make_builtin_method_ptr_name( + builtin_ty: &TyName, method: &BuiltinClassMethod, ) -> Ident { - format_ident!("{}__{}", variant_type.json_builtin_name, method.name) + format_ident!("{}__{}", to_snake_case(&builtin_ty.godot_ty), method.name) } -pub fn make_utility_function_ptr_name(function: &UtilityFunction) -> Ident { +pub(crate) fn make_utility_function_ptr_name(function: &UtilityFunction) -> Ident { safe_ident(&function.name) } @@ -820,7 +889,7 @@ fn to_rust_expr_inner(expr: &str, ty: &RustTy, is_inner: bool) -> TokenStream { ); } -fn suffixed_lit(num: impl std::fmt::Display, suffix: &Ident) -> TokenStream { +fn suffixed_lit(num: impl fmt::Display, suffix: &Ident) -> TokenStream { // i32, u16 etc happens to be also the literal suffix let combined = format!("{num}{suffix}"); combined diff --git a/godot-core/src/lib.rs b/godot-core/src/lib.rs index 026eae1f2..424187e3f 100644 --- a/godot-core/src/lib.rs +++ b/godot-core/src/lib.rs @@ -105,6 +105,7 @@ pub mod private { struct GodotPanicInfo { line: u32, file: String, + //backtrace: Backtrace, // for future use } /// Executes `code`. If a panic is thrown, it is caught and an error message is printed to Godot. @@ -128,6 +129,7 @@ pub mod private { *info.lock().unwrap() = Some(GodotPanicInfo { file: location.file().to_string(), line: location.line(), + //backtrace: Backtrace::capture(), }); } else { println!("panic occurred but can't get location information..."); @@ -154,6 +156,7 @@ pub mod private { info.line, error_context() ); + //eprintln!("Backtrace:\n{}", info.backtrace); print_panic(err); None } diff --git a/godot-ffi/src/toolbox.rs b/godot-ffi/src/toolbox.rs index 585bea08f..6c9fa1d57 100644 --- a/godot-ffi/src/toolbox.rs +++ b/godot-ffi/src/toolbox.rs @@ -158,7 +158,40 @@ impl Inner for Option { } // ---------------------------------------------------------------------------------------------------------------------------------------------- -// Crate-wide support +// Function types used for table loaders + +pub(crate) type GetClassMethod = unsafe extern "C" fn( + p_classname: sys::GDExtensionConstStringNamePtr, + p_methodname: sys::GDExtensionConstStringNamePtr, + p_hash: sys::GDExtensionInt, +) -> sys::GDExtensionMethodBindPtr; + +pub(crate) type ClassMethodBind = sys::GDExtensionMethodBindPtr; + +pub(crate) type GetBuiltinMethod = unsafe extern "C" fn( + p_type: sys::GDExtensionVariantType, + p_method: sys::GDExtensionConstStringNamePtr, + p_hash: sys::GDExtensionInt, +) -> sys::GDExtensionPtrBuiltInMethod; + +// 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) type GetUtilityFunction = unsafe extern "C" fn( + p_function: sys::GDExtensionConstStringNamePtr, + p_hash: sys::GDExtensionInt, +) -> sys::GDExtensionPtrUtilityFunction; + +pub(crate) type UtilityFunctionBind = unsafe extern "C" fn( + r_return: sys::GDExtensionTypePtr, + p_args: *const sys::GDExtensionConstTypePtr, + p_argument_count: std::os::raw::c_int, +); // ---------------------------------------------------------------------------------------------------------------------------------------------- // Utility functions @@ -183,12 +216,12 @@ pub(crate) unsafe fn unwrap_ref_unchecked_mut(opt: &mut Option) -> &mut T } } -pub(crate) type ClassMethodBind = sys::GDExtensionMethodBindPtr; - -pub(crate) fn validate_class_method( - method: ClassMethodBind, - class_name: &str, - method_name: &str, +pub(crate) fn load_class_method( + get_method_bind: GetClassMethod, + string_names: &mut sys::StringCache, + class_sname_ptr: sys::GDExtensionStringNamePtr, + class_name: &'static str, + method_name: &'static str, hash: i64, ) -> ClassMethodBind { /*crate::out!( @@ -197,6 +230,12 @@ pub(crate) fn validate_class_method( method_name, hash );*/ + + // SAFETY: function pointers provided by Godot. We have no way to validate them. + let method_sname_ptr: sys::GDExtensionStringNamePtr = string_names.fetch(method_name); + let method: ClassMethodBind = + unsafe { get_method_bind(class_sname_ptr, method_sname_ptr, hash) }; + if method.is_null() { panic!( "Failed to load class method {}::{} (hash {}).\n\ @@ -210,24 +249,12 @@ pub(crate) fn validate_class_method( 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) type UtilityFunctionBind = unsafe extern "C" fn( - r_return: sys::GDExtensionTypePtr, - p_args: *const sys::GDExtensionConstTypePtr, - p_argument_count: std::os::raw::c_int, -); - -pub(crate) fn validate_builtin_method( - method: sys::GDExtensionPtrBuiltInMethod, - variant_type: &str, - method_name: &str, +pub(crate) fn load_builtin_method( + get_builtin_method: GetBuiltinMethod, + string_names: &mut sys::StringCache, + variant_type: sys::GDExtensionVariantType, + variant_type_str: &'static str, + method_name: &'static str, hash: i64, ) -> BuiltinMethodBind { /*crate::out!( @@ -236,8 +263,15 @@ pub(crate) fn validate_builtin_method( method_name, hash );*/ + + // SAFETY: function pointers provided by Godot. We have no way to validate them. + let method_sname = string_names.fetch(method_name); + let method = unsafe { get_builtin_method(variant_type, method_sname, hash) }; + method.unwrap_or_else(|| { - panic!("Failed to load builtin method {variant_type}::{method_name} (hash {hash}).{INFO}") + panic!( + "Failed to load builtin method {variant_type_str}::{method_name} (hash {hash}).{INFO}" + ) }) } @@ -247,13 +281,17 @@ pub(crate) fn validate_builtin_lifecycle(function: Option, description: &s }) } -pub(crate) fn validate_utility_function( - utility_fn: sys::GDExtensionPtrUtilityFunction, - name: &str, +pub(crate) fn load_utility_function( + get_utility_fn: GetUtilityFunction, + string_names: &mut sys::StringCache, + fn_name_str: &'static str, hash: i64, ) -> UtilityFunctionBind { + // SAFETY: function pointers provided by Godot. We have no way to validate them. + let utility_fn = unsafe { get_utility_fn(string_names.fetch(fn_name_str), hash) }; + utility_fn.unwrap_or_else(|| { - panic!("Failed to load builtin lifecycle function {name} (hash {hash}).{INFO}") + panic!("Failed to load utility function {fn_name_str} (hash {hash}).{INFO}") }) }