diff --git a/.github/workflows/full-ci.yml b/.github/workflows/full-ci.yml index 44169789e..6f8fa0f28 100644 --- a/.github/workflows/full-ci.yml +++ b/.github/workflows/full-ci.yml @@ -142,6 +142,7 @@ jobs: - name: "Test" run: cargo test $GDEXT_FEATURES ${{ matrix.rust-extra-args }} + # For complex matrix workflow, see https://stackoverflow.com/a/65434401 godot-itest: name: godot-itest (${{ matrix.name }}) @@ -178,6 +179,13 @@ jobs: godot-binary: godot.macos.editor.dev.x86_64 #godot-prebuilt-patch: '4.1' + # TODO merge with other jobs + - name: macos-lazy-fptrs + os: macos-12 + artifact-name: macos-nightly + godot-binary: godot.macos.editor.dev.x86_64 + rust-extra-args: --features godot/lazy-function-tables + # Windows - name: windows @@ -198,6 +206,13 @@ jobs: godot-binary: godot.windows.editor.dev.x86_64.exe #godot-prebuilt-patch: '4.1' + # TODO merge with other jobs + - name: windows-lazy-fptrs + os: windows-latest + artifact-name: windows-nightly + godot-binary: godot.windows.editor.dev.x86_64.exe + rust-extra-args: --features godot/lazy-function-tables + # Linux # Don't use latest Ubuntu (22.04) as it breaks lots of ecosystem compatibility. @@ -220,6 +235,13 @@ jobs: godot-binary: godot.linuxbsd.editor.dev.x86_64 rust-extra-args: --features godot/custom-godot,godot/experimental-threads,godot/serde + # TODO merge with other jobs + - name: linux-lazy-fptrs + os: ubuntu-20.04 + artifact-name: linux-nightly + godot-binary: godot.linuxbsd.editor.dev.x86_64 + rust-extra-args: --features godot/lazy-function-tables + # Linux compat - name: linux-4.1.1 diff --git a/.github/workflows/minimal-ci.yml b/.github/workflows/minimal-ci.yml index 0a22fdae1..2ba22a18b 100644 --- a/.github/workflows/minimal-ci.yml +++ b/.github/workflows/minimal-ci.yml @@ -151,6 +151,13 @@ jobs: godot-binary: godot.linuxbsd.editor.dev.x86_64 rust-extra-args: --features godot/custom-godot,godot/experimental-threads,godot/serde + # TODO merge with other jobs + - name: linux-lazy-fptrs + os: ubuntu-20.04 + artifact-name: linux-nightly + godot-binary: godot.linuxbsd.editor.dev.x86_64 + rust-extra-args: --features godot/lazy-function-tables + # Linux compat - name: linux-4.0.4 diff --git a/godot-codegen/Cargo.toml b/godot-codegen/Cargo.toml index cee2ec8f0..e007e6782 100644 --- a/godot-codegen/Cargo.toml +++ b/godot-codegen/Cargo.toml @@ -11,6 +11,7 @@ categories = ["game-engines", "graphics"] default = ["codegen-fmt"] codegen-fmt = [] codegen-full = [] +codegen-lazy-fptrs = [] double-precision = [] custom-godot = ["godot-bindings/custom-godot"] experimental-godot-api = [] diff --git a/godot-codegen/src/central_generator.rs b/godot-codegen/src/central_generator.rs index a75c15247..2cd3c68ce 100644 --- a/godot-codegen/src/central_generator.rs +++ b/godot-codegen/src/central_generator.rs @@ -39,6 +39,7 @@ struct NamedMethodTable { method_count: usize, } +#[allow(dead_code)] // for lazy feature struct IndexedMethodTable { table_name: Ident, imports: TokenStream, @@ -46,6 +47,8 @@ struct IndexedMethodTable { pre_init_code: TokenStream, fptr_type: TokenStream, method_inits: Vec, + lazy_key_type: TokenStream, + lazy_method_init: TokenStream, named_accessors: Vec, class_count: usize, method_count: usize, @@ -53,6 +56,7 @@ struct IndexedMethodTable { // ---------------------------------------------------------------------------------------------------------------------------------------------- +#[cfg_attr(feature = "codegen-lazy-fptrs", allow(dead_code))] struct MethodInit { method_init: TokenStream, index: usize, @@ -69,6 +73,7 @@ impl ToTokens for MethodInit { struct AccessorMethod { name: Ident, index: usize, + lazy_key: TokenStream, } // ---------------------------------------------------------------------------------------------------------------------------------------------- @@ -260,7 +265,8 @@ fn make_named_method_table(info: NamedMethodTable) -> TokenStream { } } -fn make_indexed_method_table(info: IndexedMethodTable) -> TokenStream { +#[cfg(not(feature = "codegen-lazy-fptrs"))] +fn make_method_table(info: IndexedMethodTable) -> TokenStream { let IndexedMethodTable { table_name, imports, @@ -268,6 +274,8 @@ fn make_indexed_method_table(info: IndexedMethodTable) -> TokenStream { pre_init_code, fptr_type, mut method_inits, + lazy_key_type: _, + lazy_method_init: _, named_accessors, class_count, method_count, @@ -301,7 +309,7 @@ fn make_indexed_method_table(info: IndexedMethodTable) -> TokenStream { #imports pub struct #table_name { - function_pointers: [#fptr_type; #method_count], + function_pointers: Vec<#fptr_type>, } impl #table_name { @@ -315,7 +323,7 @@ fn make_indexed_method_table(info: IndexedMethodTable) -> TokenStream { #pre_init_code Self { - function_pointers: [ + function_pointers: vec![ #( #method_inits )* ] } @@ -334,6 +342,79 @@ fn make_indexed_method_table(info: IndexedMethodTable) -> TokenStream { } } +#[cfg(feature = "codegen-lazy-fptrs")] +fn make_method_table(info: IndexedMethodTable) -> TokenStream { + let IndexedMethodTable { + table_name, + imports, + ctor_parameters: _, + pre_init_code: _, + fptr_type, + method_inits: _, + lazy_key_type, + lazy_method_init, + 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); + + // Assumes that inits already have a trailing comma. + // This is necessary because some generators emit multiple lines (statements) per element. + quote! { + #imports + use crate::StringCache; + use std::collections::HashMap; + use std::cell::RefCell; + + // Exists to be stored inside RefCell. + struct InnerTable { + // 'static because at this point, the interface and lifecycle tables are globally available. + string_cache: StringCache<'static>, + function_pointers: HashMap<#lazy_key_type, #fptr_type>, + } + + // Note: get_method_bind and other function pointers could potentially be stored as fields in table, to avoid interface_fn!. + pub struct #table_name { + inner: RefCell, + } + + impl #table_name { + pub const CLASS_COUNT: usize = #class_count; + pub const METHOD_COUNT: usize = #method_count; + + #unused_attr + pub fn load() -> Self { + // SAFETY: interface and lifecycle tables are initialized at this point, so we can get 'static references to them. + let (interface, lifecycle_table) = unsafe { + (crate::get_interface(), crate::method_table()) + }; + + Self { + inner: RefCell::new(InnerTable { + string_cache: StringCache::new(interface, lifecycle_table), + function_pointers: HashMap::new(), + }), + } + } + + #[inline(always)] + pub fn fptr_by_key(&self, key: #lazy_key_type) -> #fptr_type { + let mut guard = self.inner.borrow_mut(); + let inner = &mut *guard; + *inner.function_pointers.entry(key.clone()).or_insert_with(|| { + #lazy_method_init + }) + } + + #named_method_api + } + } +} + pub(crate) fn generate_sys_builtin_methods_file( api: &ExtensionApi, builtin_types: &BuiltinTypeMap, @@ -516,6 +597,32 @@ fn make_build_config(header: &Header) -> TokenStream { }; (version.major as u8, version.minor as u8, version.patch as u8) } + + /// For a string "4.x", returns `true` if the current Godot version is strictly less than 4.x. + /// + /// Runtime equivalent of `#[cfg(before_api = "4.x")]`. + /// + /// # Panics + /// On bad input. + pub fn before_api(major_minor: &str) -> bool { + let mut parts = major_minor.split('.'); + let queried_major = parts.next().unwrap().parse::().expect("invalid major version"); + let queried_minor = parts.next().unwrap().parse::().expect("invalid minor version"); + assert_eq!(queried_major, 4, "major version must be 4"); + + let (_, minor, _) = Self::godot_runtime_version_triple(); + minor < queried_minor + } + + /// For a string "4.x", returns `true` if the current Godot version is equal or greater to 4.x. + /// + /// Runtime equivalent of `#[cfg(since_api = "4.x")]`. + /// + /// # Panics + /// On bad input. + pub fn since_api(major_minor: &str) -> bool { + !Self::before_api(major_minor) + } } } } @@ -713,6 +820,18 @@ fn make_class_method_table( pre_init_code: TokenStream::new(), // late-init, depends on class string names fptr_type: quote! { crate::ClassMethodBind }, method_inits: vec![], + lazy_key_type: quote! { crate::lazy_keys::ClassMethodKey }, + lazy_method_init: quote! { + let get_method_bind = crate::interface_fn!(classdb_get_method_bind); + crate::load_class_method( + get_method_bind, + &mut inner.string_cache, + None, + key.class_name, + key.method_name, + key.hash + ) + }, named_accessors: vec![], class_count: 0, method_count: 0, @@ -749,18 +868,33 @@ fn make_class_method_table( #( #class_sname_decls )* }; - make_indexed_method_table(table) + make_method_table(table) } /// 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) + for accessor in accessors { + let AccessorMethod { + name, + index, + lazy_key, + } = accessor; + + let code = if cfg!(feature = "codegen-lazy-fptrs") { + quote! { + #[inline(always)] + pub fn #name(&self) -> #fptr { + self.fptr_by_key(#lazy_key) + } + } + } else { + quote! { + #[inline(always)] + pub fn #name(&self) -> #fptr { + self.fptr_by_index(#index) + } } }; @@ -787,6 +921,18 @@ fn make_builtin_method_table( }, fptr_type: quote! { crate::BuiltinMethodBind }, method_inits: vec![], + lazy_key_type: quote! { crate::lazy_keys::BuiltinMethodKey }, + lazy_method_init: quote! { + let get_builtin_method = crate::interface_fn!(variant_get_ptr_builtin_method); + crate::load_builtin_method( + get_builtin_method, + &mut inner.string_cache, + key.variant_type.sys(), + key.variant_type_str, + key.method_name, + key.hash + ) + }, named_accessors: vec![], class_count: 0, method_count: 0, @@ -802,7 +948,7 @@ fn make_builtin_method_table( table.class_count += 1; } - make_indexed_method_table(table) + make_method_table(table) } fn populate_class_methods( @@ -830,9 +976,20 @@ fn populate_class_methods( // If requested, add a named accessor for this method. if special_cases::is_named_accessor_in_table(class_ty, &method.name) { + let class_name_str = class_ty.godot_ty.as_str(); + let method_name_str = method.name.as_str(); + let hash = method.hash.expect("hash present"); + table.named_accessors.push(AccessorMethod { name: make_class_method_ptr_name(class_ty, method), index, + lazy_key: quote! { + crate::lazy_keys::ClassMethodKey { + class_name: #class_name_str, + method_name: #method_name_str, + hash: #hash, + } + }, }); } } @@ -862,9 +1019,22 @@ fn populate_builtin_methods( // If requested, add a named accessor for this method. if special_cases::is_named_accessor_in_table(&builtin_ty, &method.name) { + let variant_type = &builtin_name.sys_variant_type; + let variant_type_str = &builtin_name.json_builtin_name; + let method_name_str = method.name.as_str(); + let hash = method.hash.expect("hash present"); + table.named_accessors.push(AccessorMethod { name: make_builtin_method_ptr_name(&builtin_ty, method), index, + lazy_key: quote! { + crate::lazy_keys::BuiltinMethodKey { + variant_type: #variant_type, + variant_type_str: #variant_type_str, + method_name: #method_name_str, + hash: #hash, + } + }, }); } } @@ -885,8 +1055,9 @@ fn make_class_method_init( ) }); + // Could reuse lazy key, but less code like this -> faster parsing. quote! { - crate::load_class_method(get_method_bind, string_names, #class_var, #class_name_str, #method_name_str, #hash), + crate::load_class_method(get_method_bind, string_names, Some(#class_var), #class_name_str, #method_name_str, #hash), } } @@ -907,8 +1078,19 @@ fn make_builtin_method_init( ) }); + // Could reuse lazy key, but less code like this -> faster parsing. quote! { - {let _ = #index;crate::load_builtin_method(get_builtin_method, string_names, sys::#variant_type, #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 1565a81d0..d84086650 100644 --- a/godot-codegen/src/class_generator.rs +++ b/godot-codegen/src/class_generator.rs @@ -1055,7 +1055,7 @@ fn make_methods( 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) + make_class_method_definition(method, class_name, api_level, &get_method_table, ctx) }); FnDefinitions::expand(definitions) @@ -1113,7 +1113,7 @@ fn make_special_builtin_methods(class_name: &TyName, _ctx: &Context) -> TokenStr } } -fn make_method_definition( +fn make_class_method_definition( method: &ClassMethod, class_name: &TyName, api_level: &ClassCodegenLevel, @@ -1133,6 +1133,7 @@ fn make_method_definition( } }*/ + let class_name_str = &class_name.godot_ty; let method_name_str = special_cases::maybe_renamed(class_name, &method.name); let receiver = make_receiver( @@ -1153,9 +1154,22 @@ fn make_method_definition( quote! { Some(self.instance_id) } }; + let fptr_access = if cfg!(feature = "codegen-lazy-fptrs") { + let hash = method.hash.expect("hash present for class method"); + quote! { + fptr_by_key(sys::lazy_keys::ClassMethodKey { + class_name: #class_name_str, + method_name: #method_name_str, + hash: #hash, + }) + } + } else { + quote! { fptr_by_index(#table_index) } + }; + let object_ptr = &receiver.ffi_arg; let ptrcall_invocation = quote! { - let method_bind = sys::#get_method_table().fptr_by_index(#table_index); + let method_bind = sys::#get_method_table().#fptr_access; ::out_class_ptrcall::( method_bind, @@ -1167,7 +1181,7 @@ fn make_method_definition( }; let varcall_invocation = quote! { - let method_bind = sys::#get_method_table().fptr_by_index(#table_index); + let method_bind = sys::#get_method_table().#fptr_access; ::out_class_varcall( method_bind, @@ -1215,16 +1229,32 @@ fn make_builtin_method_definition( .as_deref() .map(MethodReturn::from_type_no_meta); - let table_index = ctx.get_table_index(&MethodTableKey::BuiltinMethod { - builtin_ty: builtin_name.clone(), - method_name: method.name.clone(), - }); + let fptr_access = if cfg!(feature = "codegen-lazy-fptrs") { + let variant_type = quote! { sys::VariantType::#builtin_name }; + let variant_type_str = &builtin_name.godot_ty; + let hash = method.hash.expect("hash present for class method"); + + quote! { + fptr_by_key(sys::lazy_keys::BuiltinMethodKey { + variant_type: #variant_type, + variant_type_str: #variant_type_str, + method_name: #method_name_str, + hash: #hash, + }) + } + } else { + let table_index = ctx.get_table_index(&MethodTableKey::BuiltinMethod { + builtin_ty: builtin_name.clone(), + method_name: method.name.clone(), + }); + quote! { fptr_by_index(#table_index) } + }; let receiver = make_receiver(method.is_static, method.is_const, quote! { self.sys_ptr }); let object_ptr = &receiver.ffi_arg; let ptrcall_invocation = quote! { - let method_bind = sys::builtin_method_table().fptr_by_index(#table_index); + let method_bind = sys::builtin_method_table().#fptr_access; ::out_builtin_ptrcall::( method_bind, diff --git a/godot-codegen/src/util.rs b/godot-codegen/src/util.rs index 27af27daa..b7162ec54 100644 --- a/godot-codegen/src/util.rs +++ b/godot-codegen/src/util.rs @@ -81,6 +81,7 @@ impl ClassCodegenLevel { /// Lookup key for indexed method tables. // Could potentially save a lot of string allocations with lifetimes. +// See also crate::lazy_keys. #[derive(Eq, PartialEq, Hash)] pub(crate) enum MethodTableKey { ClassMethod { diff --git a/godot-core/Cargo.toml b/godot-core/Cargo.toml index f5a8650e0..b719e08f6 100644 --- a/godot-core/Cargo.toml +++ b/godot-core/Cargo.toml @@ -9,13 +9,14 @@ categories = ["game-engines", "graphics"] [features] default = [] -trace = ["godot-ffi/trace"] codegen-fmt = ["godot-ffi/codegen-fmt", "godot-codegen/codegen-fmt"] codegen-full = ["godot-codegen/codegen-full"] -double-precision = ["godot-codegen/double-precision"] +codegen-lazy-fptrs = ["godot-ffi/codegen-lazy-fptrs", "godot-codegen/codegen-lazy-fptrs"] custom-godot = ["godot-ffi/custom-godot", "godot-codegen/custom-godot"] +double-precision = ["godot-codegen/double-precision"] experimental-godot-api = ["godot-codegen/experimental-godot-api"] experimental-threads = [] +trace = ["godot-ffi/trace"] [dependencies] godot-ffi = { path = "../godot-ffi" } diff --git a/godot-core/src/builtin/string/string_name.rs b/godot-core/src/builtin/string/string_name.rs index eb16a9fba..444b3c8df 100644 --- a/godot-core/src/builtin/string/string_name.rs +++ b/godot-core/src/builtin/string/string_name.rs @@ -47,6 +47,7 @@ impl StringName { let c_str = std::ffi::CStr::from_bytes_with_nul(latin1_c_str) .unwrap_or_else(|_| panic!("invalid or not nul-terminated CStr: '{latin1_c_str:?}'")); + // SAFETY: latin1_c_str is nul-terminated and remains valid for entire program duration. let result = unsafe { Self::from_string_sys_init(|ptr| { sys::interface_fn!(string_name_new_with_latin1_chars)( diff --git a/godot-core/src/init/mod.rs b/godot-core/src/init/mod.rs index dc415cca8..04bfd4de0 100644 --- a/godot-core/src/init/mod.rs +++ b/godot-core/src/init/mod.rs @@ -8,6 +8,8 @@ use godot_ffi as sys; use std::cell; +pub use sys::GdextBuild; + #[doc(hidden)] // TODO consider body safe despite unsafe function, and explicitly mark unsafe {} locations pub unsafe fn __gdext_load_library( diff --git a/godot-core/src/lib.rs b/godot-core/src/lib.rs index 6cbb7e2a1..017be6aac 100644 --- a/godot-core/src/lib.rs +++ b/godot-core/src/lib.rs @@ -42,6 +42,7 @@ pub mod engine; #[allow(unreachable_code, clippy::unimplemented)] // TODO remove once #153 is implemented mod gen; + #[doc(hidden)] pub mod private { // If someone forgets #[godot_api], this causes a compile error, rather than virtual functions not being called at runtime. diff --git a/godot-ffi/Cargo.toml b/godot-ffi/Cargo.toml index a9b94e745..4eb04920a 100644 --- a/godot-ffi/Cargo.toml +++ b/godot-ffi/Cargo.toml @@ -10,6 +10,7 @@ categories = ["game-engines", "graphics"] [features] custom-godot = ["godot-bindings/custom-godot"] codegen-fmt = ["godot-codegen/codegen-fmt"] +codegen-lazy-fptrs = ["godot-codegen/codegen-lazy-fptrs"] experimental-godot-api = ["godot-codegen/experimental-godot-api"] trace = [] diff --git a/godot-ffi/src/gdextension_plus.rs b/godot-ffi/src/gdextension_plus.rs index 3e54c9ea6..9258f944f 100644 --- a/godot-ffi/src/gdextension_plus.rs +++ b/godot-ffi/src/gdextension_plus.rs @@ -111,3 +111,25 @@ pub fn panic_call_error( // In Godot source: variant.cpp:3043 or core_bind.cpp:2742 panic!("Function call failed: {function_name} -- {reason}."); } + +// ---------------------------------------------------------------------------------------------------------------------------------------------- +// Lazy method table key types +// Could reuse them in normal load functions, but less code when passing separate parameters -> faster parsing. + +#[cfg(feature = "codegen-lazy-fptrs")] +pub mod lazy_keys { + #[derive(Clone, Eq, PartialEq, Hash)] + pub struct ClassMethodKey { + pub class_name: &'static str, + pub method_name: &'static str, + pub hash: i64, + } + + #[derive(Clone, Eq, PartialEq, Hash)] + pub struct BuiltinMethodKey { + pub variant_type: crate::VariantType, + pub variant_type_str: &'static str, + pub method_name: &'static str, + pub hash: i64, + } +} diff --git a/godot-ffi/src/lib.rs b/godot-ffi/src/lib.rs index 6d5a2327a..db9a693b8 100644 --- a/godot-ffi/src/lib.rs +++ b/godot-ffi/src/lib.rs @@ -85,7 +85,7 @@ struct GodotBinding { 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, + builtin_method_table: Option, // late-init utility_function_table: UtilityFunctionTable, runtime_metadata: GdextRuntimeMetadata, config: GdextConfig, @@ -139,9 +139,6 @@ pub unsafe fn initialize( 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."); @@ -149,6 +146,19 @@ pub unsafe fn initialize( godot_version: version, }; + let builtin_method_table = { + #[cfg(feature = "codegen-lazy-fptrs")] + { + None // loaded later + } + #[cfg(not(feature = "codegen-lazy-fptrs"))] + { + let table = BuiltinMethodTable::load(&interface, &mut string_names); + out!("Loaded builtin method table."); + Some(table) + } + }; + drop(string_names); BINDING = Some(GodotBinding { @@ -165,6 +175,14 @@ pub unsafe fn initialize( }); out!("Assigned binding."); + // Lazy case: load afterwards because table's internal StringCache stores &'static references to the interface. + #[cfg(feature = "codegen-lazy-fptrs")] + { + let builtin_method_table = BuiltinMethodTable::load(); + BINDING.as_mut().unwrap().builtin_method_table = Some(builtin_method_table); + out!("Loaded builtin method table (lazily)."); + } + println!( "Initialize GDExtension API for Rust: {}", CStr::from_ptr(version.string) @@ -251,7 +269,10 @@ pub unsafe fn class_editor_api() -> &'static ClassEditorMethodTable { /// 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 + unwrap_ref_unchecked(&BINDING) + .builtin_method_table + .as_ref() + .unwrap_unchecked() } /// # Safety @@ -272,30 +293,53 @@ pub unsafe fn load_class_method_table(api_level: ClassApiLevel) { out!("Load class method table for level '{:?}'...", api_level); let begin = std::time::Instant::now(); + #[cfg(not(feature = "codegen-lazy-fptrs"))] 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, - )); + #[cfg(feature = "codegen-lazy-fptrs")] + { + binding.class_server_method_table = Some(ClassServersMethodTable::load()); + } + #[cfg(not(feature = "codegen-lazy-fptrs"))] + { + 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, - )); + #[cfg(feature = "codegen-lazy-fptrs")] + { + binding.class_scene_method_table = Some(ClassSceneMethodTable::load()); + } + #[cfg(not(feature = "codegen-lazy-fptrs"))] + { + 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, - )); + #[cfg(feature = "codegen-lazy-fptrs")] + { + binding.class_editor_method_table = Some(ClassEditorMethodTable::load()); + } + #[cfg(not(feature = "codegen-lazy-fptrs"))] + { + binding.class_editor_method_table = Some(ClassEditorMethodTable::load( + &binding.interface, + &mut string_names, + )); + } class_count = ClassEditorMethodTable::CLASS_COUNT; method_count = ClassEditorMethodTable::METHOD_COUNT; } diff --git a/godot-ffi/src/string_cache.rs b/godot-ffi/src/string_cache.rs index 7e8834153..dd58a9e65 100644 --- a/godot-ffi/src/string_cache.rs +++ b/godot-ffi/src/string_cache.rs @@ -39,20 +39,21 @@ impl<'a> StringCache<'a> { 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 { + // For Godot 4.0 and 4.1, construct StringName via String + conversion. + #[cfg(before_api = "4.2")] + unsafe { let string_new_with_latin1_chars_and_len = self .interface .string_new_with_latin1_chars_and_len .unwrap_unchecked(); + 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(); // Construct String. string_new_with_latin1_chars_and_len( @@ -69,10 +70,27 @@ impl<'a> StringCache<'a> { // Destroy String. string_destroy(string_type_ptr(string_ptr)); + } - // Return StringName. - sname.assume_init() - }; + // For Godot 4.2+, construct StringName directly from C string. + #[cfg(since_api = "4.2")] + unsafe { + let string_name_new_with_utf8_chars_and_len = self + .interface + .string_name_new_with_utf8_chars_and_len + .unwrap_unchecked(); + + // Construct StringName from string (non-static, we only need them during the cache's lifetime). + // There is no _latin_*() variant that takes length, so we have to use _utf8_*() instead. + string_name_new_with_utf8_chars_and_len( + sname_uninit_ptr(sname_ptr), + key.as_ptr() as *const std::os::raw::c_char, + key.len() as sys::GDExtensionInt, + ); + } + + // Return StringName. + let opaque = unsafe { sname.assume_init() }; let mut opaque_box = Box::new(opaque); let sname_ptr = box_to_sname_ptr(&mut opaque_box); @@ -108,20 +126,30 @@ fn box_to_sname_ptr( opaque_ptr as sys::GDExtensionStringNamePtr } +#[cfg(before_api = "4.2")] unsafe fn string_type_ptr(opaque_ptr: *mut sys::types::OpaqueString) -> sys::GDExtensionTypePtr { ptr::addr_of_mut!(*opaque_ptr) as sys::GDExtensionTypePtr } +#[cfg(before_api = "4.2")] unsafe fn string_uninit_ptr( opaque_ptr: *mut sys::types::OpaqueString, ) -> sys::GDExtensionUninitializedStringPtr { ptr::addr_of_mut!(*opaque_ptr) as sys::GDExtensionUninitializedStringPtr } +#[cfg(since_api = "4.2")] +unsafe fn sname_uninit_ptr( + opaque_ptr: *mut sys::types::OpaqueStringName, +) -> sys::GDExtensionUninitializedStringNamePtr { + ptr::addr_of_mut!(*opaque_ptr) as sys::GDExtensionUninitializedStringNamePtr +} + unsafe fn sname_type_ptr(opaque_ptr: *mut sys::types::OpaqueStringName) -> sys::GDExtensionTypePtr { ptr::addr_of_mut!(*opaque_ptr) as sys::GDExtensionTypePtr } +#[cfg(before_api = "4.2")] unsafe fn sname_uninit_type_ptr( opaque_ptr: *mut sys::types::OpaqueStringName, ) -> sys::GDExtensionUninitializedTypePtr { diff --git a/godot-ffi/src/toolbox.rs b/godot-ffi/src/toolbox.rs index cc794d74c..ab68143ac 100644 --- a/godot-ffi/src/toolbox.rs +++ b/godot-ffi/src/toolbox.rs @@ -225,7 +225,7 @@ pub(crate) unsafe fn unwrap_ref_unchecked_mut(opt: &mut Option) -> &mut T pub(crate) fn load_class_method( get_method_bind: GetClassMethod, string_names: &mut sys::StringCache, - class_sname_ptr: sys::GDExtensionStringNamePtr, + class_sname_ptr: Option, class_name: &'static str, method_name: &'static str, hash: i64, @@ -237,8 +237,10 @@ pub(crate) fn load_class_method( 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 class_sname_ptr = class_sname_ptr.unwrap_or_else(|| string_names.fetch(class_name)); + + // SAFETY: function pointers provided by Godot. We have no way to validate them. let method: ClassMethodBind = unsafe { get_method_bind(class_sname_ptr, method_sname_ptr, hash) }; diff --git a/godot/Cargo.toml b/godot/Cargo.toml index 8b146ae2b..5bf01e05c 100644 --- a/godot/Cargo.toml +++ b/godot/Cargo.toml @@ -13,6 +13,7 @@ custom-godot = ["godot-core/custom-godot"] double-precision = ["godot-core/double-precision"] formatted = ["godot-core/codegen-fmt"] serde = ["godot-core/serde"] +lazy-function-tables = ["godot-core/codegen-lazy-fptrs"] experimental-threads = ["godot-core/experimental-threads"] experimental-godot-api = ["godot-core/experimental-godot-api"] diff --git a/godot/src/lib.rs b/godot/src/lib.rs index 3f08ef92f..75af70883 100644 --- a/godot/src/lib.rs +++ b/godot/src/lib.rs @@ -146,6 +146,13 @@ //! Access to `godot::engine` APIs that Godot marks "experimental". These are under heavy development and may change at any time. //! If you opt in to this feature, expect breaking changes at compile and runtime. //! +//! * **`lazy-function-tables`** +//! +//! Instead of loading all engine function pointers at startup, load them lazily on first use. This reduces startup time and RAM usage, but +//! incurs additional overhead in each FFI call. Also, you lose the guarantee that once the library has booted, all function pointers are +//! truly available. Function calls may thus panic only at runtime, possibly in deeply nested code paths. +//! This feature is not yet thread-safe and can thus not be combined with `experimental-threads`. +//! //! # Public API //! //! Some symbols in the API are not intended for users, however Rust's visibility feature is not strong enough to express that in all cases @@ -168,6 +175,9 @@ pub use godot_core::{builtin, engine, log, obj}; #[doc(hidden)] pub use godot_core::sys; +#[cfg(all(feature = "lazy-function-tables", feature = "experimental-threads"))] +compile_error!("Thread safety for lazy function pointers is not yet implemented."); + pub mod init { pub use godot_core::init::*; diff --git a/itest/rust/src/builtin_tests/containers/dictionary_test.rs b/itest/rust/src/builtin_tests/containers/dictionary_test.rs index acc8ce08c..6d078e9b7 100644 --- a/itest/rust/src/builtin_tests/containers/dictionary_test.rs +++ b/itest/rust/src/builtin_tests/containers/dictionary_test.rs @@ -8,6 +8,7 @@ use std::collections::{HashMap, HashSet}; use godot::builtin::meta::{FromGodot, ToGodot}; use godot::builtin::{dict, varray, Dictionary, Variant}; +use godot::sys::GdextBuild; use crate::framework::{expect_panic, itest}; @@ -354,10 +355,11 @@ fn dictionary_equal() { assert_ne!(dict! {"foo": "bar"}, dict! {"bar": "foo"}); // Changed in https://github.com/godotengine/godot/pull/74588. - #[cfg(before_api = "4.2")] - assert_eq!(dict! {1: f32::NAN}, dict! {1: f32::NAN}); - #[cfg(since_api = "4.2")] - assert_ne!(dict! {1: f32::NAN}, dict! {1: f32::NAN}); + if GdextBuild::before_api("4.2") { + assert_eq!(dict! {1: f32::NAN}, dict! {1: f32::NAN}); + } else { + assert_ne!(dict! {1: f32::NAN}, dict! {1: f32::NAN}); + } } #[itest] diff --git a/itest/rust/src/builtin_tests/containers/rid_test.rs b/itest/rust/src/builtin_tests/containers/rid_test.rs index a0ea5079c..6fad06a6d 100644 --- a/itest/rust/src/builtin_tests/containers/rid_test.rs +++ b/itest/rust/src/builtin_tests/containers/rid_test.rs @@ -4,10 +4,8 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -use std::{collections::HashSet, thread}; - use godot::builtin::inner::InnerRid; -use godot::builtin::{Color, Rid, Vector2}; +use godot::builtin::Rid; use godot::engine::RenderingServer; use crate::framework::{itest, suppress_godot_print}; @@ -38,10 +36,14 @@ fn canvas_set_parent() { } #[itest] +#[cfg(feature = "experimental-threads")] fn multi_thread_test() { + use godot::builtin::{Color, Vector2}; + use std::collections::HashSet; + let threads = (0..10) .map(|_| { - thread::spawn(|| { + std::thread::spawn(|| { let mut server = RenderingServer::singleton(); (0..1000).map(|_| server.canvas_item_create()).collect() })