Skip to content

Commit 4d2251b

Browse files
authored
Merge pull request #1136 from godot-rust/qol/virtuals-no-longer-required
Godot virtuals are no longer required if a derived Godot class implements them
2 parents adcb58d + 3a1c918 commit 4d2251b

File tree

4 files changed

+142
-41
lines changed

4 files changed

+142
-41
lines changed

godot-codegen/src/generator/virtual_traits.rs

Lines changed: 78 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,13 @@
88
use crate::generator::functions_common::FnCode;
99
use crate::generator::{docs, functions_common};
1010
use crate::models::domain::{
11-
ApiView, Class, ClassLike, ClassMethod, FnQualifier, Function, TyName,
11+
ApiView, Class, ClassLike, ClassMethod, FnQualifier, Function, TyName, VirtualMethodPresence,
1212
};
13+
use crate::special_cases;
1314
use crate::util::ident;
1415
use proc_macro2::{Ident, TokenStream};
1516
use quote::quote;
17+
use std::fmt::Write;
1618

1719
pub fn make_virtual_methods_trait(
1820
class: &Class,
@@ -25,19 +27,20 @@ pub fn make_virtual_methods_trait(
2527
let trait_name = ident(trait_name_str);
2628
let class_name = &class.name().rust_ty;
2729

28-
let virtual_method_fns = make_all_virtual_methods(class, all_base_names, view);
30+
let (virtual_method_fns, extra_docs) = make_all_virtual_methods(class, all_base_names, view);
2931
let special_virtual_methods = make_special_virtual_methods(notification_enum_name);
3032

3133
let trait_doc = docs::make_virtual_trait_doc(trait_name_str, class.name());
3234

3335
quote! {
3436
#[doc = #trait_doc]
37+
#[doc = #extra_docs]
3538
#[allow(unused_variables)]
3639
#[allow(clippy::unimplemented)]
3740
#cfg_attributes
3841
pub trait #trait_name: crate::obj::GodotClass<Base = #class_name> + crate::private::You_forgot_the_attribute__godot_api {
3942
#special_virtual_methods
40-
#( #virtual_method_fns )*
43+
#virtual_method_fns
4144
}
4245
}
4346
}
@@ -150,11 +153,22 @@ fn make_special_virtual_methods(notification_enum_name: &Ident) -> TokenStream {
150153
}
151154
}
152155

