Skip to content

Commit 0c91b8a

Browse files
authored
Merge pull request #883 from godot-rust/bugfix/virtual-methods-nullable
Virtual methods take `Option<Gd<T>>` (unless whitelisted)
2 parents 32fb7f3 + 18391df commit 0c91b8a

File tree

6 files changed

+160
-45
lines changed

6 files changed

+160
-45
lines changed

Cargo.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,8 @@ members = [
1515
"examples/dodge-the-creeps/rust",
1616
"examples/hot-reload/rust",
1717
]
18+
19+
# Note about Jetbrains IDEs: "IDE Sync" (Refresh Cargo projects) may cause static analysis errors such as
20+
# "at most one `api-*` feature can be enabled". This is because by default, all Cargo features are enabled,
21+
# which isn't a setup we support. To address this, individual features can be enabled in the various
22+
# Cargo.toml files: https://www.jetbrains.com/help/rust/rust-cfg-support.html#enable-disable-feature-manually.

godot-codegen/src/generator/default_parameters.rs

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
*/
77

88
use crate::generator::functions_common;
9-
use crate::generator::functions_common::FnCode;
9+
use crate::generator::functions_common::{FnCode, FnParamTokens};
1010
use crate::models::domain::{FnParam, FnQualifier, Function, RustTy, TyName};
1111
use crate::util::{ident, safe_ident};
1212
use crate::{conv, special_cases};
@@ -49,11 +49,15 @@ pub fn make_function_definition_with_defaults(
4949
let receiver_param = &code.receiver.param;
5050
let receiver_self = &code.receiver.self_prefix;
5151

52-
let [required_params_impl_asarg, _, _] =
53-
functions_common::make_params_exprs(required_fn_params.iter().cloned(), false, true, true);
52+
let FnParamTokens {
53+
params: required_params_impl_asarg,
54+
..
55+
} = functions_common::make_params_exprs(required_fn_params.iter().cloned(), true, true);
5456

55-
let [_, _, required_args_internal] =
56-
functions_common::make_params_exprs(required_fn_params.into_iter(), false, false, false);
57+
let FnParamTokens {
58+
arg_names: required_args_internal,
59+
..
60+
} = functions_common::make_params_exprs(required_fn_params.into_iter(), false, false);
5761

5862
let return_decl = &sig.return_value().decl;
5963

godot-codegen/src/generator/functions_common.rs

Lines changed: 80 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,9 @@
77

88
use crate::generator::default_parameters;
99
use crate::models::domain::{FnParam, FnQualifier, Function, RustTy};
10+
use crate::special_cases;
1011
use crate::util::safe_ident;
11-
use proc_macro2::TokenStream;
12+
use proc_macro2::{Ident, TokenStream};
1213
use quote::{format_ident, quote};
1314

1415
pub struct FnReceiver {
@@ -83,6 +84,22 @@ impl FnDefinitions {
8384
}
8485
}
8586

87+
// Gathers multiple token vectors related to function parameters.
88+
#[derive(Default)]
89+
pub struct FnParamTokens {
90+
pub params: Vec<TokenStream>,
91+
pub param_types: Vec<TokenStream>,
92+
pub arg_names: Vec<TokenStream>,
93+
}
94+
95+
impl FnParamTokens {
96+
pub fn push_regular(&mut self, param_name: &Ident, param_ty: &RustTy) {
97+
self.params.push(quote! { #param_name: #param_ty });
98+
self.arg_names.push(quote! { #param_name });
99+
self.param_types.push(quote! { #param_ty });
100+
}
101+
}
102+
86103
pub fn make_function_definition(
87104
sig: &dyn Function,
88105
code: &FnCode,
@@ -119,12 +136,19 @@ pub fn make_function_definition(
119136
(TokenStream::new(), TokenStream::new())
120137
};
121138

122-
let [params, param_types, arg_names] = make_params_exprs(
123-
sig.params().iter(),
124-
sig.is_virtual(),
125-
!has_default_params, // For *_full function, we don't need impl AsObjectArg<T> parameters
126-
!has_default_params, // or arg.as_object_arg() calls.
127-
);
139+
let FnParamTokens {
140+
params,
141+
param_types,
142+
arg_names,
143+
} = if sig.is_virtual() {
144+
make_params_exprs_virtual(sig.params().iter(), sig)
145+
} else {
146+
make_params_exprs(
147+
sig.params().iter(),
148+
!has_default_params, // For *_full function, we don't need impl AsObjectArg<T> parameters
149+
!has_default_params, // or arg.as_object_arg() calls.
150+
)
151+
};
128152

129153
let rust_function_name_str = sig.name();
130154

@@ -200,9 +224,11 @@ pub fn make_function_definition(
200224
// This can be made more complex if ever necessary.
201225

202226
// A function() may call try_function(), its arguments should not have .as_object_arg().
203-
let [_, _, arg_names_without_asarg] = make_params_exprs(
227+
let FnParamTokens {
228+
arg_names: arg_names_without_asarg,
229+
..
230+
} = make_params_exprs(
204231
sig.params().iter(),
205-
false,
206232
!has_default_params, // For *_full function, we don't need impl AsObjectArg<T> parameters
207233
false, // or arg.as_object_arg() calls.
208234
);
@@ -307,54 +333,81 @@ pub fn make_vis(is_private: bool) -> TokenStream {
307333
// ----------------------------------------------------------------------------------------------------------------------------------------------
308334
// Implementation
309335

310-
// Method could possibly be split -- only one invocation uses all 3 return values, the rest uses only index [0] or [2].
336+
/// For non-virtual functions, returns the parameter declarations, type tokens, and names.
311337
pub(crate) fn make_params_exprs<'a>(
312338
method_args: impl Iterator<Item = &'a FnParam>,
313-
is_virtual: bool,
314339
param_is_impl_asarg: bool,
315340
arg_is_asarg: bool,
316-
) -> [Vec<TokenStream>; 3] {
317-
let mut params = vec![];
318-
let mut param_types = vec![]; // or non-generic params
319-
let mut arg_names = vec![];
341+
) -> FnParamTokens {
342+
let mut ret = FnParamTokens::default();
320343

321344
for param in method_args {
322345
let param_name = &param.name;
323346
let param_ty = &param.type_;
324347

325-
// Objects (Gd<T>) use implicit conversions via AsObjectArg. Only use in non-virtual functions.
326348
match &param.type_ {
349+
// Non-virtual functions: Objects (Gd<T>) use implicit conversions via AsObjectArg.
327350
RustTy::EngineClass {
328351
object_arg,
329352
impl_as_object_arg,
330353
..
331-
} if !is_virtual => {
354+
} => {
332355
// Parameter declarations in signature: impl AsObjectArg<T>
333356
if param_is_impl_asarg {
334-
params.push(quote! { #param_name: #impl_as_object_arg });
357+
ret.params.push(quote! { #param_name: #impl_as_object_arg });
335358
} else {
336-
params.push(quote! { #param_name: #object_arg });
359+
ret.params.push(quote! { #param_name: #object_arg });
337360
}
338361

339362
// Argument names in function body: arg.as_object_arg() vs. arg
340363
if arg_is_asarg {
341-
arg_names.push(quote! { #param_name.as_object_arg() });
364+
ret.arg_names.push(quote! { #param_name.as_object_arg() });
342365
} else {
343-
arg_names.push(quote! { #param_name });
366+
ret.arg_names.push(quote! { #param_name });
344367
}
345368

346-
param_types.push(quote! { #object_arg });
369+
ret.param_types.push(quote! { #object_arg });
347370
}
348371

349-
_ => {
350-
params.push(quote! { #param_name: #param_ty });
351-
arg_names.push(quote! { #param_name });
352-
param_types.push(quote! { #param_ty });
372+
// All other methods and parameter types: standard handling.
373+
_ => ret.push_regular(param_name, param_ty),
374+
}
375+
}
376+
377+
ret
378+
}
379+
380+
/// For virtual functions, returns the parameter declarations, type tokens, and names.
381+
pub(crate) fn make_params_exprs_virtual<'a>(
382+
method_args: impl Iterator<Item = &'a FnParam>,
383+
function_sig: &dyn Function,
384+
) -> FnParamTokens {
385+
let mut ret = FnParamTokens::default();
386+
387+
for param in method_args {
388+
let param_name = &param.name;
389+
let param_ty = &param.type_;
390+
391+
match &param.type_ {
392+
// Virtual methods accept Option<Gd<T>>, since we don't know whether objects are nullable or required.
393+
RustTy::EngineClass { .. }
394+
if !special_cases::is_class_method_param_required(
395+
function_sig.surrounding_class().unwrap(),
396+
function_sig.name(),
397+
param_name,
398+
) =>
399+
{
400+
ret.params.push(quote! { #param_name: Option<#param_ty> });
401+
ret.arg_names.push(quote! { #param_name });
402+
ret.param_types.push(quote! { #param_ty });
353403
}
404+
405+
// All other methods and parameter types: standard handling.
406+
_ => ret.push_regular(param_name, param_ty),
354407
}
355408
}
356409

357-
[params, param_types, arg_names]
410+
ret
358411
}
359412

360413
fn function_uses_pointers(sig: &dyn Function) -> bool {

godot-codegen/src/special_cases/special_cases.rs

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ use crate::models::domain::{Enum, RustTy, TyName};
2828
use crate::models::json::{JsonBuiltinMethod, JsonClassMethod, JsonUtilityFunction};
2929
use crate::special_cases::codegen_special_cases;
3030
use crate::Context;
31+
use proc_macro2::Ident;
3132
// Deliberately private -- all checks must go through `special_cases`.
3233

3334
#[rustfmt::skip]
@@ -269,6 +270,44 @@ pub fn is_class_method_const(class_name: &TyName, godot_method: &JsonClassMethod
269270
}
270271
}
271272

273+
/// Currently only for virtual methods; checks if the specified parameter is required (non-null) and can be declared as `Gd<T>`
274+
/// instead of `Option<Gd<T>>`.
275+
pub fn is_class_method_param_required(
276+
class_name: &TyName,
277+
method_name: &str,
278+
param: &Ident, // Don't use `&str` to avoid to_string() allocations for each check on call-site.
279+
) -> bool {
280+
// Note: magically, it's enough if a base class method is declared here; it will be picked up by derived classes.
281+
282+
match (class_name.godot_ty.as_str(), method_name) {
283+
// Nodes.
284+
("Node", "input") => true,
285+
("Node", "shortcut_input") => true,
286+
("Node", "unhandled_input") => true,
287+
("Node", "unhandled_key_input") => true,
288+
289+
// https://docs.godotengine.org/en/stable/classes/class_collisionobject2d.html#class-collisionobject2d-private-method-input-event
290+
("CollisionObject2D", "input_event") => true, // both parameters.
291+
292+
// UI.
293+
("Control", "gui_input") => true,
294+
295+
// Script instances.
296+
("ScriptExtension", "instance_create") => param == "for_object",
297+
("ScriptExtension", "placeholder_instance_create") => param == "for_object",
298+
("ScriptExtension", "inherits_script") => param == "script",
299+
("ScriptExtension", "instance_has") => param == "object",
300+
301+
// Editor.
302+
("EditorExportPlugin", "customize_resource") => param == "resource",
303+
("EditorExportPlugin", "customize_scene") => param == "scene",
304+
305+
("EditorPlugin", "handles") => param == "object",
306+
307+
_ => false,
308+
}
309+
}
310+
272311
/// True if builtin method is excluded. Does NOT check for type exclusion; use [`is_builtin_type_deleted`] for that.
273312
pub fn is_builtin_method_deleted(_class_name: &TyName, method: &JsonBuiltinMethod) -> bool {
274313
// Currently only deleted if codegen.

itest/rust/src/object_tests/object_test.rs

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1057,11 +1057,3 @@ fn double_use_reference() {
10571057
double_use.free();
10581058
emitter.free();
10591059
}
1060-
1061-
// ----------------------------------------------------------------------------------------------------------------------------------------------
1062-
1063-
// There isn't a good way to test editor plugins, but we can at least declare one to ensure that the macro
1064-
// compiles.
1065-
#[derive(GodotClass)]
1066-
#[class(no_init, base = EditorPlugin, editor_plugin, tool)]
1067-
struct CustomEditorPlugin;

itest/rust/src/object_tests/virtual_methods_test.rs

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,9 @@ use godot::classes::resource_loader::CacheMode;
1919
#[cfg(feature = "codegen-full")]
2020
use godot::classes::Material;
2121
use godot::classes::{
22-
BoxMesh, INode, INode2D, IPrimitiveMesh, IRefCounted, IResourceFormatLoader, IRigidBody2D,
23-
InputEvent, InputEventAction, Node, Node2D, PrimitiveMesh, RefCounted, ResourceFormatLoader,
24-
ResourceLoader, Viewport, Window,
22+
BoxMesh, IEditorPlugin, INode, INode2D, IPrimitiveMesh, IRefCounted, IResourceFormatLoader,
23+
IRigidBody2D, InputEvent, InputEventAction, Node, Node2D, Object, PrimitiveMesh, RefCounted,
24+
ResourceFormatLoader, ResourceLoader, Viewport, Window,
2525
};
2626
use godot::meta::ToGodot;
2727
use godot::obj::{Base, Gd, NewAlloc, NewGd};
@@ -153,12 +153,12 @@ impl IPrimitiveMesh for VirtualReturnTest {
153153
fn surface_get_format(&self, _index: i32) -> u32 { unreachable!() }
154154
fn surface_get_primitive_type(&self, _index: i32) -> u32 { unreachable!() }
155155
#[cfg(feature = "codegen-full")]
156-
fn surface_set_material(&mut self, _index: i32, _material: Gd<Material>) { unreachable!() }
156+
fn surface_set_material(&mut self, _index: i32, _material: Option<Gd<Material>>) { unreachable!() }
157157
#[cfg(feature = "codegen-full")]
158158
fn surface_get_material(&self, _index: i32) -> Option<Gd<Material>> { unreachable!() }
159159
fn get_blend_shape_count(&self) -> i32 { unreachable!() }
160160
fn get_blend_shape_name(&self, _index: i32) -> StringName { unreachable!() }
161-
fn set_blend_shape_name(&mut self, _index: i32, _namee: StringName) { unreachable!() }
161+
fn set_blend_shape_name(&mut self, _index: i32, _name: StringName) { unreachable!() }
162162
fn get_aabb(&self) -> godot::prelude::Aabb { unreachable!() }
163163
}
164164

@@ -773,3 +773,25 @@ impl GetSetTest {
773773
self.always_get_100
774774
}
775775
}
776+
777+
// ----------------------------------------------------------------------------------------------------------------------------------------------
778+
779+
// There isn't a good way to test editor plugins, but we can at least declare one to ensure that the macro
780+
// compiles.
781+
#[derive(GodotClass)]
782+
#[class(no_init, base = EditorPlugin, editor_plugin, tool)]
783+
struct CustomEditorPlugin;
784+
785+
// Just override EditorPlugin::edit() to verify method is declared with Option<T>.
786+
// See https://github.com/godot-rust/gdext/issues/494.
787+
#[godot_api]
788+
impl IEditorPlugin for CustomEditorPlugin {
789+
fn edit(&mut self, _object: Option<Gd<Object>>) {
790+
// Do nothing.
791+
}
792+
793+
// This parameter is non-null.
794+
fn handles(&self, _object: Gd<Object>) -> bool {
795+
true
796+
}
797+
}

0 commit comments

Comments
 (0)