Skip to content

Commit 7a27be0

Browse files
committed
Default parameters via #[opt(default = ...)] syntax
1 parent 439553a commit 7a27be0

File tree

10 files changed

+286
-112
lines changed

10 files changed

+286
-112
lines changed

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

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -121,17 +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-
default_args_count: usize,
126-
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.
127127
) -> Result<(), Self> {
128-
// This will need to be adjusted once optional parameters are supported in #[func].
129-
if arg_count + default_args_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 {
130132
return Ok(());
131133
}
132134

133135
let call_error = Self::failed_param_count(call_ctx, arg_count, param_count);
134-
135136
Err(call_error)
136137
}
137138

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: 29 additions & 9 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.offset($n), call_ctx, $n as isize)? },
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.offset(i as isize) });
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 as isize)? },
7393
)*
7494
);
7595

godot-core/src/meta/signature.rs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -63,24 +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,
71-
default_arg_count: usize,
72+
default_values: &[Variant],
7273
ret: sys::GDExtensionVariantPtr,
7374
err: *mut sys::GDExtensionCallError,
7475
func: unsafe fn(sys::GDExtensionClassInstancePtr, Params) -> Ret,
7576
) -> CallResult<()> {
7677
//$crate::out!("in_varcall: {call_ctx}");
77-
CallError::check_arg_count(call_ctx, arg_count as usize, default_arg_count, Params::LEN)?;
78+
let arg_count = arg_count as usize;
79+
CallError::check_arg_count(call_ctx, arg_count, default_values.len(), Params::LEN)?;
7880

7981
#[cfg(feature = "trace")]
8082
trace::push(true, false, call_ctx);
8183

8284
// SAFETY: TODO.
83-
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)? };
8487

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

godot-core/src/registry/method.rs

Lines changed: 2 additions & 0 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

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

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

123-
let (default_parameters, default_parameters_count) =
124-
make_default_parameters(&func_definition, signature_info)?;
123+
let default_parameters = make_default_argument_vec(
124+
&signature_info.optional_param_default_exprs,
125+
&signature_info.param_types,
126+
)?;
125127

126128
// String literals
127129
let class_name_str = class_name.to_string();
128130
let method_name_str = func_definition.godot_name();
129131

130132
let call_ctx = make_call_context(&class_name_str, &method_name_str);
131-
let varcall_fn_decl = make_varcall_fn(&call_ctx, &forwarding_closure, default_parameters_count);
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);
132141
let ptrcall_fn_decl = make_ptrcall_fn(&call_ctx, &forwarding_closure);
133142

134143
// String literals II
@@ -187,52 +196,46 @@ pub fn make_method_registration(
187196
Ok(registration)
188197
}
189198

