diff --git a/godot-bindings/src/watch.rs b/godot-bindings/src/watch.rs index 30de8fed0..f2a49e04f 100644 --- a/godot-bindings/src/watch.rs +++ b/godot-bindings/src/watch.rs @@ -26,15 +26,14 @@ impl StopWatch { } } - pub fn record(&mut self, what: &'static str) { + pub fn record(&mut self, what: impl Into) { let now = Instant::now(); let duration = now - self.last_instant; + let name = what.into(); + self.last_instant = now; - self.lwidth = usize::max(self.lwidth, what.len()); - self.metrics.push(Metric { - name: what, - duration, - }); + self.lwidth = usize::max(self.lwidth, name.len()); + self.metrics.push(Metric { name, duration }); } pub fn write_stats_to(self, to_file: &Path) { @@ -48,7 +47,7 @@ impl StopWatch { } let rwidth = log10(total.as_millis()); let total_metric = Metric { - name: "total", + name: "total".to_string(), duration: total, }; @@ -79,6 +78,6 @@ fn log10(n: u128) -> usize { } struct Metric { - name: &'static str, + name: String, duration: Duration, } diff --git a/godot-codegen/src/api_parser.rs b/godot-codegen/src/api_parser.rs index 936a89be0..af75bbc2e 100644 --- a/godot-codegen/src/api_parser.rs +++ b/godot-codegen/src/api_parser.rs @@ -67,7 +67,7 @@ pub struct Class { pub is_refcounted: bool, pub is_instantiable: bool, pub inherits: Option, - // pub api_type: String, + pub api_type: String, pub constants: Option>, pub enums: Option>, pub methods: Option>, diff --git a/godot-codegen/src/central_generator.rs b/godot-codegen/src/central_generator.rs index 0237df4d5..92b526614 100644 --- a/godot-codegen/src/central_generator.rs +++ b/godot-codegen/src/central_generator.rs @@ -9,9 +9,11 @@ use quote::{format_ident, quote, ToTokens}; use std::collections::HashMap; use std::path::Path; -use crate::util::{to_pascal_case, to_rust_type, to_snake_case}; -use crate::{api_parser::*, SubmitFn}; -use crate::{ident, util, Context}; +use crate::api_parser::*; +use crate::util::{ + option_as_slice, to_pascal_case, to_rust_type, to_snake_case, ClassCodegenLevel, +}; +use crate::{codegen_special_cases, ident, special_cases, util, Context, SubmitFn, TyName}; struct CentralItems { opaque_types: [Vec; 2], @@ -20,13 +22,22 @@ struct CentralItems { variant_ty_enumerators_ord: Vec, variant_op_enumerators_pascal: Vec, variant_op_enumerators_ord: Vec, - variant_fn_decls: Vec, - variant_fn_inits: Vec, global_enum_defs: Vec, godot_version: Header, } -pub(crate) struct TypeNames { +struct MethodTableInfo { + table_name: Ident, + imports: TokenStream, + ctor_parameters: TokenStream, + pre_init_code: TokenStream, + method_decls: Vec, + method_inits: Vec, + class_count: usize, + method_count: usize, +} + +pub struct TypeNames { /// Name in JSON: "int" or "PackedVector2Array" pub json_builtin_name: String, @@ -52,6 +63,29 @@ pub(crate) struct BuiltinTypeInfo<'a> { pub operators: Option<&'a Vec>, } +pub(crate) struct BuiltinTypeMap<'a> { + map: HashMap>, +} + +impl<'a> BuiltinTypeMap<'a> { + pub fn load(api: &'a ExtensionApi) -> Self { + Self { + map: collect_builtin_types(api), + } + } + + /// Returns an iterator over the builtin types, ordered by `VariantType` value. + fn ordered(&self) -> impl Iterator> { + let mut ordered: Vec<_> = self.map.values().collect(); + ordered.sort_by_key(|info| info.value); + ordered.into_iter() + } + + fn count(&self) -> usize { + self.map.len() + } +} + pub(crate) fn generate_sys_central_file( api: &ExtensionApi, ctx: &mut Context, @@ -59,20 +93,146 @@ pub(crate) fn generate_sys_central_file( sys_gen_path: &Path, submit_fn: &mut SubmitFn, ) { - let central_items = make_central_items(api, build_config, ctx); + let builtin_types = BuiltinTypeMap::load(api); + let central_items = make_central_items(api, build_config, builtin_types, ctx); let sys_code = make_sys_code(¢ral_items); submit_fn(sys_gen_path.join("central.rs"), sys_code); } -pub(crate) fn generate_sys_mod_file(core_gen_path: &Path, submit_fn: &mut SubmitFn) { - let code = quote! { - pub mod central; - pub mod interface; - pub mod gdextension_interface; +pub(crate) fn generate_sys_classes_file( + api: &ExtensionApi, + ctx: &mut Context, + sys_gen_path: &Path, + watch: &mut godot_bindings::StopWatch, + submit_fn: &mut SubmitFn, +) { + 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); + watch.record(format!("generate_classes_{}_file", api_level.lower())); + } +} + +pub(crate) fn generate_sys_utilities_file( + api: &ExtensionApi, + ctx: &mut Context, + sys_gen_path: &Path, + submit_fn: &mut SubmitFn, +) { + let mut table = MethodTableInfo { + table_name: ident("UtilityFunctionTable"), + imports: quote! {}, + ctor_parameters: quote! { + interface: &crate::GDExtensionInterface, + string_names: &mut crate::StringCache, + }, + pre_init_code: quote! { + let get_utility_fn = interface.variant_get_ptr_utility_function + .expect("variant_get_ptr_utility_function absent"); + }, + method_decls: vec![], + method_inits: vec![], + class_count: 0, + method_count: 0, }; - submit_fn(core_gen_path.join("mod.rs"), code); + for function in api.utility_functions.iter() { + if codegen_special_cases::is_function_excluded(function, ctx) { + continue; + } + + let fn_name_str = &function.name; + let field = util::make_utility_function_ptr_name(function); + let hash = function.hash; + + table.method_decls.push(quote! { + pub #field: crate::UtilityFunctionBind, + }); + + 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) + }, + }); + + table.method_count += 1; + } + + let code = make_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 { + table_name, + imports, + ctor_parameters, + pre_init_code, + method_decls, + method_inits, + class_count, + 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! { + #imports + + #[allow(non_snake_case)] + pub struct #table_name { + #( #method_decls )* + } + + 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 { + #( #method_inits )* + } + } + } + } +} + +pub(crate) fn generate_sys_builtin_methods_file( + api: &ExtensionApi, + builtin_types: &BuiltinTypeMap, + sys_gen_path: &Path, + submit_fn: &mut SubmitFn, +) { + let code = make_builtin_method_table(api, builtin_types); + submit_fn(sys_gen_path.join("table_builtins.rs"), code); +} + +pub(crate) fn generate_sys_builtin_lifecycle_file( + builtin_types: &BuiltinTypeMap, + sys_gen_path: &Path, + submit_fn: &mut SubmitFn, +) { + // TODO merge this and the one in central.rs, to only collect once + let code = make_builtin_lifecycle_table(builtin_types); + submit_fn(sys_gen_path.join("table_builtins_lifecycle.rs"), code); } pub(crate) fn generate_core_mod_file(gen_path: &Path, submit_fn: &mut SubmitFn) { @@ -95,7 +255,8 @@ pub(crate) fn generate_core_central_file( gen_path: &Path, submit_fn: &mut SubmitFn, ) { - let central_items = make_central_items(api, build_config, ctx); + let builtin_types = BuiltinTypeMap::load(api); + let central_items = make_central_items(api, build_config, builtin_types, ctx); let core_code = make_core_code(¢ral_items); submit_fn(gen_path.join("central.rs"), core_code); @@ -108,8 +269,6 @@ fn make_sys_code(central_items: &CentralItems) -> TokenStream { variant_ty_enumerators_ord, variant_op_enumerators_pascal, variant_op_enumerators_ord, - variant_fn_decls, - variant_fn_inits, godot_version, .. } = central_items; @@ -118,10 +277,8 @@ fn make_sys_code(central_items: &CentralItems) -> TokenStream { let [opaque_32bit, opaque_64bit] = opaque_types; quote! { - use crate::{ - ffi_methods, GDExtensionConstTypePtr, GDExtensionTypePtr, GDExtensionUninitializedTypePtr, - GDExtensionUninitializedVariantPtr, GDExtensionVariantPtr, GodotFfi, - }; + use crate::{ffi_methods, GDExtensionTypePtr, GDExtensionVariantOperator, GDExtensionVariantType, GodotFfi}; + #[cfg(target_pointer_width = "32")] pub mod types { #(#opaque_32bit)* @@ -138,20 +295,6 @@ fn make_sys_code(central_items: &CentralItems) -> TokenStream { // ---------------------------------------------------------------------------------------------------------------------------------------------- - pub struct GlobalMethodTable { - #(#variant_fn_decls)* - } - - impl GlobalMethodTable { - pub(crate) unsafe fn load(interface: &crate::GDExtensionInterface) -> Self { - Self { - #(#variant_fn_inits)* - } - } - } - - // ---------------------------------------------------------------------------------------------------------------------------------------------- - #[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)] #[repr(i32)] pub enum VariantType { @@ -163,7 +306,7 @@ fn make_sys_code(central_items: &CentralItems) -> TokenStream { impl VariantType { #[doc(hidden)] - pub fn from_sys(enumerator: crate::GDExtensionVariantType) -> Self { + pub fn from_sys(enumerator: GDExtensionVariantType) -> Self { // Annoying, but only stable alternative is transmute(), which dictates enum size match enumerator { 0 => Self::Nil, @@ -175,7 +318,7 @@ fn make_sys_code(central_items: &CentralItems) -> TokenStream { } #[doc(hidden)] - pub fn sys(self) -> crate::GDExtensionVariantType { + pub fn sys(self) -> GDExtensionVariantType { self as _ } } @@ -198,7 +341,7 @@ fn make_sys_code(central_items: &CentralItems) -> TokenStream { impl VariantOperator { #[doc(hidden)] - pub fn from_sys(enumerator: crate::GDExtensionVariantOperator) -> Self { + pub fn from_sys(enumerator: GDExtensionVariantOperator) -> Self { match enumerator { #( #variant_op_enumerators_ord => Self::#variant_op_enumerators_pascal, @@ -208,7 +351,7 @@ fn make_sys_code(central_items: &CentralItems) -> TokenStream { } #[doc(hidden)] - pub fn sys(self) -> crate::GDExtensionVariantOperator { + pub fn sys(self) -> GDExtensionVariantOperator { self as _ } } @@ -324,6 +467,7 @@ fn make_core_code(central_items: &CentralItems) -> TokenStream { fn make_central_items( api: &ExtensionApi, build_config: [&str; 2], + builtin_types: BuiltinTypeMap, ctx: &mut Context, ) -> CentralItems { let mut opaque_types = [Vec::new(), Vec::new()]; @@ -338,12 +482,11 @@ fn make_central_items( } } - let builtin_types_map = collect_builtin_types(api); let variant_operators = collect_variant_operators(api); // Generate builtin methods, now with info for all types available. // Separate vectors because that makes usage in quote! easier. - let len = builtin_types_map.len(); + let len = builtin_types.count(); let mut result = CentralItems { opaque_types, @@ -352,33 +495,17 @@ fn make_central_items( variant_ty_enumerators_ord: Vec::with_capacity(len), variant_op_enumerators_pascal: Vec::new(), variant_op_enumerators_ord: Vec::new(), - variant_fn_decls: Vec::with_capacity(len), - variant_fn_inits: Vec::with_capacity(len), global_enum_defs: Vec::new(), godot_version: api.header.clone(), }; - let mut builtin_types: Vec<_> = builtin_types_map.values().collect(); - builtin_types.sort_by_key(|info| info.value); - // Note: NIL is not part of this iteration, it will be added manually - for ty in builtin_types { - // Note: both are token streams, containing multiple function declarations/initializations - let (decls, inits) = make_variant_fns( - &ty.type_names, - ty.has_destructor, - ty.constructors, - ty.operators, - &builtin_types_map, - ); - + for ty in builtin_types.ordered() { let (pascal_name, rust_ty, ord) = make_enumerator(&ty.type_names, ty.value, ctx); result.variant_ty_enumerators_pascal.push(pascal_name); result.variant_ty_enumerators_rust.push(rust_ty); result.variant_ty_enumerators_ord.push(ord); - result.variant_fn_decls.push(decls); - result.variant_fn_inits.push(inits); } for op in variant_operators { @@ -419,6 +546,235 @@ fn make_central_items( result } +fn make_builtin_lifecycle_table(builtin_types: &BuiltinTypeMap) -> TokenStream { + let len = builtin_types.count(); + let mut table = MethodTableInfo { + table_name: ident("BuiltinLifecycleTable"), + imports: quote! { + use crate::{ + GDExtensionConstTypePtr, GDExtensionTypePtr, GDExtensionUninitializedTypePtr, + GDExtensionUninitializedVariantPtr, GDExtensionVariantPtr, + }; + }, + ctor_parameters: quote! { + interface: &crate::GDExtensionInterface, + }, + pre_init_code: quote! { + let get_construct_fn = interface.variant_get_ptr_constructor.unwrap(); + let get_destroy_fn = interface.variant_get_ptr_destructor.unwrap(); + let get_operator_fn = interface.variant_get_ptr_operator_evaluator.unwrap(); + + let get_to_variant_fn = interface.get_variant_from_type_constructor.unwrap(); + let get_from_variant_fn = interface.get_variant_to_type_constructor.unwrap(); + }, + method_decls: Vec::with_capacity(len), + method_inits: Vec::with_capacity(len), + class_count: 0, + method_count: len, + }; + + // Note: NIL is not part of this iteration, it will be added manually + for ty in builtin_types.ordered() { + let (decls, inits) = make_variant_fns( + &ty.type_names, + ty.has_destructor, + ty.constructors, + ty.operators, + &builtin_types.map, + ); + + table.method_decls.push(decls); + table.method_inits.push(inits); + table.class_count += 1; + } + + make_method_table(table) +} + +fn make_class_method_table( + api: &ExtensionApi, + api_level: ClassCodegenLevel, + ctx: &mut Context, +) -> TokenStream { + let mut table = MethodTableInfo { + table_name: api_level.table_struct(), + imports: TokenStream::new(), + 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 special_cases::is_class_deleted(&TyName::from_godot(&class.name)) + || codegen_special_cases::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); + + 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; + } + + table.pre_init_code = quote! { + let get_method_bind = interface.classdb_get_method_bind.expect("classdb_get_method_bind absent"); + + #( #class_sname_decls )* + }; + + make_method_table(table) +} + +fn make_builtin_method_table(api: &ExtensionApi, builtin_types: &BuiltinTypeMap) -> TokenStream { + let mut table = MethodTableInfo { + table_name: ident("BuiltinMethodTable"), + imports: TokenStream::new(), + 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, + }; + + // TODO reuse builtin_types without api + for builtin in api.builtin_classes.iter() { + let Some(builtin_type) = builtin_types.map.get(&builtin.name) else { + continue; // for Nil + }; + + populate_builtin_methods(&mut table, builtin, &builtin_type.type_names); + table.class_count += 1; + } + + make_method_table(table) +} + +/// Returns whether at least 1 method was added. +fn populate_class_methods( + table: &mut MethodTableInfo, + class: &Class, + 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) { + 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); + + table.method_decls.push(method_decl); + table.method_inits.push(method_init); + table.method_count += 1; + } +} + +fn populate_builtin_methods( + table: &mut MethodTableInfo, + builtin_class: &BuiltinClass, + type_name: &TypeNames, +) { + for method in option_as_slice(&builtin_class.methods) { + if codegen_special_cases::is_builtin_method_excluded(method) { + continue; + } + + let field = util::make_builtin_method_ptr_name(type_name, method); + + let method_decl = quote! { pub #field: crate::BuiltinMethodBind, }; + let method_init = make_builtin_method_init(method, &field, type_name); + + table.method_decls.push(method_decl); + table.method_inits.push(method_init); + table.method_count += 1; + } +} + +fn make_class_method_init( + method: &ClassMethod, + field: &Ident, + class_var: &Ident, + class_name_str: &str, +) -> TokenStream { + 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 + ) + }); + + 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) + }, + } +} + +fn make_builtin_method_init( + method: &BuiltinClassMethod, + field: &Ident, + type_name: &TypeNames, +) -> 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; + + let hash = method.hash.unwrap_or_else(|| { + panic!( + "builtin method has no hash: {}::{}", + variant_type_str, method_name_str + ) + }); + + 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) + }, + } +} + /// Creates a map from "normalized" class names (lowercase without underscore, makes it easy to map from different conventions) /// to meta type information, including all the type name variants fn collect_builtin_classes(api: &ExtensionApi) -> HashMap { @@ -432,7 +788,7 @@ fn collect_builtin_classes(api: &ExtensionApi) -> HashMap class_map } -/// Returns map from "PackedStringArray" to all the info +/// Returns map from the JSON names (e.g. "PackedStringArray") to all the info. pub(crate) fn collect_builtin_types(api: &ExtensionApi) -> HashMap> { let class_map = collect_builtin_classes(api); @@ -549,11 +905,11 @@ fn make_variant_fns( let to_variant = format_ident!("{}_to_variant", type_names.snake_case); let from_variant = format_ident!("{}_from_variant", type_names.snake_case); - let to_variant_error = format_load_error(&to_variant); - let from_variant_error = format_load_error(&from_variant); + let to_variant_str = to_variant.to_string(); + let from_variant_str = from_variant.to_string(); let variant_type = &type_names.sys_variant_type; - let variant_type = quote! { crate:: #variant_type }; + let variant_type = quote! { crate::#variant_type }; // Field declaration // The target types are uninitialized-ptrs, because Godot performs placement new on those: @@ -571,12 +927,12 @@ fn make_variant_fns( // Field initialization in new() let init = quote! { #to_variant: { - let ctor_fn = interface.get_variant_from_type_constructor.unwrap(); - ctor_fn(#variant_type).expect(#to_variant_error) + let fptr = unsafe { get_to_variant_fn(#variant_type) }; + crate::validate_builtin_lifecycle(fptr, #to_variant_str) }, - #from_variant: { - let ctor_fn = interface.get_variant_to_type_constructor.unwrap(); - ctor_fn(#variant_type).expect(#from_variant_error) + #from_variant: { + let fptr = unsafe { get_from_variant_fn(#variant_type) }; + crate::validate_builtin_lifecycle(fptr, #from_variant_str) }, #op_eq_inits #op_lt_inits @@ -627,8 +983,8 @@ fn make_construct_fns( let construct_default = format_ident!("{}_construct_default", type_names.snake_case); let construct_copy = format_ident!("{}_construct_copy", type_names.snake_case); - let construct_default_error = format_load_error(&construct_default); - let construct_copy_error = format_load_error(&construct_copy); + let construct_default_str = construct_default.to_string(); + let construct_copy_str = construct_copy.to_string(); let variant_type = &type_names.sys_variant_type; let (construct_extra_decls, construct_extra_inits) = @@ -643,19 +999,23 @@ fn make_construct_fns( let decls = quote! { pub #construct_default: unsafe extern "C" fn(GDExtensionUninitializedTypePtr, *const GDExtensionConstTypePtr), pub #construct_copy: unsafe extern "C" fn(GDExtensionUninitializedTypePtr, *const GDExtensionConstTypePtr), - #(#construct_extra_decls)* + #( + #construct_extra_decls + )* }; let inits = quote! { #construct_default: { - let ctor_fn = interface.variant_get_ptr_constructor.unwrap(); - ctor_fn(crate:: #variant_type, 0i32).expect(#construct_default_error) + let fptr = unsafe { get_construct_fn(crate::#variant_type, 0i32) }; + crate::validate_builtin_lifecycle(fptr, #construct_default_str) }, #construct_copy: { - let ctor_fn = interface.variant_get_ptr_constructor.unwrap(); - ctor_fn(crate:: #variant_type, 1i32).expect(#construct_copy_error) + let fptr = unsafe { get_construct_fn(crate::#variant_type, 1i32) }; + crate::validate_builtin_lifecycle(fptr, #construct_copy_str) }, - #(#construct_extra_inits)* + #( + #construct_extra_inits + )* }; (decls, inits) @@ -674,7 +1034,7 @@ fn make_extra_constructors( for (i, ctor) in constructors.iter().enumerate().skip(2) { if let Some(args) = &ctor.arguments { let type_name = &type_names.snake_case; - let ident = if args.len() == 1 && args[0].name == "from" { + let construct_custom = if args.len() == 1 && args[0].name == "from" { // Conversion constructor is named according to the source type // String(NodePath from) => string_from_node_path let arg_type = &builtin_types[&args[0].type_].type_names.snake_case; @@ -689,16 +1049,16 @@ fn make_extra_constructors( format_ident!("{type_name}_from_{arg_names}") }; - let err = format_load_error(&ident); + let construct_custom_str = construct_custom.to_string(); extra_decls.push(quote! { - pub #ident: unsafe extern "C" fn(GDExtensionUninitializedTypePtr, *const GDExtensionConstTypePtr), + pub #construct_custom: unsafe extern "C" fn(GDExtensionUninitializedTypePtr, *const GDExtensionConstTypePtr), }); let i = i as i32; extra_inits.push(quote! { - #ident: { - let ctor_fn = interface.variant_get_ptr_constructor.unwrap(); - ctor_fn(crate:: #variant_type, #i).expect(#err) + #construct_custom: { + let fptr = unsafe { get_construct_fn(crate::#variant_type, #i) }; + crate::validate_builtin_lifecycle(fptr, #construct_custom_str) }, }); } @@ -713,6 +1073,7 @@ fn make_destroy_fns(type_names: &TypeNames, has_destructor: bool) -> (TokenStrea } let destroy = format_ident!("{}_destroy", type_names.snake_case); + let destroy_str = destroy.to_string(); let variant_type = &type_names.sys_variant_type; let decls = quote! { @@ -721,8 +1082,8 @@ fn make_destroy_fns(type_names: &TypeNames, has_destructor: bool) -> (TokenStrea let inits = quote! { #destroy: { - let dtor_fn = interface.variant_get_ptr_destructor.unwrap(); - dtor_fn(crate:: #variant_type).unwrap() + let fptr = unsafe { get_destroy_fn(crate::#variant_type) }; + crate::validate_builtin_lifecycle(fptr, #destroy_str) }, }; @@ -747,10 +1108,10 @@ fn make_operator_fns( type_names.snake_case, sys_name.to_ascii_lowercase() ); - let error = format_load_error(&operator); + let operator_str = operator.to_string(); let variant_type = &type_names.sys_variant_type; - let variant_type = quote! { crate:: #variant_type }; + let variant_type = quote! { crate::#variant_type }; let sys_ident = format_ident!("GDEXTENSION_VARIANT_OP_{}", sys_name); // Field declaration @@ -761,22 +1122,14 @@ fn make_operator_fns( // Field initialization in new() let init = quote! { #operator: { - let op_finder = interface.variant_get_ptr_operator_evaluator.unwrap(); - op_finder( - crate::#sys_ident, - #variant_type, - #variant_type, - ).expect(#error) + let fptr = unsafe { get_operator_fn(crate::#sys_ident, #variant_type, #variant_type) }; + crate::validate_builtin_lifecycle(fptr, #operator_str) }, }; (decl, init) } -fn format_load_error(ident: &impl std::fmt::Display) -> String { - format!("failed to load GDExtension function `{ident}`") -} - /// Returns true if the type is so trivial that most of its operations are directly provided by Rust, and there is no need /// to expose the construct/destruct/operator methods from Godot fn is_trivial(type_names: &TypeNames) -> bool { diff --git a/godot-codegen/src/class_generator.rs b/godot-codegen/src/class_generator.rs index 9943c3512..e42c22558 100644 --- a/godot-codegen/src/class_generator.rs +++ b/godot-codegen/src/class_generator.rs @@ -10,16 +10,17 @@ use proc_macro2::{Ident, Literal, TokenStream}; use quote::{format_ident, quote, ToTokens}; use std::path::Path; +use crate::api_parser::*; use crate::central_generator::{collect_builtin_types, BuiltinTypeInfo}; use crate::context::NotificationEnum; use crate::util::{ - ident, 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, + 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, }; -use crate::{api_parser::*, SubmitFn}; use crate::{ - special_cases, util, Context, GeneratedBuiltin, GeneratedBuiltinModule, GeneratedClass, - GeneratedClassModule, ModName, RustTy, TyName, + codegen_special_cases, special_cases, util, Context, GeneratedBuiltin, GeneratedBuiltinModule, + GeneratedClass, GeneratedClassModule, ModName, RustTy, SubmitFn, TyName, }; // ---------------------------------------------------------------------------------------------------------------------------------------------- @@ -236,12 +237,9 @@ pub(crate) fn generate_class_files( let class_name = TyName::from_godot(&class.name); let module_name = ModName::from_godot(&class.name); - #[cfg(not(feature = "codegen-full"))] - if !crate::SELECTED_CLASSES.contains(&class_name.godot_ty.as_str()) { - continue; - } - - if special_cases::is_class_deleted(&class_name) { + if special_cases::is_class_deleted(&class_name) + || codegen_special_cases::is_class_excluded(class_name.godot_ty.as_str()) + { continue; } @@ -429,12 +427,6 @@ fn make_module_doc(class_name: &TyName) -> String { ) } -fn make_string_name(identifier: &str) -> TokenStream { - quote! { - StringName::from(#identifier) - } -} - fn make_constructor(class: &Class, ctx: &Context) -> TokenStream { let godot_class_name = &class.name; let godot_class_stringname = make_string_name(godot_class_name); @@ -502,10 +494,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); @@ -1015,10 +1014,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) } @@ -1075,85 +1079,14 @@ fn make_special_builtin_methods(class_name: &TyName, _ctx: &Context) -> TokenStr } } -#[cfg(not(feature = "codegen-full"))] -fn is_type_excluded(ty: &str, ctx: &mut Context) -> bool { - fn is_class_excluded(class: &str) -> bool { - !crate::SELECTED_CLASSES.contains(&class) - } - - fn is_rust_type_excluded(ty: &RustTy) -> bool { - match ty { - RustTy::BuiltinIdent(_) => false, - RustTy::BuiltinArray(_) => false, - RustTy::RawPointer { inner, .. } => is_rust_type_excluded(&inner), - RustTy::EngineArray { elem_class, .. } => is_class_excluded(elem_class.as_str()), - RustTy::EngineEnum { - surrounding_class, .. - } => match surrounding_class.as_ref() { - None => false, - Some(class) => is_class_excluded(class.as_str()), - }, - RustTy::EngineClass { class, .. } => is_class_excluded(&class), - } - } - is_rust_type_excluded(&to_rust_type(ty, None, ctx)) -} - -fn is_method_excluded( - method: &ClassMethod, - is_virtual_impl: bool, - #[allow(unused_variables)] 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)) - }) - { - return true; - } - // -- end. - - if method.name.starts_with('_') && !is_virtual_impl { - return true; - } - - false -} - -#[cfg(feature = "codegen-full")] -fn is_function_excluded(_function: &UtilityFunction, _ctx: &mut Context) -> bool { - false -} - -#[cfg(not(feature = "codegen-full"))] -fn is_function_excluded(function: &UtilityFunction, ctx: &mut Context) -> bool { - function - .return_type - .as_ref() - .map_or(false, |ret| is_type_excluded(ret.as_str(), ctx)) - || function.arguments.as_ref().map_or(false, |args| { - args.iter() - .any(|arg| is_type_excluded(arg.type_.as_str(), ctx)) - }) -} - 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) + if codegen_special_cases::is_method_excluded(method, false, ctx) + || special_cases::is_deleted(class_name, &method.name) { return FnDefinition::none(); } @@ -1168,7 +1101,6 @@ fn make_method_definition( }*/ let method_name_str = special_cases::maybe_renamed(class_name, &method.name); - let method_name_stringname = make_string_name(method_name_str); let receiver = make_receiver( method.is_static, @@ -1176,34 +1108,22 @@ fn make_method_definition( quote! { self.object_ptr }, ); - let hash = method.hash; let is_varcall = method.is_vararg; - let variant_ffi = is_varcall.then(VariantFfi::variant_ptr); - let function_provider = if is_varcall { - ident("object_method_bind_call") + + let function_provider = if method.is_vararg { + // varcall + quote! { sys::interface_fn!(object_method_bind_call) } } else { - ident("object_method_bind_ptrcall") + // ptrcall + quote! { sys::interface_fn!(object_method_bind_ptrcall) } }; - let class_name_str = &class_name.godot_ty; - let class_name_stringname = make_string_name(class_name_str); + let fn_ptr = util::make_class_method_ptr_name(&class_name.godot_ty, method); + let init_code = quote! { - let __class_name = #class_name_stringname; - let __method_name = #method_name_stringname; - let __method_bind = sys::interface_fn!(classdb_get_method_bind)( - __class_name.string_sys(), - __method_name.string_sys(), - #hash - ); - assert!( - !__method_bind.is_null(), - "failed to load method {}::{} (hash {}) -- possible Godot/gdext version mismatch", - #class_name_str, - #method_name_str, - #hash - ); - let __call_fn = sys::interface_fn!(#function_provider); + let __method_bind = sys::#get_method_table().#fn_ptr; + let __call_fn = #function_provider; }; let receiver_ffi_arg = &receiver.ffi_arg; @@ -1241,27 +1161,24 @@ fn make_builtin_method_definition( type_info: &BuiltinTypeInfo, ctx: &mut Context, ) -> FnDefinition { + if codegen_special_cases::is_builtin_method_excluded(method) { + return FnDefinition::none(); + } + let method_name_str = &method.name; - let method_name_stringname = make_string_name(method_name_str); let return_value = method .return_type .as_deref() .map(MethodReturn::from_type_no_meta); - let hash = method.hash.expect("missing hash for builtin method"); + let is_varcall = method.is_vararg; let variant_ffi = is_varcall.then(VariantFfi::type_ptr); - let variant_type = &type_info.type_names.sys_variant_type; + let fn_ptr = util::make_builtin_method_ptr_name(&type_info.type_names, method); + let init_code = quote! { - let __variant_type = sys::#variant_type; - let __method_name = #method_name_stringname; - let __call_fn = sys::interface_fn!(variant_get_ptr_builtin_method)( - __variant_type, - __method_name.string_sys(), - #hash - ); - let __call_fn = __call_fn.unwrap_unchecked(); + let __call_fn = sys::builtin_method_table().#fn_ptr; }; let receiver = make_receiver(method.is_static, method.is_const, quote! { self.sys_ptr }); @@ -1297,23 +1214,20 @@ pub(crate) fn make_utility_function_definition( function: &UtilityFunction, ctx: &mut Context, ) -> TokenStream { - if is_function_excluded(function, ctx) { + if codegen_special_cases::is_function_excluded(function, ctx) { return TokenStream::new(); } let function_name_str = &function.name; - let function_name_stringname = make_string_name(function_name_str); + let fn_ptr = util::make_utility_function_ptr_name(function); let return_value = function .return_type .as_deref() .map(MethodReturn::from_type_no_meta); - let hash = function.hash; let variant_ffi = function.is_vararg.then_some(VariantFfi::type_ptr()); let init_code = quote! { - let __function_name = #function_name_stringname; - let __call_fn = sys::interface_fn!(variant_get_ptr_utility_function)(__function_name.string_sys(), #hash); - let __call_fn = __call_fn.unwrap_unchecked(); + let __call_fn = sys::utility_function_table().#fn_ptr; }; let invocation = quote! { __call_fn(return_ptr, __args_ptr, __args.len() as i32); @@ -2044,7 +1958,7 @@ fn make_all_virtual_methods( all_virtuals .into_iter() .filter_map(|method| { - if is_method_excluded(&method, true, ctx) { + if codegen_special_cases::is_method_excluded(&method, true, ctx) { None } else { Some(make_virtual_method(&method, ctx)) diff --git a/godot-codegen/src/codegen_special_cases.rs b/godot-codegen/src/codegen_special_cases.rs new file mode 100644 index 000000000..07203480b --- /dev/null +++ b/godot-codegen/src/codegen_special_cases.rs @@ -0,0 +1,170 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +//! Codegen-dependent exclusions. Can be removed if feature `codegen-full` is removed. + +use crate::api_parser::{BuiltinClassMethod, ClassMethod, UtilityFunction}; +use crate::context::Context; +use crate::{special_cases, TyName}; + +pub(crate) fn is_builtin_method_excluded(method: &BuiltinClassMethod) -> bool { + // Builtin class methods that need varcall are not currently available in GDExtension. + // See https://github.com/godot-rust/gdext/issues/382. + method.is_vararg +} + +#[cfg(not(feature = "codegen-full"))] +pub(crate) fn is_class_excluded(class: &str) -> bool { + !SELECTED_CLASSES.contains(&class) +} + +#[cfg(feature = "codegen-full")] +pub(crate) fn is_class_excluded(_class: &str) -> bool { + false +} + +#[cfg(not(feature = "codegen-full"))] +fn is_type_excluded(ty: &str, ctx: &mut Context) -> bool { + use crate::{util, RustTy}; + + fn is_rust_type_excluded(ty: &RustTy) -> bool { + match ty { + RustTy::BuiltinIdent(_) => false, + RustTy::BuiltinArray(_) => false, + RustTy::RawPointer { inner, .. } => is_rust_type_excluded(inner), + RustTy::EngineArray { elem_class, .. } => is_class_excluded(elem_class.as_str()), + RustTy::EngineEnum { + surrounding_class, .. + } => match surrounding_class.as_ref() { + None => false, + Some(class) => is_class_excluded(class.as_str()), + }, + RustTy::EngineClass { class, .. } => is_class_excluded(class), + } + } + is_rust_type_excluded(&util::to_rust_type(ty, None, ctx)) +} + +pub(crate) fn is_method_excluded( + method: &ClassMethod, + is_virtual_impl: bool, + ctx: &mut Context, +) -> bool { + let is_arg_or_return_excluded = |ty: &str, _ctx: &mut Context| { + let class_deleted = special_cases::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; + } + + // 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; + } + + false +} + +#[cfg(feature = "codegen-full")] +pub(crate) fn is_function_excluded(_function: &UtilityFunction, _ctx: &mut Context) -> bool { + false +} + +#[cfg(not(feature = "codegen-full"))] +pub(crate) fn is_function_excluded(function: &UtilityFunction, ctx: &mut Context) -> bool { + function + .return_type + .as_ref() + .map_or(false, |ret| is_type_excluded(ret.as_str(), ctx)) + || function.arguments.as_ref().map_or(false, |args| { + args.iter() + .any(|arg| is_type_excluded(arg.type_.as_str(), ctx)) + }) +} + +// ---------------------------------------------------------------------------------------------------------------------------------------------- +// Allowed-classes + +// Classes for minimal config +#[cfg(not(feature = "codegen-full"))] +const SELECTED_CLASSES: &[&str] = &[ + "AnimatedSprite2D", + "ArrayMesh", + "Area2D", + "AudioStreamPlayer", + "BaseButton", + "Button", + "BoxMesh", + "Camera2D", + "Camera3D", + "CanvasItem", + "CanvasLayer", + "ClassDB", + "CollisionObject2D", + "CollisionShape2D", + "Control", + "Engine", + "FileAccess", + "HTTPRequest", + "Image", + "ImageTextureLayered", + "Input", + "InputEvent", + "InputEventAction", + "Label", + "MainLoop", + "Marker2D", + "Mesh", + "Node", + "Node2D", + "Node3D", + "Node3DGizmo", + "Object", + "OS", + "PackedScene", + "PathFollow2D", + "PhysicsBody2D", + "PrimitiveMesh", + "RefCounted", + "RenderingServer", + "Resource", + "ResourceFormatLoader", + "ResourceLoader", + "RigidBody2D", + "SceneTree", + "Sprite2D", + "SpriteFrames", + "TextServer", + "TextServerExtension", + "Texture", + "Texture2DArray", + "TextureLayered", + "Time", + "Timer", + "Window", + "Viewport", +]; diff --git a/godot-codegen/src/context.rs b/godot-codegen/src/context.rs index 737bf5b24..974061bfa 100644 --- a/godot-codegen/src/context.rs +++ b/godot-codegen/src/context.rs @@ -5,7 +5,7 @@ */ use crate::api_parser::Class; -use crate::{util, ExtensionApi, GodotTy, RustTy, TyName}; +use crate::{codegen_special_cases, util, ExtensionApi, GodotTy, RustTy, TyName}; use proc_macro2::{Ident, TokenStream}; use quote::{format_ident, ToTokens}; use std::collections::{HashMap, HashSet}; @@ -24,7 +24,7 @@ pub(crate) struct Context<'a> { impl<'a> Context<'a> { pub fn build_from_api(api: &'a ExtensionApi) -> Self { - let mut ctx = Context::default(); + let mut ctx = Self::default(); for class in api.singletons.iter() { ctx.singletons.insert(class.name.as_str()); @@ -44,8 +44,7 @@ impl<'a> Context<'a> { for class in api.classes.iter() { let class_name = TyName::from_godot(&class.name); - #[cfg(not(feature = "codegen-full"))] - if !crate::SELECTED_CLASSES.contains(&class_name.godot_ty.as_str()) { + if codegen_special_cases::is_class_excluded(class_name.godot_ty.as_str()) { continue; } diff --git a/godot-codegen/src/lib.rs b/godot-codegen/src/lib.rs index 33b365b10..44c3e67e7 100644 --- a/godot-codegen/src/lib.rs +++ b/godot-codegen/src/lib.rs @@ -7,6 +7,7 @@ mod api_parser; mod central_generator; mod class_generator; +mod codegen_special_cases; mod context; mod interface_generator; mod special_cases; @@ -19,7 +20,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_mod_file, + generate_sys_classes_file, }; use class_generator::{ generate_builtin_class_files, generate_class_files, generate_native_structures_files, @@ -29,6 +30,10 @@ use interface_generator::generate_sys_interface_file; use util::{ident, to_pascal_case, to_snake_case}; use utilities_generator::generate_utilities_file; +use crate::central_generator::{ + generate_sys_builtin_lifecycle_file, generate_sys_builtin_methods_file, + generate_sys_utilities_file, BuiltinTypeMap, +}; use crate::context::NotificationEnum; use proc_macro2::{Ident, TokenStream}; use quote::{quote, ToTokens}; @@ -59,8 +64,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"); @@ -68,6 +71,19 @@ pub fn generate_sys_files( generate_sys_central_file(&api, &mut ctx, build_config, sys_gen_path, &mut submit_fn); 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); + 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); + // watch records inside the function. + + generate_sys_utilities_file(&api, &mut ctx, sys_gen_path, &mut submit_fn); + watch.record("generate_utilities_file"); + let is_godot_4_0 = api.header.version_major == 4 && api.header.version_minor == 0; generate_sys_interface_file(h_path, sys_gen_path, is_godot_4_0, &mut submit_fn); watch.record("generate_interface_file"); @@ -288,66 +304,3 @@ struct GeneratedBuiltinModule { class_name: TyName, module_name: ModName, } - -// ---------------------------------------------------------------------------------------------------------------------------------------------- -// Shared config - -// Classes for minimal config -#[cfg(not(feature = "codegen-full"))] -const SELECTED_CLASSES: &[&str] = &[ - "AnimatedSprite2D", - "ArrayMesh", - "Area2D", - "AudioStreamPlayer", - "BaseButton", - "Button", - "BoxMesh", - "Camera2D", - "Camera3D", - "CanvasItem", - "CanvasLayer", - "ClassDB", - "CollisionObject2D", - "CollisionShape2D", - "Control", - "Engine", - "FileAccess", - "HTTPRequest", - "Image", - "ImageTextureLayered", - "Input", - "InputEvent", - "InputEventAction", - "Label", - "MainLoop", - "Marker2D", - "Mesh", - "Node", - "Node2D", - "Node3D", - "Node3DGizmo", - "Object", - "OS", - "PackedScene", - "PathFollow2D", - "PhysicsBody2D", - "PrimitiveMesh", - "RefCounted", - "RenderingServer", - "Resource", - "ResourceFormatLoader", - "ResourceLoader", - "RigidBody2D", - "SceneTree", - "Sprite2D", - "SpriteFrames", - "TextServer", - "TextServerExtension", - "Texture", - "Texture2DArray", - "TextureLayered", - "Time", - "Timer", - "Window", - "Viewport", -]; diff --git a/godot-codegen/src/special_cases.rs b/godot-codegen/src/special_cases.rs index f2d286a9f..039d1862b 100644 --- a/godot-codegen/src/special_cases.rs +++ b/godot-codegen/src/special_cases.rs @@ -38,12 +38,81 @@ 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 { + // TODO feature-gate experimental classes. + /* + if !cfg!(feature = "experimental-godot-api") && 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] +#[allow(dead_code)] // remove once used. +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 93ad53a06..d7e136ea6 100644 --- a/godot-codegen/src/util.rs +++ b/godot-codegen/src/util.rs @@ -4,7 +4,10 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -use crate::api_parser::{ClassConstant, Enum}; +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}; @@ -16,11 +19,109 @@ 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()) + } + + pub 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) } +// 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 fn make_builtin_method_ptr_name( + variant_type: &TypeNames, + method: &BuiltinClassMethod, +) -> Ident { + format_ident!("{}__{}", variant_type.json_builtin_name, method.name) +} + +pub fn make_utility_function_ptr_name(function: &UtilityFunction) -> Ident { + safe_ident(&function.name) +} + +// TODO should eventually be removed, as all StringNames are cached +pub fn make_string_name(identifier: &str) -> TokenStream { + quote! { + StringName::from(#identifier) + } +} + +pub fn make_sname_ptr(identifier: &str) -> TokenStream { + quote! { + string_names.fetch(#identifier) + } +} + +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 1c115936d..c5f718d79 100644 --- a/godot-core/src/init/mod.rs +++ b/godot-core/src/init/mod.rs @@ -5,10 +5,8 @@ */ use godot_ffi as sys; -use sys::out; use std::cell; -use std::collections::BTreeMap; #[doc(hidden)] // TODO consider body safe despite unsafe function, and explicitly mark unsafe {} locations @@ -30,20 +28,18 @@ pub unsafe fn __gdext_load_library( sys::initialize(interface_or_get_proc_address, library, config); - let mut handle = InitHandle::new(); - - let success = E::load_library(&mut handle); - // No early exit, unclear if Godot still requires output parameters to be set + // Currently no way to express failure; could be exposed to E if necessary. + // No early exit, unclear if Godot still requires output parameters to be set. + let success = true; let godot_init_params = sys::GDExtensionInitialization { - minimum_initialization_level: handle.lowest_init_level().to_sys(), + minimum_initialization_level: E::min_level().to_sys(), userdata: std::ptr::null_mut(), - initialize: Some(ffi_initialize_layer), - deinitialize: Some(ffi_deinitialize_layer), + initialize: Some(ffi_initialize_layer::), + deinitialize: Some(ffi_deinitialize_layer::), }; *init = godot_init_params; - INIT_HANDLE = Some(handle); success as u8 }; @@ -54,45 +50,63 @@ pub unsafe fn __gdext_load_library( is_success.unwrap_or(0) } -unsafe extern "C" fn ffi_initialize_layer( +unsafe extern "C" fn ffi_initialize_layer( _userdata: *mut std::ffi::c_void, init_level: sys::GDExtensionInitializationLevel, ) { - let ctx = || { - format!( - "failed to initialize GDExtension layer `{:?}`", - InitLevel::from_sys(init_level) - ) - }; + let level = InitLevel::from_sys(init_level); + let ctx = || format!("failed to initialize GDExtension level `{:?}`", level); - crate::private::handle_panic(ctx, || { - let handle = INIT_HANDLE.as_mut().unwrap(); - handle.run_init_function(InitLevel::from_sys(init_level)); + // 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); }); } -unsafe extern "C" fn ffi_deinitialize_layer( +unsafe extern "C" fn ffi_deinitialize_layer( _userdata: *mut std::ffi::c_void, init_level: sys::GDExtensionInitializationLevel, ) { - let ctx = || { - format!( - "failed to deinitialize GDExtension layer `{:?}`", - InitLevel::from_sys(init_level) - ) - }; + let level = InitLevel::from_sys(init_level); + let ctx = || format!("failed to deinitialize GDExtension level `{:?}`", level); - crate::private::handle_panic(ctx, || { - let handle = INIT_HANDLE.as_mut().unwrap(); - handle.run_deinit_function(InitLevel::from_sys(init_level)); + // Swallow panics. + let _ = crate::private::handle_panic(ctx, || { + E::on_level_deinit(level); + gdext_on_level_deinit(level); }); } -// ---------------------------------------------------------------------------------------------------------------------------------------------- +/// Tasks needed to be done by gdext internally upon loading an initialization level. Called before user code. +fn gdext_on_level_init(level: InitLevel) { + // SAFETY: we are in the main thread, during initialization, no other logic is happening. + // TODO: in theory, a user could start a thread in one of the early levels, and run concurrent code that messes with the global state + // (e.g. class registration). This would break the assumption that the load_class_method_table() calls are exclusive. + // We could maybe protect globals with a mutex until initialization is complete, and then move it to a directly-accessible, read-only static. + unsafe { + match level { + InitLevel::Core => {} + InitLevel::Servers => { + sys::load_class_method_table(sys::ClassApiLevel::Server); + } + InitLevel::Scene => { + sys::load_class_method_table(sys::ClassApiLevel::Scene); + crate::auto_register_classes(); + } + InitLevel::Editor => { + sys::load_class_method_table(sys::ClassApiLevel::Editor); + } + } + } +} -// FIXME make safe -#[doc(hidden)] -pub static mut INIT_HANDLE: Option = None; +/// Tasks needed to be done by gdext internally upon unloading an initialization level. Called after user code. +fn gdext_on_level_deinit(_level: InitLevel) { + // No logic at the moment. +} + +// ---------------------------------------------------------------------------------------------------------------------------------------------- /// Defines the entry point for a GDExtension Rust library. /// @@ -125,15 +139,31 @@ pub static mut INIT_HANDLE: Option = None; /// [safety]: https://godot-rust.github.io/book/gdext/advanced/safety.html // FIXME intra-doc link pub unsafe trait ExtensionLibrary { - fn load_library(handle: &mut InitHandle) -> bool { - handle.register_layer(InitLevel::Scene, DefaultLayer); - true - } - /// Determines if and how an extension's code is run in the editor. fn editor_run_behavior() -> EditorRunBehavior { EditorRunBehavior::ToolClassesOnly } + + /// Determines the initialization level at which the extension is loaded (`Scene` by default). + /// + /// If the level is lower than [`InitLevel::Scene`], the engine needs to be restarted to take effect. + fn min_level() -> InitLevel { + InitLevel::Scene + } + + /// Custom logic when a certain init-level of Godot is loaded. + /// + /// This will only be invoked for levels >= [`Self::min_level()`], in ascending order. Use `if` or `match` to hook to specific levels. + fn on_level_init(_level: InitLevel) { + // Nothing by default. + } + + /// Custom logic when a certain init-level of Godot is unloaded. + /// + /// This will only be invoked for levels >= [`Self::min_level()`], in descending order. Use `if` or `match` to hook to specific levels. + fn on_level_deinit(_level: InitLevel) { + // Nothing by default. + } } /// Determines if and how an extension's code is run in the editor. @@ -162,91 +192,28 @@ pub enum EditorRunBehavior { AllClasses, } -pub trait ExtensionLayer: 'static { - fn initialize(&mut self); - fn deinitialize(&mut self); -} - -// ---------------------------------------------------------------------------------------------------------------------------------------------- - -struct DefaultLayer; - -impl ExtensionLayer for DefaultLayer { - fn initialize(&mut self) { - crate::auto_register_classes(); - } - - fn deinitialize(&mut self) { - // Nothing -- note that any cleanup task should be performed outside of this method, - // as the user is free to use a different impl, so cleanup code may not be run. - } -} - -// ---------------------------------------------------------------------------------------------------------------------------------------------- - -pub struct InitHandle { - layers: BTreeMap>, - // success: bool, -} - -impl InitHandle { - pub fn new() -> Self { - Self { - layers: BTreeMap::new(), - // success: true, - } - } - - pub fn register_layer(&mut self, level: InitLevel, layer: impl ExtensionLayer) { - self.layers.insert(level, Box::new(layer)); - } - - // pub fn mark_failed(&mut self) { - // self.success = false; - // } - - pub fn lowest_init_level(&self) -> InitLevel { - self.layers - .iter() - .next() - .map(|(k, _v)| *k) - .unwrap_or(InitLevel::Scene) - } - - pub fn run_init_function(&mut self, level: InitLevel) { - // if let Some(f) = self.init_levels.remove(&level) { - // f(); - // } - if let Some(layer) = self.layers.get_mut(&level) { - out!("init: initialize level {level:?}..."); - layer.initialize() - } else { - out!("init: skip init of level {level:?}."); - } - } - - pub fn run_deinit_function(&mut self, level: InitLevel) { - if let Some(layer) = self.layers.get_mut(&level) { - out!("init: deinitialize level {level:?}..."); - layer.deinitialize() - } else { - out!("init: skip deinit of level {level:?}."); - } - } -} - -impl Default for InitHandle { - fn default() -> Self { - Self::new() - } -} // ---------------------------------------------------------------------------------------------------------------------------------------------- +/// Stage of the Godot initialization process. +/// +/// Godot's initialization and deinitialization processes are split into multiple stages, like a stack. At each level, +/// a different amount of engine functionality is available. Deinitialization happens in reverse order. +/// +/// See also: +/// - [`ExtensionLibrary::on_level_init()`] +/// - [`ExtensionLibrary::on_level_deinit()`] #[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)] pub enum InitLevel { + /// First level loaded by Godot. Builtin types are available, classes are not. Core, + + /// Second level loaded by Godot. Only server classes and builtins are available. Servers, + + /// Third level loaded by Godot. Most classes are available. Scene, + + /// Fourth level loaded by Godot, only in the editor. All classes are available. Editor, } 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/godot_ffi.rs b/godot-ffi/src/godot_ffi.rs index bb2d2edd0..e89071708 100644 --- a/godot-ffi/src/godot_ffi.rs +++ b/godot-ffi/src/godot_ffi.rs @@ -4,8 +4,9 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -use crate::{self as sys, ptr_then}; +use crate as sys; use std::{error::Error, fmt::Debug, marker::PhantomData, ptr}; +use sys::ptr_then; /// Adds methods to convert from and to Godot FFI pointers. /// See [crate::ffi_methods] for ergonomic implementation. diff --git a/godot-ffi/src/lib.rs b/godot-ffi/src/lib.rs index 1594d7007..230f63807 100644 --- a/godot-ffi/src/lib.rs +++ b/godot-ffi/src/lib.rs @@ -17,13 +17,25 @@ deref_nullptr, clippy::redundant_static_lifetimes )] -pub(crate) mod gen; +pub(crate) mod gen { + pub mod table_builtins; + pub mod table_builtins_lifecycle; + pub mod table_servers_classes; + pub mod table_scene_classes; + pub mod table_editor_classes; + pub mod table_utilities; + + pub mod central; + pub mod gdextension_interface; + pub mod interface; +} mod compat; mod gdextension_plus; mod godot_ffi; mod opaque; mod plugins; +mod string_cache; mod toolbox; use compat::BindingCompat; @@ -39,19 +51,42 @@ pub use crate::godot_ffi::{ from_sys_init_or_init_default, GodotFfi, GodotFuncMarshal, GodotNullablePtr, PrimitiveConversionError, PtrcallType, }; + +// Method tables +pub use gen::table_builtins::*; +pub use gen::table_builtins_lifecycle::*; +pub use gen::table_editor_classes::*; +pub use gen::table_scene_classes::*; +pub use gen::table_servers_classes::*; +pub use gen::table_utilities::*; + +// Other pub use gdextension_plus::*; pub use gen::central::*; pub use gen::gdextension_interface::*; pub use gen::interface::*; +pub use string_cache::StringCache; pub use toolbox::*; // ---------------------------------------------------------------------------------------------------------------------------------------------- // API to access Godot via FFI +#[derive(Debug)] +pub enum ClassApiLevel { + Server, + Scene, + Editor, +} + struct GodotBinding { interface: GDExtensionInterface, library: GDExtensionClassLibraryPtr, - method_table: GlobalMethodTable, + global_method_table: BuiltinLifecycleTable, + 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, + utility_function_table: UtilityFunctionTable, runtime_metadata: GdextRuntimeMetadata, config: GdextConfig, } @@ -99,16 +134,31 @@ pub unsafe fn initialize( let interface = compat.load_interface(); out!("Loaded interface."); - let method_table = GlobalMethodTable::load(&interface); - out!("Loaded builtin table."); + let global_method_table = BuiltinLifecycleTable::load(&interface); + out!("Loaded global method table."); + + let mut string_names = StringCache::new(&interface, &global_method_table); + + let builtin_method_table = BuiltinMethodTable::load(&interface, &mut string_names); + out!("Loaded builtin method table."); + + let utility_function_table = UtilityFunctionTable::load(&interface, &mut string_names); + out!("Loaded utility function table."); let runtime_metadata = GdextRuntimeMetadata { godot_version: version, }; + drop(string_names); + BINDING = Some(GodotBinding { interface, - method_table, + global_method_table, + class_server_method_table: None, + class_scene_method_table: None, + class_editor_method_table: None, + builtin_method_table, + utility_function_table, library, runtime_metadata, config, @@ -150,8 +200,115 @@ pub unsafe fn get_library() -> GDExtensionClassLibraryPtr { /// /// The interface must have been initialised with [`initialize`] before calling this function. #[inline(always)] -pub unsafe fn method_table() -> &'static GlobalMethodTable { - &unwrap_ref_unchecked(&BINDING).method_table +pub unsafe fn method_table() -> &'static BuiltinLifecycleTable { + &unwrap_ref_unchecked(&BINDING).global_method_table +} + +/// # Safety +/// +/// The interface must have been initialised with [`initialize`] before calling this function. +#[inline(always)] +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() +} + +/// # Safety +/// +/// The interface must have been initialised with [`initialize`] before calling this function. +#[inline(always)] +pub unsafe fn builtin_method_table() -> &'static BuiltinMethodTable { + &unwrap_ref_unchecked(&BINDING).builtin_method_table +} + +/// # Safety +/// +/// The interface must have been initialised with [`initialize`] before calling this function. +#[inline(always)] +pub unsafe fn utility_function_table() -> &'static UtilityFunctionTable { + &unwrap_ref_unchecked(&BINDING).utility_function_table +} + +/// # Safety +/// +/// The interface must have been initialised with [`initialize`] before calling this function. +#[inline(always)] +pub unsafe fn load_class_method_table(api_level: ClassApiLevel) { + let binding = unwrap_ref_unchecked_mut(&mut BINDING); + + out!("Load class method table for level '{:?}'...", api_level); + let begin = std::time::Instant::now(); + + 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!( + "{:?} level: loaded {} classes and {} methods in {}s.", + api_level, + class_count, + method_count, + _elapsed.as_secs_f64() + ); } /// # Safety diff --git a/godot-ffi/src/string_cache.rs b/godot-ffi/src/string_cache.rs new file mode 100644 index 000000000..7e8834153 --- /dev/null +++ b/godot-ffi/src/string_cache.rs @@ -0,0 +1,129 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +use crate as sys; + +use std::collections::HashMap; +use std::mem::MaybeUninit; +use std::ptr; + +/// Caches `StringName` instances at initialization. +pub struct StringCache<'a> { + // Box is needed for element stability (new insertions don't move object; i.e. pointers to it remain valid). + instances_by_str: HashMap<&'static str, Box>, + interface: &'a sys::GDExtensionInterface, + builtin_lifecycle: &'a sys::BuiltinLifecycleTable, +} + +impl<'a> StringCache<'a> { + pub fn new( + interface: &'a sys::GDExtensionInterface, + builtin_lifecycle: &'a sys::BuiltinLifecycleTable, + ) -> Self { + Self { + instances_by_str: HashMap::new(), + interface, + builtin_lifecycle, + } + } + + /// Get a pointer to a `StringName`. Reuses cached instances, only deallocates on destruction of this cache. + pub fn fetch(&mut self, key: &'static str) -> sys::GDExtensionStringNamePtr { + assert!(key.is_ascii(), "string is not ASCII: {key}"); + + // Already cached. + if let Some(opaque_box) = self.instances_by_str.get_mut(key) { + return box_to_sname_ptr(opaque_box); + } + + let string_name_from_string = self.builtin_lifecycle.string_name_from_string; + let string_destroy = self.builtin_lifecycle.string_destroy; + + let mut string = MaybeUninit::::uninit(); + let string_ptr = string.as_mut_ptr(); + + let mut sname = MaybeUninit::::uninit(); + let sname_ptr = sname.as_mut_ptr(); + + let opaque = unsafe { + let string_new_with_latin1_chars_and_len = self + .interface + .string_new_with_latin1_chars_and_len + .unwrap_unchecked(); + + // Construct String. + string_new_with_latin1_chars_and_len( + string_uninit_ptr(string_ptr), + key.as_ptr() as *const std::os::raw::c_char, + key.len() as sys::GDExtensionInt, + ); + + // Convert String -> StringName. + string_name_from_string( + sname_uninit_type_ptr(sname_ptr), + [sys::to_const_ptr(string_type_ptr(string_ptr))].as_ptr(), + ); + + // Destroy String. + string_destroy(string_type_ptr(string_ptr)); + + // Return StringName. + sname.assume_init() + }; + + let mut opaque_box = Box::new(opaque); + let sname_ptr = box_to_sname_ptr(&mut opaque_box); + + self.instances_by_str.insert(key, opaque_box); + sname_ptr + } +} + +/// Destroy all string names. +impl<'a> Drop for StringCache<'a> { + fn drop(&mut self) { + let string_name_destroy = self.builtin_lifecycle.string_name_destroy; + + unsafe { + for (_, mut opaque_box) in self.instances_by_str.drain() { + let opaque_ptr = ptr::addr_of_mut!(*opaque_box); + string_name_destroy(sname_type_ptr(opaque_ptr)); + } + } + } +} + +// ---------------------------------------------------------------------------------------------------------------------------------------------- +// Implementation +// These are tiny wrappers to avoid exposed `as` casts (which are very easy to get wrong, i.e. extra dereference). +// Using a trait to abstract over String/StringName is overkill and also doesn't work due to both having same Opaque type. + +fn box_to_sname_ptr( + boxed: &mut Box, +) -> sys::GDExtensionStringNamePtr { + let opaque_ptr = ptr::addr_of_mut!(**boxed); + opaque_ptr as sys::GDExtensionStringNamePtr +} + +unsafe fn string_type_ptr(opaque_ptr: *mut sys::types::OpaqueString) -> sys::GDExtensionTypePtr { + ptr::addr_of_mut!(*opaque_ptr) as sys::GDExtensionTypePtr +} + +unsafe fn string_uninit_ptr( + opaque_ptr: *mut sys::types::OpaqueString, +) -> sys::GDExtensionUninitializedStringPtr { + ptr::addr_of_mut!(*opaque_ptr) as sys::GDExtensionUninitializedStringPtr +} + +unsafe fn sname_type_ptr(opaque_ptr: *mut sys::types::OpaqueStringName) -> sys::GDExtensionTypePtr { + ptr::addr_of_mut!(*opaque_ptr) as sys::GDExtensionTypePtr +} + +unsafe fn sname_uninit_type_ptr( + opaque_ptr: *mut sys::types::OpaqueStringName, +) -> sys::GDExtensionUninitializedTypePtr { + ptr::addr_of_mut!(*opaque_ptr) as sys::GDExtensionUninitializedTypePtr +} diff --git a/godot-ffi/src/toolbox.rs b/godot-ffi/src/toolbox.rs index 34b6fca0a..585bea08f 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 @@ -73,24 +75,13 @@ macro_rules! debug_assert_godot { debug_assert!( $expr, "Godot engine not available; make sure you are not calling it from unit/doc tests" - ); // previous message: "unchecked access to Option::None" + ); }; } // ---------------------------------------------------------------------------------------------------------------------------------------------- // 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(), - } -} - /// Extract value from box before `into_inner()` is stable #[allow(clippy::boxed_local)] // false positive pub fn unbox(value: Box) -> T { @@ -165,3 +156,105 @@ 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) 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, + hash: i64, +) -> BuiltinMethodBind { + /*crate::out!( + "Load builtin method {}::{} (hash {})...", + variant_type, + method_name, + hash + );*/ + method.unwrap_or_else(|| { + panic!("Failed to load builtin method {variant_type}::{method_name} (hash {hash}).{INFO}") + }) +} + +pub(crate) fn validate_builtin_lifecycle(function: Option, description: &str) -> T { + function.unwrap_or_else(|| { + panic!("Failed to load builtin lifecycle function {description}.{INFO}",) + }) +} + +pub(crate) fn validate_utility_function( + utility_fn: sys::GDExtensionPtrUtilityFunction, + name: &str, + hash: i64, +) -> UtilityFunctionBind { + utility_fn.unwrap_or_else(|| { + panic!("Failed to load builtin lifecycle function {name} (hash {hash}).{INFO}") + }) +} + +const INFO: &str = "\nMake sure gdext and Godot are compatible: https://godot-rust.github.io/book/gdext/advanced/compatibility.html"; diff --git a/godot-macros/src/class/data_models/func.rs b/godot-macros/src/class/data_models/func.rs index 903bf42fe..2ef43f246 100644 --- a/godot-macros/src/class/data_models/func.rs +++ b/godot-macros/src/class/data_models/func.rs @@ -39,7 +39,7 @@ pub fn make_virtual_method_callback( unsafe extern "C" fn function( instance_ptr: sys::GDExtensionClassInstancePtr, - args: *const sys::GDExtensionConstTypePtr, + args_ptr: *const sys::GDExtensionConstTypePtr, ret: sys::GDExtensionTypePtr, ) { #invocation; @@ -244,19 +244,20 @@ fn make_varcall_func( wrapped_method: &TokenStream, ) -> TokenStream { let invocation = make_varcall_invocation(method_name, sig_tuple, wrapped_method); + let method_name_str = method_name.to_string(); quote! { { unsafe extern "C" fn function( _method_data: *mut std::ffi::c_void, instance_ptr: sys::GDExtensionClassInstancePtr, - args: *const sys::GDExtensionConstVariantPtr, + args_ptr: *const sys::GDExtensionConstVariantPtr, _arg_count: sys::GDExtensionInt, ret: sys::GDExtensionVariantPtr, err: *mut sys::GDExtensionCallError, ) { let success = ::godot::private::handle_panic( - || stringify!(#method_name), + || #method_name_str, || #invocation ); @@ -287,7 +288,7 @@ fn make_ptrcall_func( unsafe extern "C" fn function( _method_data: *mut std::ffi::c_void, instance_ptr: sys::GDExtensionClassInstancePtr, - args: *const sys::GDExtensionConstTypePtr, + args_ptr: *const sys::GDExtensionConstTypePtr, ret: sys::GDExtensionTypePtr, ) { let success = ::godot::private::handle_panic( @@ -323,7 +324,7 @@ fn make_ptrcall_invocation( quote! { <#sig_tuple as ::godot::builtin::meta::PtrcallSignatureTuple>::ptrcall( instance_ptr, - args, + args_ptr, ret, #wrapped_method, #method_name_str, @@ -343,7 +344,7 @@ fn make_varcall_invocation( quote! { <#sig_tuple as ::godot::builtin::meta::VarcallSignatureTuple>::varcall( instance_ptr, - args, + args_ptr, ret, err, #wrapped_method, diff --git a/godot/src/lib.rs b/godot/src/lib.rs index c967551c5..2aa5ecc6a 100644 --- a/godot/src/lib.rs +++ b/godot/src/lib.rs @@ -196,7 +196,7 @@ pub mod prelude { PackedSceneVirtual, RefCounted, RefCountedVirtual, Resource, ResourceVirtual, SceneTree, SceneTreeVirtual, }; - pub use super::init::{gdextension, ExtensionLayer, ExtensionLibrary, InitHandle, InitLevel}; + pub use super::init::{gdextension, ExtensionLibrary, InitLevel}; pub use super::log::*; pub use super::obj::{Base, Gd, GdMut, GdRef, GodotClass, Inherits, InstanceId, Share};