Skip to content

Commit adcb58d

Browse files
authored
Merge pull request #1162 from godot-rust/qol/final-and-uninstantiable-classes
Final and non-instantiable classes
2 parents 9f60c2e + 1bda87b commit adcb58d

File tree

7 files changed

+158
-41
lines changed

7 files changed

+158
-41
lines changed

godot-codegen/src/context.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -271,8 +271,8 @@ impl<'a> Context<'a> {
271271
self.native_structures_types.contains(ty_name)
272272
}
273273

274-
pub fn is_singleton(&self, class_name: &str) -> bool {
275-
self.singletons.contains(class_name)
274+
pub fn is_singleton(&self, class_name: &TyName) -> bool {
275+
self.singletons.contains(class_name.godot_ty.as_str())
276276
}
277277

278278
pub fn inheritance_tree(&self) -> &InheritanceTree {

godot-codegen/src/generator/classes.rs

Lines changed: 107 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ pub fn generate_class_files(
6060
struct GeneratedClass {
6161
code: TokenStream,
6262
notification_enum: NotificationEnum,
63-
inherits_macro_ident: Ident,
63+
inherits_macro_ident: Option<Ident>,
6464
/// Sidecars are the associated modules with related enum/flag types, such as `node_3d` for `Node3D` class.
6565
has_sidecar_module: bool,
6666
}
@@ -69,10 +69,17 @@ struct GeneratedClassModule {
6969
class_name: TyName,
7070
module_name: ModName,
7171
own_notification_enum_name: Option<Ident>,
72-
inherits_macro_ident: Ident,
72+
inherits_macro_ident: Option<Ident>,
7373
is_pub_sidecar: bool,
7474
}
7575

76+
struct Construction {
77+
constructor: TokenStream,
78+
construct_doc: &'static str,
79+
final_doc: Option<&'static str>,
80+
godot_default_impl: TokenStream,
81+
}
82+
7683
fn make_class(class: &Class, ctx: &mut Context, view: &ApiView) -> GeneratedClass {
7784
let class_name = class.name();
7885

@@ -90,8 +97,16 @@ fn make_class(class: &Class, ctx: &mut Context, view: &ApiView) -> GeneratedClas
9097
None => (quote! { crate::obj::NoBase }, None),
9198
};
9299

93-
let (constructor, construct_doc, godot_default_impl) = make_constructor_and_default(class, ctx);
94-
let construct_doc = construct_doc.replace("Self", &class_name.rust_ty.to_string());
100+
let Construction {
101+
constructor,
102+
construct_doc,
103+
final_doc,
104+
godot_default_impl,
105+
} = make_constructor_and_default(class, ctx);
106+
107+
let mut extended_class_doc = construct_doc.replace("Self", &class_name.rust_ty.to_string());
108+
extended_class_doc.push_str(final_doc.unwrap_or_default());
109+
95110
let api_level = class.api_level;
96111
let init_level = api_level.to_init_level();
97112

@@ -130,10 +145,10 @@ fn make_class(class: &Class, ctx: &mut Context, view: &ApiView) -> GeneratedClas
130145

131146
let enums = enums::make_enums(&class.enums, &cfg_attributes);
132147
let constants = constants::make_constants(&class.constants);
133-
let inherits_macro = format_ident!("unsafe_inherits_transitive_{}", class_name.rust_ty);
134148
let deref_impl = make_deref_impl(class_name, &base_ty);
135149

136150
let all_bases = ctx.inheritance_tree().collect_all_bases(class_name);
151+
let (inherits_macro_ident, inherits_macro_code) = make_inherits_macro(class, &all_bases);
137152
let (notification_enum, notification_enum_name) =
138153
notifications::make_notification_enum(class_name, &all_bases, &cfg_attributes, ctx);
139154

@@ -180,11 +195,6 @@ fn make_class(class: &Class, ctx: &mut Context, view: &ApiView) -> GeneratedClas
180195
}
181196
};
182197

183-
let inherits_macro_safety_doc = format!(
184-
"The provided class must be a subclass of all the superclasses of [`{}`]",
185-
class_name.rust_ty
186-
);
187-
188198
// mod re_export needed, because class should not appear inside the file module, and we can't re-export private struct as pub.
189199
let imports = util::make_imports();
190200
let tokens = quote! {
@@ -199,7 +209,7 @@ fn make_class(class: &Class, ctx: &mut Context, view: &ApiView) -> GeneratedClas
199209
use super::*;
200210

201211
#[doc = #class_doc]
202-
#[doc = #construct_doc]
212+
#[doc = #extended_class_doc]
203213
#cfg_attributes
204214
#[derive(Debug)]
205215
#[repr(C)]
@@ -247,20 +257,7 @@ fn make_class(class: &Class, ctx: &mut Context, view: &ApiView) -> GeneratedClas
247257

248258
#godot_default_impl
249259
#deref_impl
250-
251-
/// # Safety
252-
///
253-
#[doc = #inherits_macro_safety_doc]
254-
#[macro_export]
255-
#[allow(non_snake_case)]
256-
macro_rules! #inherits_macro {
257-
($Class:ident) => {
258-
unsafe impl ::godot::obj::Inherits<::godot::classes::#class_name> for $Class {}
259-
#(
260-
unsafe impl ::godot::obj::Inherits<::godot::classes::#all_bases> for $Class {}
261-
)*
262-
}
263-
}
260+
#inherits_macro_code
264261
}
265262