190-
fn make_default_parameters(
191-
func_definition: &FuncDefinition,
192-
signature_info: &SignatureInfo,
193-
) -> Result<(TokenStream, usize), venial::Error> {
194-
let default_parameters =
195-
validate_default_parameters(&func_definition.signature_info.default_parameters)?;
196-
let len = default_parameters.len();
197-
let default_parameters_type = signature_info
198-
.param_types
199-
.iter()
200-
.rev()
201-
.take(default_parameters.len())
202-
.rev();
203-
let default_parameters = default_parameters
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
204212
.iter()
205-
.zip(default_parameters_type)
206-
.map(|(value, ty)| quote!(::godot::builtin::Variant::from(#value)));
207-
// .map(|(value, ty)| quote!(::godot::meta::arg_into_ref!(#value: #ty)));
208-
let default_parameters = quote! {vec![#(#default_parameters),*]};
209-
Ok((default_parameters, len))
210-
}
213+
.skip(all_params.len() - optional_param_default_exprs.len());
211214

212-
fn validate_default_parameters(
213-
default_parameters: &[Option<TokenStream>],
214-
) -> ParseResult<Vec<TokenStream>> {
215-
let mut res = vec![];
216-
let mut allowed = true;
217-
for param in default_parameters.iter().rev() {
218-
match (param, allowed) {
219-
(Some(tk), true) => {
220-
res.push(tk.clone()); // toreview: if we really care about it, we can use &mut sig_info and mem::take() as we don't use this later
221-
}
222-
(None, true) => {
223-
allowed = false;
224-
}
225-
(None, false) => {}
226-
(Some(tk), false) => {
227-
return bail!(
228-
tk,
229-
"opt arguments are only allowed at the end of the argument list."
230-
);
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+
)
231223
}
232-
}
233-
}
234-
res.reverse();
235-
Ok(res)
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+
})
236239
}
237240

238241
// ----------------------------------------------------------------------------------------------------------------------------------------------
@@ -260,8 +263,9 @@ pub struct SignatureInfo {
260263
///
261264
/// Index points into original venial tokens (i.e. takes into account potential receiver params).
262265
pub modified_param_types: Vec<(usize, venial::TypeExpr)>,
263-
/// Contains expressions of the default values of parameters.
264-
pub default_parameters: Vec<Option<TokenStream>>,
266+
267+
/// Default value expressions `EXPR` from `#[opt(default = EXPR)]`, for all optional parameters.
268+
pub optional_param_default_exprs: Vec<TokenStream>,
265269
}
266270

267271
impl SignatureInfo {
@@ -274,7 +278,7 @@ impl SignatureInfo {
274278
param_types: vec![],
275279
return_type: quote! { () },
276280
modified_param_types: vec![],
277-
default_parameters: vec![],
281+
optional_param_default_exprs: vec![],
278282
}
279283
}
280284

@@ -468,7 +472,7 @@ pub(crate) fn into_signature_info(
468472
let params_span = signature.span();
469473
let mut param_idents = Vec::with_capacity(num_params);
470474
let mut param_types = Vec::with_capacity(num_params);
471-
let ret_type = match signature.return_ty {
475+
let return_type = match signature.return_ty {
472476
None => quote! { () },
473477
Some(ty) => map_self_to_class_name(ty.tokens, class_name),
474478
};
@@ -524,9 +528,9 @@ pub(crate) fn into_signature_info(
524528
params_span,
525529
param_idents,
526530
param_types,
527-
return_type: ret_type,
531+
return_type,
528532
modified_param_types,
529-
default_parameters: vec![],
533+
optional_param_default_exprs: vec![], // Assigned outside, if relevant.
530534
}
531535
}
532536

@@ -611,9 +615,9 @@ fn make_method_flags(
611615
fn make_varcall_fn(
612616
call_ctx: &TokenStream,
613617
wrapped_method: &TokenStream,
614-
default_parameters_count: usize,
618+
default_parameters: &TokenStream,
615619
) -> TokenStream {
616-
let invocation = make_varcall_invocation(wrapped_method, default_parameters_count);
620+
let invocation = make_varcall_invocation(wrapped_method, default_parameters);
617621

618622
// TODO reduce amount of code generated, by delegating work to a library function. Could even be one that produces this function pointer.
619623
quote! {
@@ -678,19 +682,22 @@ fn make_ptrcall_invocation(wrapped_method: &TokenStream, is_virtual: bool) -> To
678682
/// Generate code for a `varcall()` call expression.
679683
fn make_varcall_invocation(
680684
wrapped_method: &TokenStream,
681-
default_parameters_count: usize,
685+
default_parameters: &TokenStream,
682686
) -> TokenStream {
683687
quote! {
684-
::godot::meta::Signature::<CallParams, CallRet>::in_varcall(
685-
instance_ptr,
686-
&call_ctx,
687-
args_ptr,
688-
arg_count,
689-
#default_parameters_count,
690-
ret,
691-
err,
692-
#wrapped_method,
693-
)
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+
}
694701
}
695702
}
696703

0 commit comments

Comments
 (0)