Skip to content

Commit 5470c11

Browse files
authored
Merge pull request #1315 from godot-rust/qol/type-safe-replacements
More type-safe engine APIs (mostly int -> enum)
2 parents b0ba40e + 8215c6b commit 5470c11

File tree

20 files changed

+471
-78
lines changed

20 files changed

+471
-78
lines changed

godot-codegen/src/generator/classes.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -573,6 +573,8 @@ fn make_class_method_definition(
573573
return FnDefinition::none();
574574
};
575575

576+
// Note: parameter type replacements (int -> enum) are already handled during domain mapping.
577+
576578
let rust_class_name = class.name().rust_ty.to_string();
577579
let rust_method_name = method.name();
578580
let godot_method_name = method.godot_name();

godot-codegen/src/generator/default_parameters.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ pub fn make_function_definition_with_defaults(
8080
#[doc = #builder_doc]
8181
#[must_use]
8282
#cfg_attributes
83-
pub struct #builder_ty<'a> {
83+
#vis struct #builder_ty<'a> {
8484
_phantom: std::marker::PhantomData<&'a ()>,
8585
#( #builder_field_decls, )*
8686
}

godot-codegen/src/models/domain.rs

Lines changed: 110 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -509,47 +509,93 @@ pub struct FnParam {
509509
}
510510

