Skip to content

Commit

Permalink
Integrate hash into __[default_]virtual_call callbacks
Browse files Browse the repository at this point in the history
Use `I*` trait base class to lookup virtual hashes.
  • Loading branch information
Bromeon committed Dec 26, 2024
1 parent 3ab4024 commit 85723ff
Show file tree
Hide file tree
Showing 7 changed files with 143 additions and 52 deletions.
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;
}
}
30 changes: 28 additions & 2 deletions godot-core/src/registry/callbacks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -109,10 +109,23 @@ pub unsafe extern "C" fn free<T: GodotClass>(
crate::storage::destroy_storage::<T>(instance);
}

#[cfg(since_api = "4.4")]
pub unsafe extern "C" fn get_virtual<T: cap::ImplementsGodotVirtual>(
_class_user_data: *mut std::ffi::c_void,
name: sys::GDExtensionConstStringNamePtr,
hash: u32,
) -> sys::GDExtensionClassCallVirtual {
// This string is not ours, so we cannot call the destructor on it.
let borrowed_string = StringName::borrow_string_sys(name);
let method_name = borrowed_string.to_string();

T::__virtual_call(method_name.as_str(), hash)
}

#[cfg(before_api = "4.4")]
pub unsafe extern "C" fn get_virtual<T: cap::ImplementsGodotVirtual>(
_class_user_data: *mut std::ffi::c_void,
name: sys::GDExtensionConstStringNamePtr,
#[cfg(since_api = "4.4")] _hash: u32,
) -> sys::GDExtensionClassCallVirtual {
// This string is not ours, so we cannot call the destructor on it.
let borrowed_string = StringName::borrow_string_sys(name);
Expand All @@ -121,10 +134,23 @@ pub unsafe extern "C" fn get_virtual<T: cap::ImplementsGodotVirtual>(
T::__virtual_call(method_name.as_str())
}

#[cfg(since_api = "4.4")]
pub unsafe extern "C" fn default_get_virtual<T: UserClass>(
_class_user_data: *mut std::ffi::c_void,
name: sys::GDExtensionConstStringNamePtr,
hash: u32,
) -> sys::GDExtensionClassCallVirtual {
// This string is not ours, so we cannot call the destructor on it.
let borrowed_string = StringName::borrow_string_sys(name);
let method_name = borrowed_string.to_string();

T::__default_virtual_call(method_name.as_str(), hash)
}

#[cfg(before_api = "4.4")]
pub unsafe extern "C" fn default_get_virtual<T: UserClass>(
_class_user_data: *mut std::ffi::c_void,
name: sys::GDExtensionConstStringNamePtr,
#[cfg(since_api = "4.4")] _hash: u32,
) -> sys::GDExtensionClassCallVirtual {
// This string is not ours, so we cannot call the destructor on it.
let borrowed_string = StringName::borrow_string_sys(name);
Expand Down
3 changes: 2 additions & 1 deletion godot-ffi/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,8 @@ pub use gen::table_editor_classes::*;
pub use gen::table_scene_classes::*;
pub use gen::table_servers_classes::*;
pub use gen::table_utilities::*;
pub use gen::virtual_hashes;
#[cfg(since_api = "4.4")]
pub use gen::virtual_hashes as known_virtual_hashes;

// Other
pub use extras::*;
Expand Down
5 changes: 3 additions & 2 deletions godot-macros/src/class/data_models/func.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,12 @@ pub struct FuncDefinition {
// Virtual methods are non-static by their nature; so there's no support for static ones.
pub fn make_virtual_callback(
class_name: &Ident,
signature_info: SignatureInfo,
signature_info: &SignatureInfo,
before_kind: BeforeKind,
) -> TokenStream {
let method_name = &signature_info.method_name;

let wrapped_method = make_forwarding_closure(class_name, &signature_info, before_kind);
let wrapped_method = make_forwarding_closure(class_name, signature_info, before_kind);
let sig_tuple = signature_info.tuple_type();

let call_ctx = make_call_context(
Expand Down Expand Up @@ -192,6 +192,7 @@ impl SignatureInfo {
}
}

#[derive(Copy, Clone)]
pub enum BeforeKind {
/// Default: just call the method.
Without,
Expand Down
110 changes: 76 additions & 34 deletions godot-macros/src/class/data_models/interface_trait_impl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,13 @@
use crate::class::{into_signature_info, make_virtual_callback, BeforeKind, SignatureInfo};
use crate::{util, ParseResult};

use proc_macro2::TokenStream;
use proc_macro2::{Ident, TokenStream};
use quote::quote;

/// Codegen for `#[godot_api] impl ISomething for MyType`
pub fn transform_trait_impl(original_impl: venial::Impl) -> ParseResult<TokenStream> {
let (class_name, trait_path) = util::validate_trait_impl_virtual(&original_impl, "godot_api")?;
let (class_name, trait_path, trait_base_class) =
util::validate_trait_impl_virtual(&original_impl, "godot_api")?;
let class_name_obj = util::class_name_obj(&class_name);

let mut godot_init_impl = TokenStream::new();
Expand All @@ -37,9 +38,7 @@ pub fn transform_trait_impl(original_impl: venial::Impl) -> ParseResult<TokenStr
let mut property_get_revert_fn = None;
let mut property_can_revert_fn = None;

let mut virtual_methods = vec![];
let mut virtual_method_cfg_attrs = vec![];
let mut virtual_method_names = vec![];
let mut overridden_virtuals = vec![];

let prv = quote! { ::godot::private };

Expand All @@ -61,9 +60,8 @@ pub fn transform_trait_impl(original_impl: venial::Impl) -> ParseResult<TokenStr
.into_iter()
.collect::<Vec<_>>();

let method_name = method.name.to_string();

match method_name.as_str() {
let method_name_str = method.name.to_string();
match method_name_str.as_str() {
"register_class" => {
// Implements the trait once for each implementation of this method, forwarding the cfg attrs of each
// implementation to the generated trait impl. If the cfg attrs allow for multiple implementations of
Expand Down Expand Up @@ -289,59 +287,70 @@ pub fn transform_trait_impl(original_impl: venial::Impl) -> ParseResult<TokenStr
}

// Other virtual methods, like ready, process etc.
_ => {
method_name_str => {
let method_name_ident = method.name.clone();
let method = util::reduce_to_signature(method);

// Godot-facing name begins with underscore.
//
// godot-codegen special-cases the virtual method called _init (which exists on a handful of classes, distinct from the default
// constructor) to init_ext, to avoid Rust-side ambiguity. See godot_codegen::class_generator::virtual_method_name.
let virtual_method_name = if method_name == "init_ext" {
let virtual_method_name = if method_name_str == "init_ext" {
String::from("_init")
} else {
format!("_{method_name}")
format!("_{method_name_str}")
};

let signature_info = into_signature_info(method, &class_name, false);

// Overridden ready() methods additionally have an additional `__before_ready()` call (for OnReady inits).
let before_kind = if method_name == "ready" {
let before_kind = if method_name_str == "ready" {
BeforeKind::WithBefore
} else {
BeforeKind::Without
};

let hash_if_cond = if cfg!(since_api = "4.4") {
// If ever the `I*` verbatim validation is relaxed (it won't work with use-renames or other weird edge cases), the approach
// with known_virtual_hashes module could be changed to something like the following (GodotBase = nearest Godot base class):
// __get_virtual_hash::<Self::GodotBase>("method")
Some(quote! {
if hash == ::godot::sys::known_virtual_hashes::#trait_base_class::#method_name_ident
})
} else {
None
};

// Note that, if the same method is implemented multiple times (with different cfg attr combinations),
// then there will be multiple match arms annotated with the same cfg attr combinations, thus they will
// be reduced to just one arm (at most, if the implementations aren't all removed from compilation) for
// each distinct method.
virtual_method_cfg_attrs.push(cfg_attrs);
virtual_method_names.push(virtual_method_name);
virtual_methods.push((signature_info, before_kind));
overridden_virtuals.push(OverriddenVirtualFn {
cfg_attrs,
method_name: virtual_method_name,
hash_if_cond,
signature_info,
before_kind,
});
}
}
}

// If there is no ready() method explicitly overridden, we need to add one, to ensure that __before_ready() is called to
// initialize the OnReady fields.
if !virtual_methods
.iter()
.any(|(sig, _)| sig.method_name == "ready")
{
let signature_info = SignatureInfo::fn_ready();
if !overridden_virtuals.iter().any(|v| v.method_name == "ready") {
let match_arm = OverriddenVirtualFn {
cfg_attrs: vec![],
method_name: "_ready".to_string(),
hash_if_cond: None, // TODO?
signature_info: SignatureInfo::fn_ready(),
before_kind: BeforeKind::OnlyBefore,
};

virtual_method_cfg_attrs.push(vec![]);
virtual_method_names.push("_ready".to_string());
virtual_methods.push((signature_info, BeforeKind::OnlyBefore));
overridden_virtuals.push(match_arm);
}

let tool_check = util::make_virtual_tool_check();
let virtual_method_callbacks: Vec<TokenStream> = virtual_methods
.into_iter()
.map(|(signature_info, before_kind)| {
make_virtual_callback(&class_name, signature_info, before_kind)
})
.collect();

// Use 'match' as a way to only emit 'Some(...)' if the given cfg attrs allow.
// This permits users to conditionally remove virtual method impls from compilation while also removing their FFI
Expand All @@ -360,6 +369,17 @@ pub fn transform_trait_impl(original_impl: venial::Impl) -> ParseResult<TokenStr
let property_get_revert_fn = convert_to_match_expression_or_none(property_get_revert_fn);
let property_can_revert_fn = convert_to_match_expression_or_none(property_can_revert_fn);

// See also __default_virtual_call() codegen.
let hash_param = if cfg!(since_api = "4.4") {
quote! { hash: u32, }
} else {
TokenStream::new()
};

let virtual_match_arms = overridden_virtuals
.iter()
.map(|v| v.make_match_arm(&class_name));

let result = quote! {
#original_impl
#godot_init_impl
Expand All @@ -374,16 +394,13 @@ pub fn transform_trait_impl(original_impl: venial::Impl) -> ParseResult<TokenStr
impl ::godot::private::You_forgot_the_attribute__godot_api for #class_name {}

impl ::godot::obj::cap::ImplementsGodotVirtual for #class_name {
fn __virtual_call(name: &str, hash: u32) -> ::godot::sys::GDExtensionClassCallVirtual {
fn __virtual_call(name: &str, #hash_param) -> ::godot::sys::GDExtensionClassCallVirtual {
//println!("virtual_call: {}.{}", std::any::type_name::<Self>(), name);
use ::godot::obj::UserClass as _;
#tool_check

match name {
#(
#(#virtual_method_cfg_attrs)*
#virtual_method_names => #virtual_method_callbacks,
)*
#( #virtual_match_arms )*
_ => None,
}
}
Expand Down Expand Up @@ -413,6 +430,31 @@ pub fn transform_trait_impl(original_impl: venial::Impl) -> ParseResult<TokenStr
Ok(result)
}

struct OverriddenVirtualFn<'a> {
cfg_attrs: Vec<&'a venial::Attribute>,
method_name: String,
hash_if_cond: Option<TokenStream>,
signature_info: SignatureInfo,
before_kind: BeforeKind,
}

impl OverriddenVirtualFn<'_> {
fn make_match_arm(&self, class_name: &Ident) -> TokenStream {
let cfg_attrs = self.cfg_attrs.iter();
let method_name = self.method_name.as_str();
let hash_if_cond = &self.hash_if_cond;

// Lazily generate code for the actual work (calling user function).
let method_callback =
make_virtual_callback(class_name, &self.signature_info, self.before_kind);

quote! {
#(#cfg_attrs)*
#method_name #hash_if_cond => #method_callback,
}
}
}

/// Expects either Some(quote! { () => A, () => B, ... }) or None as the 'tokens' parameter.
/// The idea is that the () => ... arms can be annotated by cfg attrs, so, if any of them compiles (and assuming the cfg
/// attrs only allow one arm to 'survive' compilation), their return value (Some(...)) will be prioritized over the
Expand Down
20 changes: 17 additions & 3 deletions godot-macros/src/class/derive_godot_class.rs
Original file line number Diff line number Diff line change
Expand Up @@ -308,13 +308,27 @@ fn make_user_class_impl(
let tool_check = util::make_virtual_tool_check();
let signature_info = SignatureInfo::fn_ready();

let callback = make_virtual_callback(class_name, signature_info, BeforeKind::OnlyBefore);
let callback = make_virtual_callback(class_name, &signature_info, BeforeKind::OnlyBefore);

// See also __virtual_call() codegen.
let (hash_param, hash_check);
if cfg!(since_api = "4.4") {
hash_param = quote! { hash: u32, };
hash_check = quote! { && hash == ::godot::sys::known_virtual_hashes::Node::ready };
} else {
hash_param = TokenStream::new();
hash_check = TokenStream::new();
}

let default_virtual_fn = quote! {
fn __default_virtual_call(name: &str) -> ::godot::sys::GDExtensionClassCallVirtual {
fn __default_virtual_call(
name: &str,
#hash_param
) -> ::godot::sys::GDExtensionClassCallVirtual {
use ::godot::obj::UserClass as _;
#tool_check

if name == "_ready" {
if name == "_ready" #hash_check {
#callback
} else {
None
Expand Down
17 changes: 9 additions & 8 deletions godot-macros/src/util/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -166,30 +166,31 @@ pub(crate) fn validate_impl(
validate_self(original_impl, attr)
}

/// Validates that the declaration is the of the form `impl Trait for SomeType`, where the name
/// of `Trait` begins with `I`.
/// Validates that the declaration is the of the form `impl Trait for SomeType`, where the name of `Trait` begins with `I`.
///
/// Returns `(class_name, trait_path, trait_base_class)`, e.g. `(MyClass, godot::prelude::INode3D, Node3D)`.
pub(crate) fn validate_trait_impl_virtual<'a>(
original_impl: &'a venial::Impl,
attr: &str,
) -> ParseResult<(Ident, &'a venial::TypeExpr)> {
) -> ParseResult<(Ident, &'a venial::TypeExpr, Ident)> {
let trait_name = original_impl.trait_ty.as_ref().unwrap(); // unwrap: already checked outside
let typename = extract_typename(trait_name);

// Validate trait
if !typename
let Some(base_class) = typename
.as_ref()
.map_or(false, |seg| seg.ident.to_string().starts_with('I'))
{
.and_then(|seg| seg.ident.to_string().strip_prefix('I').map(ident))
else {
return bail!(
original_impl,
"#[{attr}] for trait impls requires a virtual method trait (trait name should start with 'I')",
);
}
};

// Validate self
validate_self(original_impl, attr).map(|class_name| {
// let trait_name = typename.unwrap(); // unwrap: already checked in 'Validate trait'
(class_name, trait_name)
(class_name, trait_name, base_class)
})
}

Expand Down

0 comments on commit 85723ff

Please sign in to comment.