Skip to content

Commit 309881c

Browse files
authored
Merge pull request #1396 from godot-rust/feature/func-default-params
Default parameters via `#[opt(default = ...)]` syntax
2 parents 53043c9 + 7405d51 commit 309881c

File tree

10 files changed

+327
-44
lines changed

10 files changed

+327
-44
lines changed

godot-core/src/meta/error/call_error.rs

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -121,16 +121,18 @@ impl CallError {
121121
/// Checks whether number of arguments matches the number of parameters.
122122
pub(crate) fn check_arg_count(
123123
call_ctx: &CallContext,
124-
arg_count: usize,
125-
param_count: usize,
124+
arg_count: usize, // Arguments passed by the caller.
125+
default_value_count: usize, // Fallback/default values, *not* arguments.
126+
param_count: usize, // Parameters declared by the function.
126127
) -> Result<(), Self> {
127-
// This will need to be adjusted once optional parameters are supported in #[func].
128-
if arg_count == param_count {
128+
// Valid if both:
129+
// - Provided args + available defaults (fallbacks) are enough to fill all parameters.
130+
// - Provided args don't exceed parameter count.
131+
if arg_count + default_value_count >= param_count && arg_count <= param_count {
129132
return Ok(());
130133
}
131134

132135
let call_error = Self::failed_param_count(call_ctx, arg_count, param_count);
133-
134136
Err(call_error)
135137
}
136138

@@ -180,7 +182,7 @@ impl CallError {
180182
/// Returns an error for a failed parameter conversion.
181183
pub(crate) fn failed_param_conversion<P>(
182184
call_ctx: &CallContext,
183-
param_index: isize,
185+
param_index: usize,
184186
convert_error: ConvertError,
185187
) -> Self {
186188
let param_ty = std::any::type_name::<P>();

godot-core/src/meta/method_info.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ pub struct MethodInfo {
2424
pub class_name: ClassId,
2525
pub return_type: PropertyInfo,
2626
pub arguments: Vec<PropertyInfo>,
27+
/// Whether default arguments are real "arguments" is controversial. From the function PoV they are, but for the caller,
28+
/// they are just pre-set values to fill in for missing arguments.
2729
pub default_arguments: Vec<Variant>,
2830
pub flags: MethodFlags,
2931
}

godot-core/src/meta/param_tuple.rs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,15 +42,18 @@ pub trait ParamTuple: Sized {
4242
/// As an example, this would be used for user-defined functions that will be called from Godot, however this is _not_ used when
4343
/// calling a Godot function from Rust code.
4444
pub trait InParamTuple: ParamTuple {
45-
/// Converts `args_ptr` to `Self` by first going through [`Variant`].
45+
/// Converts `args_ptr` to `Self`, merging with default values if needed.
4646
///
4747
/// # Safety
4848
///
49-
/// - `args_ptr` must be a pointer to an array of length [`Self::LEN`](ParamTuple::LEN)
49+
/// - `args_ptr` must be a pointer to an array of length `arg_count`
5050
/// - Each element of `args_ptr` must be reborrowable as a `&Variant` with a lifetime that lasts for the duration of the call.
51-
#[doc(hidden)] // Hidden since v0.3.2.
51+
/// - `arg_count + default_values.len()` must equal `Self::LEN`
52+
#[doc(hidden)]
5253
unsafe fn from_varcall_args(
5354
args_ptr: *const sys::GDExtensionConstVariantPtr,
55+
arg_count: usize,
56+
default_values: &[Variant],
5457
call_ctx: &CallContext,
5558
) -> CallResult<Self>;
5659

godot-core/src/meta/param_tuple/impls.rs

Lines changed: 34 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -56,20 +56,40 @@ macro_rules! unsafe_impl_param_tuple {
5656
impl<$($P),*> InParamTuple for ($($P,)*) where $($P: FromGodot + fmt::Debug),* {
5757
unsafe fn from_varcall_args(
5858
args_ptr: *const sys::GDExtensionConstVariantPtr,
59+
arg_count: usize,
60+
default_values: &[Variant],
5961
call_ctx: &crate::meta::CallContext,
6062
) -> CallResult<Self> {
61-
let args = (
62-
$(
63-
// SAFETY: `args_ptr` is an array with length `Self::LEN` and each element is a valid pointer, since they
64-
// are all reborrowable as references.
65-
unsafe { *args_ptr.offset($n) },
66-
)*
67-
);
63+
// Fast path: all args provided, no defaults needed (zero allocations).
64+
if arg_count == Self::LEN {
65+
let param_tuple = (
66+
$(
67+
unsafe { varcall_arg::<$P>(*args_ptr.add($n), call_ctx, $n)? },
68+
)*
69+
);
70+
return Ok(param_tuple);
71+
}
72+
73+
// Slow path: merge provided args with defaults (requires allocation).
74+
let mut all_args = Vec::with_capacity(Self::LEN);
75+
76+
// Copy all provided args.
77+
for i in 0..arg_count {
78+
all_args.push(unsafe { *args_ptr.add(i) });
79+
}
80+
81+
// Fill remaining parameters with default values.
82+
let required_param_count = Self::LEN - default_values.len();
83+
let first_missing_index = arg_count - required_param_count;
84+
for i in first_missing_index..default_values.len() {
85+
all_args.push(default_values[i].var_sys());
86+
}
6887

88+
// Convert all args to the tuple.
6989
let param_tuple = (
7090
$(
71-
// SAFETY: Each pointer in `args_ptr` is reborrowable as a `&Variant` for the duration of this call.
72-
unsafe { varcall_arg::<$P>(args.$n, call_ctx, $n)? },
91+
// SAFETY: Each pointer in `args_ptr` is borrowable as a &Variant for the duration of this call.
92+
unsafe { varcall_arg::<$P>(all_args[$n], call_ctx, $n)? },
7393
)*
7494
);
7595

@@ -191,16 +211,16 @@ unsafe_impl_param_tuple!((p0, 0): P0, (p1, 1): P1, (p2, 2): P2, (p3, 3): P3, (p4
191211
/// Convert the `N`th argument of `args_ptr` into a value of type `P`.
192212
///
193213
/// # Safety
194-
/// - It must be safe to dereference the address at `args_ptr.offset(N)`.
195-
/// - The pointer at `args_ptr.offset(N)` must follow the safety requirements as laid out in
214+
/// - It must be safe to dereference the address at `args_ptr.add(N)`.
215+
/// - The pointer at `args_ptr.add(N)` must follow the safety requirements as laid out in
196216
/// [`GodotFfi::from_arg_ptr`].
197-
pub(super) unsafe fn ptrcall_arg<P: FromGodot, const N: isize>(
217+
pub(super) unsafe fn ptrcall_arg<P: FromGodot, const N: usize>(
198218
args_ptr: *const sys::GDExtensionConstTypePtr,
199219
call_ctx: &CallContext,
200220
call_type: sys::PtrcallType,
201221
) -> CallResult<P> {
202222
// SAFETY: It is safe to dereference `args_ptr` at `N`.
203-
let offset_ptr = unsafe { *args_ptr.offset(N) };
223+
let offset_ptr = unsafe { *args_ptr.add(N) };
204224

205225
// SAFETY: The pointer follows the safety requirements from `GodotFfi::from_arg_ptr`.
206226
let ffi = unsafe {
@@ -220,7 +240,7 @@ pub(super) unsafe fn ptrcall_arg<P: FromGodot, const N: isize>(
220240
pub(super) unsafe fn varcall_arg<P: FromGodot>(
221241
arg: sys::GDExtensionConstVariantPtr,
222242
call_ctx: &CallContext,
223-
param_index: isize,
243+
param_index: usize,
224244
) -> CallResult<P> {
225245
// SAFETY: It is safe to dereference `args_ptr` at `N` as a `Variant`.
226246
let variant_ref = unsafe { Variant::borrow_var_sys(arg) };

godot-core/src/meta/signature.rs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,23 +63,27 @@ where
6363
/// # Safety
6464
/// A call to this function must be caused by Godot making a varcall with parameters `Params` and return type `Ret`.
6565
#[inline]
66+
#[allow(clippy::too_many_arguments)]
6667
pub unsafe fn in_varcall(
6768
instance_ptr: sys::GDExtensionClassInstancePtr,
6869
call_ctx: &CallContext,
6970
args_ptr: *const sys::GDExtensionConstVariantPtr,
7071
arg_count: i64,
72+
default_values: &[Variant],
7173
ret: sys::GDExtensionVariantPtr,
7274
err: *mut sys::GDExtensionCallError,
7375
func: unsafe fn(sys::GDExtensionClassInstancePtr, Params) -> Ret,
7476
) -> CallResult<()> {
7577
//$crate::out!("in_varcall: {call_ctx}");
76-
CallError::check_arg_count(call_ctx, arg_count as usize, Params::LEN)?;
78+
let arg_count = arg_count as usize;
79+
CallError::check_arg_count(call_ctx, arg_count, default_values.len(), Params::LEN)?;
7780

7881
#[cfg(feature = "trace")]
7982
trace::push(true, false, call_ctx);
8083

8184
// SAFETY: TODO.
82-
let args = unsafe { Params::from_varcall_args(args_ptr, call_ctx)? };
85+
let args =
86+
unsafe { Params::from_varcall_args(args_ptr, arg_count, default_values, call_ctx)? };
8387

8488
let rust_result = unsafe { func(instance_ptr, args) };
8589
// SAFETY: TODO.

godot-core/src/registry/method.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ pub struct ClassMethodInfo {
3434
method_flags: MethodFlags,
3535
return_value: Option<MethodParamOrReturnInfo>,
3636
arguments: Vec<MethodParamOrReturnInfo>,
37+
/// Whether default arguments are real "arguments" is controversial. From the function PoV they are, but for the caller,
38+
/// they are just pre-set values to fill in for missing arguments.
3739
default_arguments: Vec<Variant>,
3840
}
3941

@@ -59,12 +61,11 @@ impl ClassMethodInfo {
5961
ptrcall_func: sys::GDExtensionClassMethodPtrCall,
6062
method_flags: MethodFlags,
6163
param_names: &[&str],
62-
// default_arguments: Vec<Variant>, - not yet implemented
64+
default_arguments: Vec<Variant>,
6365
) -> Self {
6466
let return_value = Ret::Via::return_info();
6567
let arguments = Signature::<Params, Ret>::param_names(param_names);
6668

67-
let default_arguments = vec![]; // not yet implemented.
6869
assert!(
6970
default_arguments.len() <= arguments.len(),
7071
"cannot have more default arguments than arguments"

godot-macros/src/class/data_models/func.rs

Lines changed: 86 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -120,12 +120,24 @@ pub fn make_method_registration(
120120
interface_trait,
121121
);
122122

123+
let default_parameters = make_default_argument_vec(
124+
&signature_info.optional_param_default_exprs,
125+
&signature_info.param_types,
126+
)?;
127+
123128
// String literals
124129
let class_name_str = class_name.to_string();
125130
let method_name_str = func_definition.godot_name();
126131

127132
let call_ctx = make_call_context(&class_name_str, &method_name_str);
128-
let varcall_fn_decl = make_varcall_fn(&call_ctx, &forwarding_closure);
133+
134+
// Both varcall and ptrcall functions are always generated and registered, even when default parameters are present via #[opt].
135+
// Key differences are:
136+
// - varcall: handles default parameters, applying them when caller provides fewer arguments.
137+
// - ptrcall: optimized path without default handling, can be used when caller provides all arguments.
138+
//
139+
// Godot decides at call-time which calling convention to use based on available type information.
140+
let varcall_fn_decl = make_varcall_fn(&call_ctx, &forwarding_closure, &default_parameters);
129141
let ptrcall_fn_decl = make_ptrcall_fn(&call_ctx, &forwarding_closure);
130142

131143
// String literals II
@@ -166,6 +178,7 @@ pub fn make_method_registration(
166178
&[
167179
#( #param_ident_strs ),*
168180
],
181+
#default_parameters,
169182
)
170183
};
171184

@@ -183,6 +196,48 @@ pub fn make_method_registration(
183196
Ok(registration)
184197
}
185198

199+
/// Generates code to create a `Vec<Variant>` containing default argument values for varcall. Allocates on every call.
200+
fn make_default_argument_vec(
201+
optional_param_default_exprs: &[TokenStream],
202+
all_params: &[venial::TypeExpr],
203+
) -> ParseResult<TokenStream> {
204+
// Optional params appearing at the end has already been validated in validate_default_exprs().
205+
206+
// Early exit: all parameters are required, not optional. This check is not necessary for correctness.
207+
if optional_param_default_exprs.is_empty() {
208+
return Ok(quote! { vec![] });
209+
}
210+
211+
let optional_param_types = all_params
212+
.iter()
213+
.skip(all_params.len() - optional_param_default_exprs.len());
214+
215+
let default_parameters = optional_param_default_exprs
216+
.iter()
217+
.zip(optional_param_types)
218+
.map(|(value, param_type)| {
219+
quote! {
220+
::godot::builtin::Variant::from(
221+
::godot::meta::AsArg::<#param_type>::into_arg(#value)
222+
)
223+
}
224+
});
225+
226+
// Performance: This generates `vec![...]` in the varcall FFI function, which allocates on *every* call when default parameters
227+
// are present. This is a performance cost we accept for now.
228+
//
229+
// If no #[opt] attributes are used, this generates `vec![]` which does *not* allocate, so most #[func] functions are unaffected.
230+
//
231+
// Potential future improvements:
232+
// - Use `Global<Vec<Variant>>` (or LazyLock/thread_local) to allocate once per function instead of per call.
233+
// - Store defaults in MethodInfo during registration and retrieve via method_data pointer.
234+
//
235+
// Note also that there may be a semantic difference on reusing the same object vs. recreating it, see Python's default-param issue.
236+
Ok(quote! {
237+
vec![ #(#default_parameters),* ]
238+
})
239+
}
240+
186241
// ----------------------------------------------------------------------------------------------------------------------------------------------
187242
// Implementation
188243

@@ -208,6 +263,9 @@ pub struct SignatureInfo {
208263
///
209264
/// Index points into original venial tokens (i.e. takes into account potential receiver params).
210265
pub modified_param_types: Vec<(usize, venial::TypeExpr)>,
266+
267+
/// Default value expressions `EXPR` from `#[opt(default = EXPR)]`, for all optional parameters.
268+
pub optional_param_default_exprs: Vec<TokenStream>,
211269
}
212270

213271
impl SignatureInfo {
@@ -220,6 +278,7 @@ impl SignatureInfo {
220278
param_types: vec![],
221279
return_type: quote! { () },
222280
modified_param_types: vec![],
281+
optional_param_default_exprs: vec![],
223282
}
224283
}
225284

@@ -413,7 +472,7 @@ pub(crate) fn into_signature_info(
413472
let params_span = signature.span();
414473
let mut param_idents = Vec::with_capacity(num_params);
415474
let mut param_types = Vec::with_capacity(num_params);
416-
let ret_type = match signature.return_ty {
475+
let return_type = match signature.return_ty {
417476
None => quote! { () },
418477
Some(ty) => map_self_to_class_name(ty.tokens, class_name),
419478
};
@@ -469,8 +528,9 @@ pub(crate) fn into_signature_info(
469528
params_span,
470529
param_idents,
471530
param_types,
472-
return_type: ret_type,
531+
return_type,
473532
modified_param_types,
533+
optional_param_default_exprs: vec![], // Assigned outside, if relevant.
474534
}
475535
}
476536

@@ -552,8 +612,12 @@ fn make_method_flags(
552612
}
553613

554614
/// Generate code for a C FFI function that performs a varcall.
555-
fn make_varcall_fn(call_ctx: &TokenStream, wrapped_method: &TokenStream) -> TokenStream {
556-
let invocation = make_varcall_invocation(wrapped_method);
615+
fn make_varcall_fn(
616+
call_ctx: &TokenStream,
617+
wrapped_method: &TokenStream,
618+
default_parameters: &TokenStream,
619+
) -> TokenStream {
620+
let invocation = make_varcall_invocation(wrapped_method, default_parameters);
557621

558622
// TODO reduce amount of code generated, by delegating work to a library function. Could even be one that produces this function pointer.
559623
quote! {
@@ -616,17 +680,24 @@ fn make_ptrcall_invocation(wrapped_method: &TokenStream, is_virtual: bool) -> To
616680
}
617681

618682
/// Generate code for a `varcall()` call expression.
619-
fn make_varcall_invocation(wrapped_method: &TokenStream) -> TokenStream {
683+
fn make_varcall_invocation(
684+
wrapped_method: &TokenStream,
685+
default_parameters: &TokenStream,
686+
) -> TokenStream {
620687
quote! {
621-
::godot::meta::Signature::<CallParams, CallRet>::in_varcall(
622-
instance_ptr,
623-
&call_ctx,
624-
args_ptr,
625-
arg_count,
626-
ret,
627-
err,
628-
#wrapped_method,
629-
)
688+
{
689+
let defaults = #default_parameters;
690+
::godot::meta::Signature::<CallParams, CallRet>::in_varcall(
691+
instance_ptr,
692+
&call_ctx,
693+
args_ptr,
694+
arg_count,
695+
&defaults,
696+
ret,
697+
err,
698+
#wrapped_method,
699+
)
700+
}
630701
}
631702
}
632703

0 commit comments

Comments
 (0)