153-
fn make_virtual_method(method: &ClassMethod) -> Option<TokenStream> {
156+
fn make_virtual_method(
157+
method: &ClassMethod,
158+
presence: VirtualMethodPresence,
159+
) -> Option<TokenStream> {
154160
if !method.is_virtual() {
155161
return None;
156162
}
157163

164+
// Possibly change behavior of required/optional-ness of the virtual method in derived classes.
165+
// It's also possible that it's removed, which would not declare it at all in the `I*` trait.
166+
let is_virtual_required = match presence {
167+
VirtualMethodPresence::Inherit => method.is_virtual_required(),
168+
VirtualMethodPresence::Override { is_required } => is_required,
169+
VirtualMethodPresence::Remove => return None,
170+
};
171+
158172
// Virtual methods are never static.
159173
let qualifier = method.qualifier();
160174
assert!(matches!(qualifier, FnQualifier::Mut | FnQualifier::Const));
@@ -166,7 +180,7 @@ fn make_virtual_method(method: &ClassMethod) -> Option<TokenStream> {
166180
// make_return() requests following args, but they are not used for virtual methods. We can provide empty streams.
167181
varcall_invocation: TokenStream::new(),
168182
ptrcall_invocation: TokenStream::new(),
169-
is_virtual_required: method.is_virtual_required(),
183+
is_virtual_required,
170184
is_varcall_fallible: true,
171185
},
172186
None,
@@ -181,24 +195,76 @@ fn make_all_virtual_methods(
181195
class: &Class,
182196
all_base_names: &[TyName],
183197
view: &ApiView,
184-
) -> Vec<TokenStream> {
185-
let mut all_tokens = vec![];
198+
) -> (TokenStream, String) {
199+
let mut all_tokens = TokenStream::new();
186200

187201
for method in class.methods.iter() {
188202
// Assumes that inner function filters on is_virtual.
189-
if let Some(tokens) = make_virtual_method(method) {
190-
all_tokens.push(tokens);
203+
if let Some(tokens) = make_virtual_method(method, VirtualMethodPresence::Inherit) {
204+
all_tokens.extend(tokens);
191205
}
192206
}
193207

208+
let mut changes_from_base = String::new();
209+
194210
for base_name in all_base_names {
195211
let base_class = view.get_engine_class(base_name);
196212
for method in base_class.methods.iter() {
197-
if let Some(tokens) = make_virtual_method(method) {
198-
all_tokens.push(tokens);
213+
// Certain derived classes in Godot implement a virtual method declared in a base class, thus no longer
214+
// making it required. This isn't advertised in the extension_api, but instead manually tracked via special cases.
215+
let derived_presence =
216+
special_cases::get_derived_virtual_method_presence(class.name(), method.name());
217+
218+
// Collect all changes in a Markdown table.
219+
let new = match derived_presence {
220+
VirtualMethodPresence::Inherit => None,
221+
VirtualMethodPresence::Override { is_required } => {
222+
Some(format_required(is_required))
223+
}
224+
VirtualMethodPresence::Remove => Some("removed"),
225+
};
226+
if let Some(new) = new {
227+
let orig = format_required(method.is_virtual_required());
228+
let base_name = &base_name.rust_ty;
229+
let method_name = format_method_name(method);
230+
231+
write!(
232+
changes_from_base,
233+
"\n| [`I{base_name}::{method_name}`](crate::classes::I{base_name}::{method_name}) | {orig} | {new} |"
234+
)
235+
.unwrap();
236+
}
237+
238+
if let Some(tokens) = make_virtual_method(method, derived_presence) {
239+
all_tokens.extend(tokens);
199240
}
200241
}
201242
}
202243

203-
all_tokens
244+
let extra_docs = if changes_from_base.is_empty() {
245+
String::new()
246+
} else {
247+
let class_name = &class.name().rust_ty;
248+
format!(
249+
"\n\n# Changes from base interface traits\n\
250+
The following virtual methods originally declared in direct/indirect base classes have their presence (required/optional) changed \
251+
in `I{class_name}`. This can happen if Godot already overrides virtual methods, discouraging the user from further overriding them.\n\
252+
\n\n| Base method | Original | New |\n| --- | --- | --- |{changes_from_base}"
253+
)
254+
};
255+
256+
(all_tokens, extra_docs)
257+
}
258+
259+
fn format_required(is_required: bool) -> &'static str {
260+
if is_required {
261+
"required"
262+
} else {
263+
"optional"
264+
}
265+
}
266+
267+
fn format_method_name(method: &ClassMethod) -> &str {
268+
// TODO when we have `_unsafe` or similar postfix with raw pointers, update this here.
269+
method.name()
204270
}

godot-codegen/src/models/domain.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -723,6 +723,20 @@ pub enum ArgPassing {
723723

724724
// ----------------------------------------------------------------------------------------------------------------------------------------------
725725

726+
/// Behavior of virtual methods in derived classes.
727+
pub enum VirtualMethodPresence {
728+
/// Preserve default behavior of base class (required or optional).
729+
Inherit,
730+
731+
/// Virtual method is now required/optional according to `is_required`, independent of base method declaration.
732+
Override { is_required: bool },
733+
734+
/// Virtual method is removed in derived classes (no longer appearing in their interface trait).
735+
Remove,
736+
}
737+
738+
// ----------------------------------------------------------------------------------------------------------------------------------------------
739+
726740
/// Contains multiple naming conventions for types (classes, builtin classes, enums).
727741
#[derive(Clone, Eq, PartialEq, Hash)]
728742
pub struct TyName {

godot-codegen/src/models/domain_mapping.rs

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -487,20 +487,26 @@ impl ClassMethod {
487487

488488
// Since Godot 4.4, GDExtension advertises whether virtual methods have a default implementation or are required to be overridden.
489489
#[cfg(before_api = "4.4")]
490-
let is_virtual_required = special_cases::is_virtual_method_required(
491-
&class_name.rust_ty.to_string(),
492-
rust_method_name,
493-
);
490+
let is_virtual_required =
491+
special_cases::is_virtual_method_required(&class_name, rust_method_name);
494492

495493
#[cfg(since_api = "4.4")]
496-
let is_virtual_required = method.is_virtual
497-
&& method.is_required.unwrap_or_else(|| {
494+
#[allow(clippy::let_and_return)]
495+
let is_virtual_required = method.is_virtual && {
496+
// Evaluate this always first (before potential manual overrides), to detect mistakes in spec.
497+
let is_required_in_json = method.is_required.unwrap_or_else(|| {
498498
panic!(
499499
"virtual method {}::{} lacks field `is_required`",
500500
class_name.rust_ty, rust_method_name
501501
);
502502
});
503503

504+
// Potential special cases come here. The situation "virtual function is required in base class, but not in derived"
505+
// is not handled here, but in virtual_traits.rs. Here, virtual methods appear only once, in their base.
506+
507+
is_required_in_json
508+
};
509+
504510
Some(Self {
505511
common: FunctionCommon {
506512
name: rust_method_name.to_string(),

godot-codegen/src/special_cases/special_cases.rs

Lines changed: 38 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
#![allow(clippy::match_like_matches_macro)] // if there is only one rule
2727

2828
use crate::conv::to_enum_type_uncached;
29-
use crate::models::domain::{Enum, RustTy, TyName};
29+
use crate::models::domain::{Enum, RustTy, TyName, VirtualMethodPresence};
3030
use crate::models::json::{JsonBuiltinMethod, JsonClassMethod, JsonSignal, JsonUtilityFunction};
3131
use crate::special_cases::codegen_special_cases;
3232
use crate::util::option_as_slice;
@@ -659,9 +659,11 @@ pub fn get_interface_extra_docs(trait_name: &str) -> Option<&'static str> {
659659
}
660660

661661
#[cfg(before_api = "4.4")]
662-
pub fn is_virtual_method_required(class_name: &str, method: &str) -> bool {
663-
match (class_name, method) {
664-
("ScriptLanguageExtension", _) => method != "get_doc_comment_delimiters",
662+
pub fn is_virtual_method_required(class_name: &TyName, rust_method_name: &str) -> bool {
663+
// Do not call is_derived_virtual_method_required() here; that is handled in virtual_traits.rs.
664+
665+
match (class_name.godot_ty.as_str(), rust_method_name) {
666+
("ScriptLanguageExtension", method) => method != "get_doc_comment_delimiters",
665667

666668
("ScriptExtension", "editor_can_reload_from_file")
667669
| ("ScriptExtension", "can_instantiate")
@@ -699,7 +701,7 @@ pub fn is_virtual_method_required(class_name: &str, method: &str) -> bool {
699701
| ("EditorExportPlugin", "customize_scene")
700702
| ("EditorExportPlugin", "get_customization_configuration_hash")
701703
| ("EditorExportPlugin", "get_name")
702-
| ("EditorVcsInterface", _)
704+
| ("EditorVCSInterface", _)
703705
| ("MovieWriter", _)
704706
| ("TextServerExtension", "has_feature")
705707
| ("TextServerExtension", "get_name")
@@ -805,23 +807,23 @@ pub fn is_virtual_method_required(class_name: &str, method: &str) -> bool {
805807
| ("PacketPeerExtension", "get_available_packet_count")
806808
| ("PacketPeerExtension", "get_max_packet_size")
807809
| ("StreamPeerExtension", "get_available_bytes")
808-
| ("WebRtcDataChannelExtension", "poll")
809-
| ("WebRtcDataChannelExtension", "close")
810-
| ("WebRtcDataChannelExtension", "set_write_mode")
811-
| ("WebRtcDataChannelExtension", "get_write_mode")
812-
| ("WebRtcDataChannelExtension", "was_string_packet")
813-
| ("WebRtcDataChannelExtension", "get_ready_state")
814-
| ("WebRtcDataChannelExtension", "get_label")
815-
| ("WebRtcDataChannelExtension", "is_ordered")
816-
| ("WebRtcDataChannelExtension", "get_id")
817-
| ("WebRtcDataChannelExtension", "get_max_packet_life_time")
818-
| ("WebRtcDataChannelExtension", "get_max_retransmits")
819-
| ("WebRtcDataChannelExtension", "get_protocol")
820-
| ("WebRtcDataChannelExtension", "is_negotiated")
821-
| ("WebRtcDataChannelExtension", "get_buffered_amount")
822-
| ("WebRtcDataChannelExtension", "get_available_packet_count")
823-
| ("WebRtcDataChannelExtension", "get_max_packet_size")
824-
| ("WebRtcPeerConnectionExtension", _)
810+
| ("WebRTCDataChannelExtension", "poll")
811+
| ("WebRTCDataChannelExtension", "close")
812+
| ("WebRTCDataChannelExtension", "set_write_mode")
813+
| ("WebRTCDataChannelExtension", "get_write_mode")
814+
| ("WebRTCDataChannelExtension", "was_string_packet")
815+
| ("WebRTCDataChannelExtension", "get_ready_state")
816+
| ("WebRTCDataChannelExtension", "get_label")
817+
| ("WebRTCDataChannelExtension", "is_ordered")
818+
| ("WebRTCDataChannelExtension", "get_id")
819+
| ("WebRTCDataChannelExtension", "get_max_packet_life_time")
820+
| ("WebRTCDataChannelExtension", "get_max_retransmits")
821+
| ("WebRTCDataChannelExtension", "get_protocol")
822+
| ("WebRTCDataChannelExtension", "is_negotiated")
823+
| ("WebRTCDataChannelExtension", "get_buffered_amount")
824+
| ("WebRTCDataChannelExtension", "get_available_packet_count")
825+
| ("WebRTCDataChannelExtension", "get_max_packet_size")
826+
| ("WebRTCPeerConnectionExtension", _)
825827
| ("MultiplayerPeerExtension", "get_available_packet_count")
826828
| ("MultiplayerPeerExtension", "get_max_packet_size")
827829
| ("MultiplayerPeerExtension", "set_transfer_channel")
@@ -839,7 +841,20 @@ pub fn is_virtual_method_required(class_name: &str, method: &str) -> bool {
839841
| ("MultiplayerPeerExtension", "get_unique_id")
840842
| ("MultiplayerPeerExtension", "get_connection_status") => true,
841843

842-
(_, _) => false,
844+
_ => false,
845+
}
846+
}
847+
848+
// Adjustments for Godot 4.4+, where a virtual method is no longer needed (e.g. in a derived class).
849+
#[rustfmt::skip]
850+
pub fn get_derived_virtual_method_presence(class_name: &TyName, rust_method_name: &str) -> VirtualMethodPresence {
851+
match (class_name.godot_ty.as_str(), rust_method_name) {
852+
// Required in base class, no longer in derived; https://github.com/godot-rust/gdext/issues/1133.
853+
| ("AudioStreamPlaybackResampled", "mix")
854+
=> VirtualMethodPresence::Remove,
855+
856+
// Default: inherit presence from base class.
857+
_ => VirtualMethodPresence::Inherit,
843858
}
844859
}
845860

0 commit comments

Comments
 (0)