266263
#builders
@@ -275,11 +272,69 @@ fn make_class(class: &Class, ctx: &mut Context, view: &ApiView) -> GeneratedClas
275272
name: notification_enum_name,
276273
declared_by_own_class: notification_enum.is_some(),
277274
},
278-
inherits_macro_ident: inherits_macro,
275+
inherits_macro_ident,
279276
has_sidecar_module,
280277
}
281278
}
282279

280+
/// If the class can be inherited from (non-final), create a macro that can be accessed in subclasses to implement the `Inherits` trait.
281+
///
282+
/// Returns empty tokens if the class is final.
283+
fn make_inherits_macro(class: &Class, all_bases: &[TyName]) -> (Option<Ident>, TokenStream) {
284+
let class_name = class.name();
285+
286+
// Create a macro that can be accessed in subclasses to implement the Inherits trait.
287+
// Use this name because when typing a non-existent class, users will be met with the following error:
288+
// could not find `inherit_from_OS__ensure_class_exists` in `class_macros`
289+
//
290+
// Former macro name was `unsafe_inherits_transitive_*`.
291+
let inherits_macro_ident =
292+
format_ident!("inherit_from_{}__ensure_class_exists", class_name.rust_ty);
293+
294+
// For final classes, we can directly create a meaningful compile error.
295+
if class.is_final {
296+
let error_msg = format!(
297+
"Class `{}` is final and cannot be inherited from.",
298+
class_name.rust_ty
299+
);
300+
301+
let code = quote! {
302+
#[macro_export]
303+
#[allow(non_snake_case)]
304+
macro_rules! #inherits_macro_ident {
305+
($Class:ident) => {
306+
compile_error!(#error_msg);
307+
}
308+
}
309+
};
310+
311+
return (None, code);
312+
}
313+
314+
let inherits_macro_safety_doc = format!(
315+
"The provided class must be a subclass of all the superclasses of [`{}`]",
316+
class_name.rust_ty
317+
);
318+
319+
let code = quote! {
320+
/// # Safety
321+
///
322+
#[doc = #inherits_macro_safety_doc]
323+
#[macro_export]
324+
#[allow(non_snake_case)]
325+
macro_rules! #inherits_macro_ident {
326+
($Class:ident) => {
327+
unsafe impl ::godot::obj::Inherits<::godot::classes::#class_name> for $Class {}
328+
#(
329+
unsafe impl ::godot::obj::Inherits<::godot::classes::#all_bases> for $Class {}
330+
)*
331+
}
332+
}
333+
};
334+
335+
(Some(inherits_macro_ident), code)
336+
}
337+
283338
fn make_class_module_file(classes_and_modules: Vec<GeneratedClassModule>) -> TokenStream {
284339
let mut class_decls = Vec::new();
285340
let mut notify_decls = Vec::new();
@@ -318,6 +373,11 @@ fn make_class_module_file(classes_and_modules: Vec<GeneratedClassModule>) -> Tok
318373
..
319374
} = m;
320375