511511
impl FnParam {
512-
pub fn new_range(method_args: &Option<Vec<JsonMethodArg>>, ctx: &mut Context) -> Vec<FnParam> {
513-
option_as_slice(method_args)
514-
.iter()
515-
.map(|arg| Self::new(arg, ctx))
516-
.collect()
512+
/// Creates a new parameter builder for constructing function parameters with configurable options.
513+
pub fn builder() -> FnParamBuilder {
514+
FnParamBuilder::new()
515+
}
516+
}
517+
518+
/// Builder for constructing `FnParam` instances with configurable enum replacements and default value handling.
519+
pub struct FnParamBuilder {
520+
replacements: EnumReplacements,
521+
no_defaults: bool,
522+
}
523+
524+
impl FnParamBuilder {
525+
/// Creates a new parameter builder with default settings (no replacements, defaults enabled).
526+
pub fn new() -> Self {
527+
Self {
528+
replacements: &[],
529+
no_defaults: false,
530+
}
517531
}
518532

519-
pub fn new_range_no_defaults(
533+
/// Configures the builder to use specific enum replacements.
534+
pub fn enum_replacements(mut self, replacements: EnumReplacements) -> Self {
535+
self.replacements = replacements;
536+
self
537+
}
538+
539+
/// Configures the builder to exclude default values from generated parameters.
540+
pub fn no_defaults(mut self) -> Self {
541+
self.no_defaults = true;
542+
self
543+
}
544+
545+
/// Builds a single function parameter from the provided JSON method argument.
546+
pub fn build_single(self, method_arg: &JsonMethodArg, ctx: &mut Context) -> FnParam {
547+
self.build_single_impl(method_arg, ctx)
548+
}
549+
550+
/// Builds a vector of function parameters from the provided JSON method arguments.
551+
pub fn build_many(
552+
self,
520553
method_args: &Option<Vec<JsonMethodArg>>,
521554
ctx: &mut Context,
522555
) -> Vec<FnParam> {
523556
option_as_slice(method_args)
524557
.iter()
525-
.map(|arg| Self::new_no_defaults(arg, ctx))
558+
.map(|arg| self.build_single_impl(arg, ctx))
526559
.collect()
527560
}
528561

529-
pub fn new(method_arg: &JsonMethodArg, ctx: &mut Context) -> FnParam {
562+
/// Core implementation for processing a single JSON method argument into a `FnParam`.
563+
fn build_single_impl(&self, method_arg: &JsonMethodArg, ctx: &mut Context) -> FnParam {
530564
let name = safe_ident(&method_arg.name);
531565
let type_ = conv::to_rust_type(&method_arg.type_, method_arg.meta.as_ref(), ctx);
532-
let default_value = method_arg
533-
.default_value
534-
.as_ref()
535-
.map(|v| conv::to_rust_expr(v, &type_));
566+
567+
// Apply enum replacement if one exists for this parameter
568+
let matching_replacement = self
569+
.replacements
570+
.iter()
571+
.find(|(p, ..)| *p == method_arg.name);
572+
let type_ = if let Some((_, enum_name, is_bitfield)) = matching_replacement {
573+
if !type_.is_integer() {
574+
panic!(
575+
"Parameter `{}` is of type {}, but can only replace int with enum",
576+
method_arg.name, type_
577+
);
578+
}
579+
conv::to_enum_type_uncached(enum_name, *is_bitfield)
580+
} else {
581+
type_
582+
};
583+
584+
let default_value = if self.no_defaults {
585+
None
586+
} else {
587+
method_arg
588+
.default_value
589+
.as_ref()
590+
.map(|v| conv::to_rust_expr(v, &type_))
591+
};
536592

537593
FnParam {
538594
name,
539595
type_,
540596
default_value,
541597
}
542598
}
543-
544-
/// `impl AsArg<Gd<T>>` for object parameters. Only set if requested and `T` is an engine class.
545-
pub fn new_no_defaults(method_arg: &JsonMethodArg, ctx: &mut Context) -> FnParam {
546-
FnParam {
547-
name: safe_ident(&method_arg.name),
548-
type_: conv::to_rust_type(&method_arg.type_, method_arg.meta.as_ref(), ctx),
549-
//type_: to_rust_type(&method_arg.type_, &method_arg.meta, ctx),
550-
default_value: None,
551-
}
552-
}
553599
}
554600

555601
impl fmt::Debug for FnParam {
@@ -572,9 +618,31 @@ pub struct FnReturn {
572618

573619
impl FnReturn {
574620
pub fn new(return_value: &Option<JsonMethodReturn>, ctx: &mut Context) -> Self {
621+
Self::with_enum_replacements(return_value, &[], ctx)
622+
}
623+
624+
pub fn with_enum_replacements(
625+
return_value: &Option<JsonMethodReturn>,
626+
replacements: EnumReplacements,
627+
ctx: &mut Context,
628+
) -> Self {
575629
if let Some(ret) = return_value {
576630
let ty = conv::to_rust_type(&ret.type_, ret.meta.as_ref(), ctx);
577631

632+
// Apply enum replacement if one exists for return type (indicated by empty string)
633+
let matching_replacement = replacements.iter().find(|(p, ..)| p.is_empty());
634+
let ty = if let Some((_, enum_name, is_bitfield)) = matching_replacement {
635+
if !ty.is_integer() {
636+
panic!(
637+
"Return type is of type {}, but can only replace int with enum",
638+
ty
639+
);
640+
}
641+
conv::to_enum_type_uncached(enum_name, *is_bitfield)
642+
} else {
643+
ty
644+
};
645+
578646
Self {
579647
decl: ty.return_decl(),
580648
type_: Some(ty),
@@ -607,6 +675,14 @@ impl FnReturn {
607675
}
608676
}
609677

678+
// ----------------------------------------------------------------------------------------------------------------------------------------------
679+
// Int->enum replacements
680+
681+
/// Replacement of int->enum in engine APIs; each tuple being `(param_name, enum_type, is_bitfield)`.
682+
///
683+
/// Empty string `""` is used as `param_name` to denote return type replacements.
684+
pub type EnumReplacements = &'static [(&'static str, &'static str, bool)];
685+
610686
// ----------------------------------------------------------------------------------------------------------------------------------------------
611687
// Godot type
612688

@@ -682,6 +758,18 @@ impl RustTy {
682758
other => quote! { -> #other },
683759
}
684760
}
761+
762+
pub fn is_integer(&self) -> bool {
763+
let RustTy::BuiltinIdent { ty, .. } = self else {
764+
return false;
765+
};
766+
767+
// isize/usize currently not supported (2025-09), but this is more future-proof.
768+
matches!(
769+
ty.to_string().as_str(),
770+
"i8" | "i16" | "i32" | "i64" | "u8" | "u16" | "u32" | "u64" | "isize" | "usize"
771+
)
772+
}
685773
}
686774

687775
impl ToTokens for RustTy {

godot-codegen/src/models/domain_mapping.rs

Lines changed: 64 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,14 @@ use crate::context::Context;
1313
use crate::models::domain::{
1414
BuildConfiguration, BuiltinClass, BuiltinMethod, BuiltinSize, BuiltinVariant, Class,
1515
ClassCommons, ClassConstant, ClassConstantValue, ClassMethod, ClassSignal, Constructor, Enum,
16-
Enumerator, EnumeratorValue, ExtensionApi, FnDirection, FnParam, FnQualifier, FnReturn,
17-
FunctionCommon, GodotApiVersion, ModName, NativeStructure, Operator, RustTy, Singleton, TyName,
18-
UtilityFunction,
16+
EnumReplacements, Enumerator, EnumeratorValue, ExtensionApi, FnDirection, FnParam, FnQualifier,
17+
FnReturn, FunctionCommon, GodotApiVersion, ModName, NativeStructure, Operator, RustTy,
18+
Singleton, TyName, UtilityFunction,
1919
};
2020
use crate::models::json::{
2121
JsonBuiltinClass, JsonBuiltinMethod, JsonBuiltinSizes, JsonClass, JsonClassConstant,
2222
JsonClassMethod, JsonConstructor, JsonEnum, JsonEnumConstant, JsonExtensionApi, JsonHeader,
23-
JsonMethodReturn, JsonNativeStructure, JsonOperator, JsonSignal, JsonSingleton,
23+
JsonMethodArg, JsonMethodReturn, JsonNativeStructure, JsonOperator, JsonSignal, JsonSingleton,
2424
JsonUtilityFunction,
2525
};
2626
use crate::util::{get_api_level, ident, option_as_slice};
@@ -378,7 +378,9 @@ impl BuiltinMethod {
378378
godot_name: method.name.clone(),
379379
// Disable default parameters for builtin classes.
380380
// They are not public-facing and need more involved implementation (lifetimes etc.). Also reduces number of symbols in API.
381-
parameters: FnParam::new_range_no_defaults(&method.arguments, ctx),
381+
parameters: FnParam::builder()
382+
.no_defaults()
383+
.build_many(&method.arguments, ctx),
382384
return_value: FnReturn::new(&return_value, ctx),
383385
is_vararg: method.is_vararg,
384386
is_private: false, // See 'exposed' below. Could be special_cases::is_method_private(builtin_name, &method.name),
@@ -431,7 +433,7 @@ impl ClassMethod {
431433

432434
Self::from_json_inner(
433435
method,
434-
rust_method_name,
436+
rust_method_name.as_ref(),
435437
class_name,
436438
FnDirection::Outbound { hash },
437439
ctx,
@@ -518,8 +520,22 @@ impl ClassMethod {
518520
is_required_in_json
519521
};
520522

521-
let parameters = FnParam::new_range(&method.arguments, ctx);
522-
let return_value = FnReturn::new(&method.return_value, ctx);
523+
// Ensure that parameters/return types listed in the replacement truly exist in the method.
524+
// The validation function now returns the validated replacement slice for reuse.
525+
let enum_replacements = validate_enum_replacements(
526+
class_name,
527+
&method.name,
528+
option_as_slice(&method.arguments),
529+
method.return_value.is_some(),
530+
);
531+
532+
let parameters = FnParam::builder()
533+
.enum_replacements(enum_replacements)
534+
.build_many(&method.arguments, ctx);
535+
536+
let return_value =
537+
FnReturn::with_enum_replacements(&method.return_value, enum_replacements, ctx);
538+
523539
let is_unsafe = Self::function_uses_pointers(&parameters, &return_value);
524540

525541
// Future note: if further changes are made to the virtual method name, make sure to make it reversible so that #[godot_api]
@@ -586,7 +602,7 @@ impl ClassSignal {
586602

587603
Some(Self {
588604
name: json_signal.name.clone(),
589-
parameters: FnParam::new_range(&json_signal.arguments, ctx),
605+
parameters: FnParam::builder().build_many(&json_signal.arguments, ctx),
590606
surrounding_class: surrounding_class.clone(),
591607
})
592608
}
@@ -605,7 +621,7 @@ impl UtilityFunction {
605621
let parameters = if function.is_vararg && args.len() == 1 && args[0].name == "arg1" {
606622
vec![]
607623
} else {
608-
FnParam::new_range(&function.arguments, ctx)
624+
FnParam::builder().build_many(&function.arguments, ctx)
609625
};
610626

611627
let godot_method_name = function.name.clone();
@@ -737,6 +753,44 @@ impl ClassConstant {
737753
}
738754
}
739755

756+
// ----------------------------------------------------------------------------------------------------------------------------------------------
757+
758+
/// Validates that all parameters and non-unit return types declared in an enum replacement slices actually exist in the method.
759+
///
760+
/// This is a measure to prevent accidental typos or listing inexistent parameters, which would have no effect.
761+
fn validate_enum_replacements(
762+
class_ty: &TyName,
763+
godot_method_name: &str,
764+
method_arguments: &[JsonMethodArg],
765+
has_return_type: bool,
766+
) -> EnumReplacements {
767+
let replacements =
768+
special_cases::get_class_method_param_enum_replacement(class_ty, godot_method_name);
769+
770+
for (param_name, enum_name, _) in replacements {
771+
if param_name.is_empty() {
772+
assert!(has_return_type,
773+
"Method `{class}.{godot_method_name}` has no return type, but replacement with `{enum_name}` is declared",
774+
class = class_ty.godot_ty
775+
);
776+
} else if !method_arguments.iter().any(|arg| arg.name == *param_name) {
777+
let available_params = method_arguments
778+
.iter()
779+
.map(|arg| format!(" * {}: {}", arg.name, arg.type_))
780+
.collect::<Vec<_>>()
781+
.join("\n");
782+
783+
panic!(
784+
"Method `{class}.{godot_method_name}` has no parameter `{param_name}`, but a replacement with `{enum_name}` is declared\n\
785+
\n{count} parameters available:\n{available_params}\n",
786+
class = class_ty.godot_ty, count = method_arguments.len(),
787+
);
788+
}
789+
}
790+
791+
replacements
792+
}
793+
740794
// ----------------------------------------------------------------------------------------------------------------------------------------------
741795
// Native structures
742796

0 commit comments

Comments
 (0)