Skip to content

Commit

Permalink
Merge pull request #991 from godot-rust/feature/compat-virtual-methods
Browse files Browse the repository at this point in the history
Godot FFI: postinit create, compat virtual methods, icon paths
  • Loading branch information
Bromeon authored Jan 1, 2025
2 parents d10e9a0 + 76386c3 commit b619959
Show file tree
Hide file tree
Showing 20 changed files with 421 additions and 148 deletions.
17 changes: 11 additions & 6 deletions godot-codegen/src/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,14 +69,14 @@ impl<'a> Context<'a> {
// Populate derived-to-base relations
if let Some(base) = class.inherits.as_ref() {
let base_name = TyName::from_godot(base);
println!(
"* Add engine class {} <- inherits {}",
class_name.description(),
base_name.description()
);
// println!(
// "* Add engine class {} <- inherits {}",
// class_name.description(),
// base_name.description()
// );
ctx.inheritance_tree.insert(class_name.clone(), base_name);
} else {
println!("* Add engine class {}", class_name.description());
// println!("* Add engine class {}", class_name.description());
}

// Populate notification constants (first, only for classes that declare them themselves).
Expand Down Expand Up @@ -347,6 +347,11 @@ impl InheritanceTree {
assert!(existing.is_none(), "Duplicate inheritance insert");
}

#[allow(unused)] // Currently 4.4 gated, for virtual method hashes.
pub fn direct_base(&self, derived_name: &TyName) -> Option<TyName> {
self.derived_to_base.get(derived_name).cloned()
}

/// Returns all base classes, without the class itself, in order from nearest to furthest (object).
pub fn collect_all_bases(&self, derived_name: &TyName) -> Vec<TyName> {
let mut upgoer = derived_name;
Expand Down
10 changes: 5 additions & 5 deletions godot-codegen/src/generator/functions_common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
use crate::generator::default_parameters;
use crate::models::domain::{ArgPassing, FnParam, FnQualifier, Function, RustTy};
use crate::special_cases;
use crate::util::{lifetime, safe_ident};
use crate::util::lifetime;
use proc_macro2::{Ident, TokenStream};
use quote::{format_ident, quote};

Expand Down Expand Up @@ -155,11 +155,11 @@ pub fn make_function_definition(
make_params_exprs(sig.params().iter(), passing)
};

let rust_function_name_str = sig.name();
let rust_function_name = sig.name_ident();

let (primary_fn_name, default_fn_code, default_structs_code);
if has_default_params {
primary_fn_name = format_ident!("{}_full", safe_ident(rust_function_name_str));
primary_fn_name = format_ident!("{}_full", rust_function_name);

(default_fn_code, default_structs_code) =
default_parameters::make_function_definition_with_defaults(
Expand All @@ -169,7 +169,7 @@ pub fn make_function_definition(
cfg_attributes,
);
} else {
primary_fn_name = safe_ident(rust_function_name_str);
primary_fn_name = rust_function_name.clone();
default_fn_code = TokenStream::new();
default_structs_code = TokenStream::new();
};
Expand Down Expand Up @@ -227,7 +227,7 @@ pub fn make_function_definition(
}
} else {
let try_return_decl = &sig.return_value().call_result_decl();
let try_fn_name = format_ident!("try_{}", rust_function_name_str);
let try_fn_name = format_ident!("try_{}", rust_function_name);

// Note: all varargs functions are non-static, which is why there are some shortcuts in try_*() argument forwarding.
// This can be made more complex if ever necessary.
Expand Down
4 changes: 2 additions & 2 deletions godot-codegen/src/generator/method_tables.rs
Original file line number Diff line number Diff line change
Expand Up @@ -159,8 +159,8 @@ pub fn make_utility_function_table(api: &ExtensionApi) -> TokenStream {
};

for function in api.utility_functions.iter() {
let field = generator::utility_functions::make_utility_function_ptr_name(function);
let fn_name_str = function.name();
let field = generator::utility_functions::make_utility_function_ptr_name(fn_name_str);
let hash = function.hash();

table.method_decls.push(quote! {
Expand Down Expand Up @@ -191,7 +191,7 @@ struct NamedMethodTable {
method_count: usize,
}

#[allow(dead_code)] // for lazy feature
#[allow(dead_code)] // Individual fields would need to be cfg'ed with: feature = "codegen-lazy-fptrs".
struct IndexedMethodTable {
table_name: Ident,
imports: TokenStream,
Expand Down
14 changes: 14 additions & 0 deletions godot-codegen/src/generator/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ pub mod notifications;
pub mod utility_functions;
pub mod virtual_traits;

#[cfg(since_api = "4.4")]
pub mod virtual_hashes;

// ----------------------------------------------------------------------------------------------------------------------------------------------

// Some file generation functions are in specific modules:
Expand All @@ -44,6 +47,8 @@ pub fn generate_sys_module_file(sys_gen_path: &Path, submit_fn: &mut SubmitFn) {
pub mod table_scene_classes;
pub mod table_editor_classes;
pub mod table_utilities;
#[cfg(since_api = "4.4")]
pub mod virtual_hashes;

pub mod central;
pub mod gdextension_interface;
Expand Down Expand Up @@ -77,6 +82,15 @@ pub fn generate_sys_classes_file(
submit_fn(sys_gen_path.join(filename), code);
watch.record(format!("generate_classes_{}_file", api_level.lower()));
}

// From 4.4 onward, generate table that maps all virtual methods to their known hashes.
// This allows Godot to fall back to an older compatibility function if one is not supported.
#[cfg(since_api = "4.4")]
{
let code = virtual_hashes::make_virtual_hashes_file(api, ctx);
submit_fn(sys_gen_path.join("virtual_hashes.rs"), code);
watch.record("generate_virtual_hashes_file");
}
}

pub fn generate_sys_utilities_file(
Expand Down
10 changes: 5 additions & 5 deletions godot-codegen/src/generator/utility_functions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,16 +35,16 @@ pub(crate) fn generate_utilities_file(
submit_fn(gen_path.join("utilities.rs"), tokens);
}

pub(crate) fn make_utility_function_ptr_name(godot_function_name: &str) -> Ident {
util::safe_ident(godot_function_name)
pub(crate) fn make_utility_function_ptr_name(function: &dyn Function) -> Ident {
function.name_ident()
}

pub(crate) fn make_utility_function_definition(function: &UtilityFunction) -> TokenStream {
let function_ident = make_utility_function_ptr_name(function);
let function_name_str = function.name();
let fn_ptr = make_utility_function_ptr_name(function_name_str);

let ptrcall_invocation = quote! {
let utility_fn = sys::utility_function_table().#fn_ptr;
let utility_fn = sys::utility_function_table().#function_ident;

<CallSig as PtrcallSignatureTuple>::out_utility_ptrcall(
utility_fn,
Expand All @@ -54,7 +54,7 @@ pub(crate) fn make_utility_function_definition(function: &UtilityFunction) -> To
};

let varcall_invocation = quote! {
let utility_fn = sys::utility_function_table().#fn_ptr;
let utility_fn = sys::utility_function_table().#function_ident;

<CallSig as VarcallSignatureTuple>::out_utility_ptrcall_varargs(
utility_fn,
Expand Down
61 changes: 61 additions & 0 deletions godot-codegen/src/generator/virtual_hashes.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/*
* Copyright (c) godot-rust; Bromeon and contributors.
* 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::context::Context;
use crate::models::domain::{Class, ClassLike, ExtensionApi, FnDirection, Function};
use proc_macro2::TokenStream;
use quote::quote;

pub fn make_virtual_hashes_file(api: &ExtensionApi, ctx: &mut Context) -> TokenStream {
make_virtual_hashes_for_all_classes(&api.classes, ctx)
}

fn make_virtual_hashes_for_all_classes(all_classes: &[Class], ctx: &mut Context) -> TokenStream {
let modules = all_classes
.iter()
.map(|class| make_virtual_hashes_for_class(class, ctx));

quote! {
#![allow(non_snake_case, non_upper_case_globals, unused_imports)]

#( #modules )*
}
}

fn make_virtual_hashes_for_class(class: &Class, ctx: &mut Context) -> TokenStream {
let class_name = class.name();

// Import all base class hashes via `use` statements.
let use_base_class = if let Some(base_class) = ctx.inheritance_tree().direct_base(class_name) {
quote! {
pub use super::#base_class::*;
}
} else {
TokenStream::new()
};

let constants = class.methods.iter().filter_map(|method| {
let FnDirection::Virtual { hash } = method.direction() else {
return None;
};

let method_name = method.name_ident();
let constant = quote! {
pub const #method_name: u32 = #hash;
};

Some(constant)
});

// Even if there are no virtual methods, we need to generate the module, to enable base class imports via `use`.
quote! {
pub mod #class_name {
#use_base_class
#( #constants )*
}
}
}
17 changes: 13 additions & 4 deletions godot-codegen/src/models/domain.rs
Original file line number Diff line number Diff line change
Expand Up @@ -291,9 +291,14 @@ pub trait Function: fmt::Display {
fn surrounding_class(&self) -> Option<&TyName>;

// Default:
/// Rust name as string slice.
fn name(&self) -> &str {
&self.common().name
}
/// Rust name as `Ident`. Might be cached in future.
fn name_ident(&self) -> Ident {
safe_ident(self.name())
}
fn godot_name(&self) -> &str {
&self.common().godot_name
}
Expand All @@ -310,7 +315,7 @@ pub trait Function: fmt::Display {
self.common().is_private
}
fn is_virtual(&self) -> bool {
matches!(self.direction(), FnDirection::Virtual)
matches!(self.direction(), FnDirection::Virtual { .. })
}
fn direction(&self) -> FnDirection {
self.common().direction
Expand All @@ -330,7 +335,7 @@ pub struct UtilityFunction {
impl UtilityFunction {
pub fn hash(&self) -> i64 {
match self.direction() {
FnDirection::Virtual => unreachable!("utility function cannot be virtual"),
FnDirection::Virtual { .. } => unreachable!("utility function cannot be virtual"),
FnDirection::Outbound { hash } => hash,
}
}
Expand Down Expand Up @@ -370,7 +375,7 @@ pub struct BuiltinMethod {
impl BuiltinMethod {
pub fn hash(&self) -> i64 {
match self.direction() {
FnDirection::Virtual => unreachable!("builtin method cannot be virtual"),
FnDirection::Virtual { .. } => unreachable!("builtin method cannot be virtual"),
FnDirection::Outbound { hash } => hash,
}
}
Expand Down Expand Up @@ -441,7 +446,11 @@ impl fmt::Display for ClassMethod {
#[derive(Copy, Clone, Debug)]
pub enum FnDirection {
/// Godot -> Rust.
Virtual,
Virtual {
// Since PR https://github.com/godotengine/godot/pull/100674, virtual methods have a compat hash, too.
#[cfg(since_api = "4.4")]
hash: u32,
},

/// Rust -> Godot.
Outbound { hash: i64 },
Expand Down
36 changes: 25 additions & 11 deletions godot-codegen/src/models/domain_mapping.rs
Original file line number Diff line number Diff line change
Expand Up @@ -419,20 +419,34 @@ impl ClassMethod {
ctx: &mut Context,
) -> Option<Self> {
assert!(method.is_virtual);
assert!(
method.hash.is_none(),
"hash present for virtual class method"
);

// Hash for virtual methods is available from Godot 4.4, see https://github.com/godotengine/godot/pull/100674.
let direction = FnDirection::Virtual {
#[cfg(since_api = "4.4")]
hash: {
// TODO(v0.3,virtual-compat): re-enable this in favor of 0 fallback.
/*let hash_i64 = method.hash.unwrap_or_else(|| {
panic!(
"virtual class methods must have a hash since Godot 4.4; missing: {}.{}",
class_name.godot_ty, method.name
)
});*/
// Temporarily use hash 0 until upstream PR is merged.
let hash_i64 = method.hash.unwrap_or(0);

// TODO see if we can use u32 everywhere.
hash_i64.try_into().unwrap_or_else(|_| {
panic!(
"virtual method {}.{} has hash {} that is out of range for u32",
class_name.godot_ty, method.name, hash_i64
)
})
},
};

let rust_method_name = Self::make_virtual_method_name(class_name, &method.name);

Self::from_json_inner(
method,
rust_method_name,
class_name,
FnDirection::Virtual,
ctx,
)
Self::from_json_inner(method, rust_method_name, class_name, direction, ctx)
}

fn from_json_inner(
Expand Down
6 changes: 1 addition & 5 deletions godot-core/src/obj/bounds.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,6 @@

use crate::obj::cap::GodotDefault;
use crate::obj::{Bounds, Gd, GodotClass, RawGd};
use crate::registry::callbacks;
use crate::storage::{InstanceCache, Storage};
use crate::{out, sys};
use private::Sealed;
Expand Down Expand Up @@ -418,10 +417,7 @@ impl Declarer for DeclUser {
where
T: GodotDefault + Bounds<Declarer = Self>,
{
unsafe {
let object_ptr = callbacks::create::<T>(std::ptr::null_mut());
Gd::from_obj_sys(object_ptr)
}
Gd::default_instance()
}
}

Expand Down
5 changes: 5 additions & 0 deletions godot-core/src/obj/gd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -454,7 +454,12 @@ impl<T: GodotClass> Gd<T> {
T: cap::GodotDefault,
{
unsafe {
// Default value (and compat one) for `p_notify_postinitialize` is true in Godot.
#[cfg(since_api = "4.4")]
let object_ptr = callbacks::create::<T>(std::ptr::null_mut(), sys::conv::SYS_TRUE);
#[cfg(before_api = "4.4")]
let object_ptr = callbacks::create::<T>(std::ptr::null_mut());

Gd::from_obj_sys(object_ptr)
}
}
Expand Down
10 changes: 8 additions & 2 deletions godot-core/src/obj/traits.rs
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,10 @@ pub trait UserClass: Bounds<Declarer = bounds::DeclUser> {
fn __before_ready(&mut self);

#[doc(hidden)]
fn __default_virtual_call(_method_name: &str) -> sys::GDExtensionClassCallVirtual {
fn __default_virtual_call(
_method_name: &str,
#[cfg(since_api = "4.4")] _hash: u32,
) -> sys::GDExtensionClassCallVirtual {
None
}
}
Expand Down Expand Up @@ -586,6 +589,9 @@ pub mod cap {
/// Auto-implemented for `#[godot_api] impl XyVirtual for MyClass` blocks
pub trait ImplementsGodotVirtual: GodotClass {
#[doc(hidden)]
fn __virtual_call(_name: &str) -> sys::GDExtensionClassCallVirtual;
fn __virtual_call(
name: &str,
#[cfg(since_api = "4.4")] hash: u32,
) -> sys::GDExtensionClassCallVirtual;
}
}
Loading

0 comments on commit b619959

Please sign in to comment.