Skip to content

Commit 295029c

Browse files
committed
Support default methods on builtins; rework API
1 parent 7528baf commit 295029c

File tree

16 files changed

+271
-102
lines changed

16 files changed

+271
-102
lines changed

godot-codegen/src/context.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -210,7 +210,7 @@ impl<'a> Context<'a> {
210210
methods: &[JsonBuiltinMethod],
211211
ctx: &mut Context,
212212
) {
213-
let builtin_ty = TyName::from_godot(builtin.name.as_str());
213+
let builtin_ty = TyName::from_godot_builtin(builtin);
214214
if special_cases::is_builtin_type_deleted(&builtin_ty) {
215215
return;
216216
}

godot-codegen/src/conv/type_conversions.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ fn to_hardcoded_rust_ident(full_ty: &GodotTy) -> Option<&str> {
5050
("double", None) => "f64",
5151
("double", Some(meta)) => panic!("unhandled type double with meta {meta:?}"),
5252

53-
// Others
53+
// Others. Keep in sync with BuiltinClass::from_json().
5454
("bool", None) => "bool",
5555
("String", None) => "GString",
5656
("Array", None) => "VariantArray",

godot-codegen/src/generator/builtins.rs

Lines changed: 54 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,14 @@ use crate::{conv, util, SubmitFn};
2323
// Shared with native_structures.rs.
2424
pub struct GeneratedBuiltin {
2525
pub code: TokenStream,
26+
pub has_sidecar_module: bool,
2627
}
2728

2829
pub struct GeneratedBuiltinModule {
29-
pub symbol_ident: Ident,
30+
pub outer_builtin: Ident,
31+
pub inner_builtin: Ident,
3032
pub module_name: ModName,
33+
pub is_pub_sidecar: bool,
3134
}
3235

3336
pub fn generate_builtin_class_files(
@@ -49,16 +52,18 @@ pub fn generate_builtin_class_files(
4952
let module_name = class.mod_name();
5053

5154
let variant_shout_name = util::ident(variant.godot_shout_name());
52-
let generated_class = make_builtin_class(class, &variant_shout_name, ctx);
53-
let file_contents = generated_class.code;
55+
let generated_builtin = make_builtin_class(class, &variant_shout_name, ctx);
56+
let file_contents = generated_builtin.code;
5457

5558
let out_path = gen_path.join(format!("{}.rs", module_name.rust_mod));
5659

5760
submit_fn(out_path, file_contents);
5861

5962
modules.push(GeneratedBuiltinModule {
60-
symbol_ident: class.inner_name().clone(),
63+
outer_builtin: class.name().rust_ty.clone(),
64+
inner_builtin: class.inner_name().clone(),
6165
module_name: module_name.clone(),
66+
is_pub_sidecar: generated_builtin.has_sidecar_module,
6267
});
6368
}
6469

@@ -71,14 +76,21 @@ pub fn generate_builtin_class_files(
7176
pub fn make_builtin_module_file(classes_and_modules: Vec<GeneratedBuiltinModule>) -> TokenStream {
7277
let decls = classes_and_modules.iter().map(|m| {
7378
let GeneratedBuiltinModule {
79+
outer_builtin,
80+
inner_builtin,
7481
module_name,
75-
symbol_ident,
76-
..
82+
is_pub_sidecar,
7783
} = m;
7884

85+
// Module is public if it has default extenders (Ex* builders). Enums do not contribute, they're all manually redefined.
86+
let vis = is_pub_sidecar.then_some(quote! { pub });
87+
88+
let doc = format!("Default extenders for builtin type [`{outer_builtin}`][crate::builtin::{outer_builtin}].");
89+
7990
quote! {
80-
mod #module_name;
81-
pub use #module_name::#symbol_ident;
91+
#[doc = #doc]
92+
#vis mod #module_name;
93+
pub use #module_name::#inner_builtin;
8294
}
8395
});
8496

@@ -103,30 +115,39 @@ fn make_builtin_class(
103115
else {
104116
panic!("Rust type `{godot_name}` categorized wrong")
105117
};
106-
let inner_class = class.inner_name();
118+
let inner_builtin = class.inner_name();
107119

108-
// Note: builders are currently disabled for builtins, even though default parameters exist (e.g. String::substr).
109-
// We just use the full signature instead. For outer APIs (user-facing), try to find idiomatic APIs.
120+
// Enable `Ex*` builders for builtins to provide consistent API with classes.
121+
// By default, builders are only placed on outer methods (for methods that are exposed per special_cases.rs), to reduce codegen.
122+
// However it's possible to enable builders on the inner type, too, which is why both `inner_builders` + `outer_builders` exist.
110123
#[rustfmt::skip]
111124
let (
112-
FnDefinitions { functions: inner_methods, .. },
113-
FnDefinitions { functions: outer_methods, .. },
125+
FnDefinitions { functions: inner_methods, builders: inner_builders },
126+
FnDefinitions { functions: outer_methods, builders: outer_builders },
114127
) = make_builtin_methods(class, variant_shout_name, &class.methods, ctx);
115128

116129
let imports = util::make_imports();
117130
let enums = enums::make_enums(&class.enums, &TokenStream::new());
118131
let special_constructors = make_special_builtin_methods(class.name(), ctx);
119132

120-
// mod re_export needed, because class should not appear inside the file module, and we can't re-export private struct as pub
133+
// `mod re_export` needed for builder structs to reference the Inner* type, similar to how classes use `re_export` for the class type.
121134
let code = quote! {
122135
#imports
123136

124-
#[repr(transparent)]
125-
pub struct #inner_class<'a> {
126-
_outer_lifetime: std::marker::PhantomData<&'a ()>,
127-
sys_ptr: sys::GDExtensionTypePtr,
137+
pub(super) mod re_export {
138+
use super::*;
139+
140+
// Do *not* try to limit visibility, because inner types are used in PackedArrayElement::Inner<'a> associated type.
141+
// Need to redesign that trait otherwise, and split into private/public parts.
142+
#[doc(hidden)]
143+
#[repr(transparent)]
144+
pub struct #inner_builtin<'inner> {
145+
pub(super) _outer_lifetime: std::marker::PhantomData<&'inner ()>,
146+
pub(super) sys_ptr: sys::GDExtensionTypePtr,
147+
}
128148
}
129-
impl<'a> #inner_class<'a> {
149+
150+
impl<'inner> re_export::#inner_builtin<'inner> {
130151
pub fn from_outer(outer: &#outer_class) -> Self {
131152
Self {
132153
_outer_lifetime: std::marker::PhantomData,
@@ -136,16 +157,29 @@ fn make_builtin_class(
136157
#special_constructors
137158
#inner_methods
138159
}
160+
161+
// Re-export Inner* type for convenience.
162+
pub use re_export::#inner_builtin;
163+
139164
// Selected APIs appear directly in the outer class.
140165
impl #outer_class {
141166
#outer_methods
142167
}
143168

169+
#inner_builders
170+
#outer_builders
144171
#enums
145172
};
146173
// note: TypePtr -> ObjectPtr conversion OK?
147174

148-
GeneratedBuiltin { code }
175+
// If any exposed builders are present, generate a sidecar module for the builtin.
176+
// Do not care about inner Ex* builders, or builtin enums (enums are manually defined).
177+
let has_sidecar_module = !outer_builders.is_empty();
178+
179+
GeneratedBuiltin {
180+
code,
181+
has_sidecar_module,
182+
}
149183
}
150184

151185
/// Returns 2 definition packs, one for the `Inner*` methods, and one for those ending up directly in the public-facing (outer) class.

godot-codegen/src/generator/default_parameters.rs

Lines changed: 62 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ pub fn make_function_definition_with_defaults(
3333
let default_parameter_usage = format!("To set the default parameters, use [`Self::{extended_fn_name}`] and its builder methods. See [the book](https://godot-rust.github.io/book/godot-api/functions.html#default-parameters) for detailed usage instructions.");
3434
let vis = functions_common::make_vis(sig.is_private());
3535

36-
let (builder_doc, surround_class_prefix) = make_extender_doc(sig, &extended_fn_name);
36+
let (builder_doc, surround_class_path) = make_extender_doc(sig, &extended_fn_name);
3737

3838
let ExtenderReceiver {
3939
object_fn_param,
@@ -62,26 +62,26 @@ pub fn make_function_definition_with_defaults(
6262
let return_decl = &sig.return_value().decl;
6363

6464
// If either the builder has a lifetime (non-static/global method), or one of its parameters is a reference,
65-
// then we need to annotate the _ex() function with an explicit lifetime. Also adjust &self -> &'a self.
65+
// then we need to annotate the _ex() function with an explicit lifetime. Also adjust &self -> &'ex self.
6666
let receiver_self = &code.receiver.self_prefix;
6767
let simple_receiver_param = &code.receiver.param;
68-
let extended_receiver_param = &code.receiver.param_lifetime_a;
68+
let extended_receiver_param = &code.receiver.param_lifetime_ex;
6969

7070
let builders = quote! {
7171
#[doc = #builder_doc]
7272
#[must_use]
7373
#cfg_attributes
74-
#vis struct #builder_ty<'a> {
75-
_phantom: std::marker::PhantomData<&'a ()>,
74+
#vis struct #builder_ty<'ex> {
75+
_phantom: std::marker::PhantomData<&'ex ()>,
7676
#( #builder_field_decls, )*
7777
}
7878

7979
// #[allow] exceptions:
8080
// - wrong_self_convention: to_*() and from_*() are taken from Godot
8181
// - redundant_field_names: 'value: value' is a possible initialization pattern
82-
// - needless-update: Remainder expression '..self' has nothing left to change
82+
// - needless_update: Remainder expression '..self' has nothing left to change
8383
#[allow(clippy::wrong_self_convention, clippy::redundant_field_names, clippy::needless_update)]
84-
impl<'a> #builder_ty<'a> {
84+
impl<'ex> #builder_ty<'ex> {
8585
fn new(
8686
//#object_param
8787
#( #builder_ctor_params, )*
@@ -98,7 +98,7 @@ pub fn make_function_definition_with_defaults(
9898
#[inline]
9999
pub fn done(self) #return_decl {
100100
let Self { _phantom, #( #builder_field_names, )* } = self;
101-
#surround_class_prefix #full_fn_name(
101+
#surround_class_path::#full_fn_name(
102102
#( #full_fn_args, )* // includes `surround_object` if present
103103
)
104104
}
@@ -122,10 +122,10 @@ pub fn make_function_definition_with_defaults(
122122
// _ex() function:
123123
// Lifetime is set if any parameter is a reference OR if the method is not static/global (and thus can refer to self).
124124
#[inline]
125-
#vis fn #extended_fn_name<'a> (
125+
#vis fn #extended_fn_name<'ex> (
126126
#extended_receiver_param
127127
#( #class_method_required_params_lifetimed, )*
128-
) -> #builder_ty<'a> {
128+
) -> #builder_ty<'ex> {
129129
#builder_ty::new(
130130
#object_arg
131131
#( #class_method_required_args, )*
@@ -137,11 +137,30 @@ pub fn make_function_definition_with_defaults(
137137
}
138138

139139
pub fn function_uses_default_params(sig: &dyn Function) -> bool {
140-
sig.params().iter().any(|arg| arg.default_value.is_some())
140+
// For builtin types, strip "Inner" prefix, while avoiding collision with classes that might start with "Inner".
141+
let class_or_builtin = sig.surrounding_class().map(|ty| {
142+
let rust_name = ty.rust_ty.to_string();
143+
144+
// If it’s builtin and starts with "Inner", drop that prefix; otherwise keep original string.
145+
match (sig.is_builtin(), rust_name.strip_prefix("Inner")) {
146+
(true, Some(rest)) => rest.to_string(),
147+
_ => rust_name,
148+
}
149+
});
150+
151+
let fn_declares_default_params = sig.params().iter().any(|arg| arg.default_value.is_some())
141152
&& !special_cases::is_method_excluded_from_default_params(
142-
sig.surrounding_class(),
153+
class_or_builtin.as_deref(),
143154
sig.name(),
144-
)
155+
);
156+
157+
// For builtins, only generate `Ex*` builders if the method is exposed on the outer type.
158+
// This saves on code generation and compile time for `Inner*` methods.
159+
if fn_declares_default_params && sig.is_builtin() {
160+
return sig.is_exposed_outer_builtin();
161+
}
162+
163+
fn_declares_default_params
145164
}
146165

147166
// ----------------------------------------------------------------------------------------------------------------------------------------------
@@ -179,11 +198,17 @@ fn make_extender_doc(sig: &dyn Function, extended_fn_name: &Ident) -> (String, T
179198
#[allow(clippy::uninlined_format_args)]
180199
match sig.surrounding_class() {
181200
Some(TyName { rust_ty, .. }) => {
182-
surround_class_prefix = quote! { re_export::#rust_ty:: };
201+
surround_class_prefix = make_qualified_type(sig, rust_ty, true);
202+
let path = if sig.is_builtin() {
203+
format!("crate::builtin::{rust_ty}")
204+
} else {
205+
format!("super::{rust_ty}")
206+
};
183207
builder_doc = format!(
184-
"Default-param extender for [`{class}::{method}`][super::{class}::{method}].",
208+
"Default-param extender for [`{class}::{method}`][{path}::{method}].",
185209
class = rust_ty,
186210
method = extended_fn_name,
211+
path = path,
187212
);
188213
}
189214
None => {
@@ -213,13 +238,13 @@ fn make_extender_receiver(sig: &dyn Function) -> ExtenderReceiver {
213238
// Only add it if the method is not global or static.
214239
match sig.surrounding_class() {
215240
Some(surrounding_class) if !sig.qualifier().is_static_or_global() => {
216-
let class = &surrounding_class.rust_ty;
241+
let ty = make_qualified_type(sig, &surrounding_class.rust_ty, false);
217242

218243
ExtenderReceiver {
219244
object_fn_param: Some(FnParam {
220245
name: ident("surround_object"),
221246
type_: RustTy::ExtenderReceiver {
222-
tokens: quote! { &'a #builder_mut re_export::#class },
247+
tokens: quote! { &'ex #builder_mut #ty },
223248
},
224249
default_value: None,
225250
}),
@@ -331,3 +356,23 @@ fn make_extender(
331356
class_method_required_args,
332357
}
333358
}
359+
360+
/// Returns a qualified type path for builtin or class.
361+
///
362+
/// Type categories:
363+
/// - Exposed outer builtins (like `GString`): direct type reference without `re_export::`.
364+
/// - `Inner\*` types (like `InnerString`): `re_export::Type<'ex>`, with lifetime if `with_inner_lifetime` is true.
365+
/// - Classes (like `Node`): `re_export::Type` without lifetime.
366+
fn make_qualified_type(
367+
sig: &dyn Function,
368+
class_or_builtin: &Ident,
369+
with_inner_lifetime: bool,
370+
) -> TokenStream {
371+
if sig.is_exposed_outer_builtin() {
372+
quote! { #class_or_builtin }
373+
} else if with_inner_lifetime && sig.is_builtin() {
374+
quote! { re_export::#class_or_builtin<'ex> }
375+
} else {
376+
quote! { re_export::#class_or_builtin }
377+
}
378+
}

0 commit comments

Comments
 (0)