376+
// For final classes, do nothing.
377+
let Some(inherits_macro_ident) = inherits_macro_ident else {
378+
return TokenStream::new();
379+
};
380+
321381
// We cannot re-export the following, because macro is in the crate root
322382
// pub use #module_ident::re_export::#inherits_macro_ident;
323383
quote! {
@@ -341,17 +401,15 @@ fn make_class_module_file(classes_and_modules: Vec<GeneratedClassModule>) -> Tok
341401
}
342402
}
343403

344-
fn make_constructor_and_default(
345-
class: &Class,
346-
ctx: &Context,
347-
) -> (TokenStream, &'static str, TokenStream) {
348-
let godot_class_name = &class.name().godot_ty;
349-
let godot_class_stringname = make_string_name(godot_class_name);
350-
// Note: this could use class_name() but is not yet done due to upcoming lazy-load refactoring.
404+
fn make_constructor_and_default(class: &Class, ctx: &Context) -> Construction {
405+
let class_name = class.name();
406+
407+
let godot_class_stringname = make_string_name(&class_name.godot_ty);
408+
// Note: this could use class_name() but is not yet done due to potential future lazy-load refactoring.
351409
//let class_name_obj = quote! { <Self as crate::obj::GodotClass>::class_name() };
352410

353411
let (constructor, construct_doc, has_godot_default_impl);
354-
if ctx.is_singleton(godot_class_name) {
412+
if ctx.is_singleton(class_name) {
355413
// Note: we cannot return &'static mut Self, as this would be very easy to mutably alias.
356414
// &'static Self would be possible, but we would lose the whole mutability information (even if that is best-effort and
357415
// not strict Rust mutability, it makes the API much more usable).
@@ -391,6 +449,15 @@ fn make_constructor_and_default(
391449
has_godot_default_impl = true;
392450
}
393451

452+
let final_doc = if class.is_final {
453+
Some(
454+
"\n\n# Final class\n\n\
455+
This class is _final_, meaning you cannot inherit from it.",
456+
)
457+
} else {
458+
None
459+
};
460+
394461
let godot_default_impl = if has_godot_default_impl {
395462
let class_name = &class.name().rust_ty;
396463
quote! {
@@ -404,7 +471,12 @@ fn make_constructor_and_default(
404471
TokenStream::new()
405472
};
406473

407-
(constructor, construct_doc, godot_default_impl)
474+
Construction {
475+
constructor,
476+
construct_doc,
477+
final_doc,
478+
godot_default_impl,
479+
}
408480
}
409481

410482
fn make_deref_impl(class_name: &TyName, base_ty: &TokenStream) -> TokenStream {

godot-codegen/src/generator/notifications.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ pub fn make_notify_methods(class_name: &TyName, ctx: &mut Context) -> TokenStrea
6060

6161
pub fn make_notification_enum(
6262
class_name: &TyName,
63-
all_bases: &Vec<TyName>,
63+
all_bases: &[TyName],
6464
cfg_attributes: &TokenStream,
6565
ctx: &mut Context,
6666
) -> (Option<TokenStream>, Ident) {

godot-codegen/src/models/domain.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,8 @@ pub struct Class {
161161
pub is_refcounted: bool,
162162
pub is_instantiable: bool,
163163
pub is_experimental: bool,
164+
/// `true` if inheriting the class is disallowed.
165+
pub is_final: bool,
164166
pub inherits: Option<String>,
165167
pub api_level: ClassCodegenLevel,
166168
pub constants: Vec<ClassConstant>,

godot-codegen/src/models/domain_mapping.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,9 @@ impl Class {
9191

9292
// Already checked in is_class_deleted(), but code remains more maintainable if those are separate, and it's cheap to validate.
9393
let is_experimental = special_cases::is_class_experimental(&ty_name.godot_ty);
94+
let is_instantiable =
95+
special_cases::is_class_instantiable(&ty_name).unwrap_or(json.is_instantiable);
96+
let is_final = ctx.is_singleton(&ty_name) || special_cases::is_class_final(&ty_name);
9497

9598
let mod_name = ModName::from_godot(&ty_name.godot_ty);
9699

@@ -129,8 +132,9 @@ impl Class {
129132
mod_name,
130133
},
131134
is_refcounted: json.is_refcounted,
132-
is_instantiable: json.is_instantiable,
135+
is_instantiable,
133136
is_experimental,
137+
is_final,
134138
inherits: json.inherits.clone(),
135139
api_level: get_api_level(json),
136140
constants,

godot-codegen/src/special_cases/special_cases.rs

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,42 @@ pub fn is_class_experimental(godot_class_name: &str) -> bool {
206206
}
207207
}
208208

209+
/// Whether a class can be instantiated (overrides Godot's defaults in some cases).
210+
///
211+
/// Returns `None` if the Godot default should be taken.
212+
pub fn is_class_instantiable(class_ty: &TyName) -> Option<bool> {
213+
let class_name = class_ty.godot_ty.as_str();
214+
215+
// The default constructor is available but callers meet with the following Godot error:
216+
// "ERROR: XY can't be created directly. Use create_tween() method."
217+
// for the following classes XY:
218+
//Tween, PropertyTweener, PropertyTweener, IntervalTweener, CallbackTweener, MethodTweener, SubtweenTweener,
219+
220+
if class_name == "Tween" || class_name.ends_with("Tweener") {
221+
return Some(false);
222+
}
223+
224+
None
225+
}
226+
227+
/// Whether a class is final, i.e. cannot be inherited from.
228+
///
229+
/// Note that cases where the final status can be inferred from other properties (e.g. being a singleton) are already handled outside this
230+
/// function.
231+
#[rustfmt::skip]
232+
pub fn is_class_final(class_ty: &TyName) -> bool {
233+
match class_ty.godot_ty.as_str(){
234+
// https://github.com/godot-rust/gdext/issues/1129
235+
| "AudioStreamGeneratorPlayback"
236+
| "AudioStreamPlaybackInteractive"
237+
| "AudioStreamPlaybackPlaylist"
238+
| "AudioStreamPlaybackPolyphonic"
239+
| "AudioStreamPlaybackSynchronized"
240+
241+
=> true, _ => false
242+
}
243+
}
244+
209245
/// Whether a method is available in the method table as a named accessor.
210246
#[rustfmt::skip]
211247
pub fn is_named_accessor_in_table(class_or_builtin_ty: &TyName, godot_method_name: &str) -> bool {

godot-macros/src/class/derive_godot_class.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,10 @@ pub fn derive_godot_class(item: venial::Item) -> ParseResult<TokenStream> {
6969
#[cfg(not(all(feature = "register-docs", since_api = "4.3")))]
7070
let docs = quote! {};
7171
let base_class = quote! { ::godot::classes::#base_ty };
72-
let inherits_macro = format_ident!("unsafe_inherits_transitive_{}", base_ty);
72+
73+
// Use this name because when typing a non-existent class, users will be met with the following error:
74+
// could not find `inherit_from_OS__ensure_class_exists` in `class_macros`.
75+
let inherits_macro_ident = format_ident!("inherit_from_{}__ensure_class_exists", base_ty);
7376

7477
let prv = quote! { ::godot::private };
7578
let godot_exports_impl = make_property_impl(class_name, &fields);
@@ -186,7 +189,7 @@ pub fn derive_godot_class(item: venial::Item) -> ParseResult<TokenStream> {
186189
)
187190
));
188191

189-
#prv::class_macros::#inherits_macro!(#class_name);
192+
#prv::class_macros::#inherits_macro_ident!(#class_name);
190193
})
191194
}
192195

0 commit comments

Comments
 (0)