Skip to content

Commit adfd7fb

Browse files
committed
Mark some classes as "final" and prevent inheritance
Includes singletons as well as certain classes marked "abstract" in Godot (de-facto final).
1 parent 2c9c960 commit adfd7fb

File tree

7 files changed

+103
-32
lines changed

7 files changed

+103
-32
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: 73 additions & 27 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,7 +69,7 @@ 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

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

131131
let enums = enums::make_enums(&class.enums, &cfg_attributes);
132132
let constants = constants::make_constants(&class.constants);
133-
let inherits_macro = format_ident!("unsafe_inherits_transitive_{}", class_name.rust_ty);
134133
let deref_impl = make_deref_impl(class_name, &base_ty);
135134

136135
let all_bases = ctx.inheritance_tree().collect_all_bases(class_name);
136+
let (inherits_macro_ident, inherits_macro_code) = make_inherits_macro(class, &all_bases);
137137
let (notification_enum, notification_enum_name) =
138138
notifications::make_notification_enum(class_name, &all_bases, &cfg_attributes, ctx);
139139

@@ -180,11 +180,6 @@ fn make_class(class: &Class, ctx: &mut Context, view: &ApiView) -> GeneratedClas
180180
}
181181
};
182182

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-
188183
// mod re_export needed, because class should not appear inside the file module, and we can't re-export private struct as pub.
189184
let imports = util::make_imports();
190185
let tokens = quote! {
@@ -247,20 +242,7 @@ fn make_class(class: &Class, ctx: &mut Context, view: &ApiView) -> GeneratedClas
247242

248243
#godot_default_impl
249244
#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-
}
245+
#inherits_macro_code
264246
}
265247

266248
#builders
@@ -275,11 +257,69 @@ fn make_class(class: &Class, ctx: &mut Context, view: &ApiView) -> GeneratedClas
275257
name: notification_enum_name,
276258
declared_by_own_class: notification_enum.is_some(),
277259
},
278-
inherits_macro_ident: inherits_macro,
260+
inherits_macro_ident,
279261
has_sidecar_module,
280262
}
281263
}
282264

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

361+
// For final classes, do nothing.
362+
let Some(inherits_macro_ident) = inherits_macro_ident else {
363+
return TokenStream::new();
364+
};
365+
321366
// We cannot re-export the following, because macro is in the crate root
322367
// pub use #module_ident::re_export::#inherits_macro_ident;
323368
quote! {
@@ -345,13 +390,14 @@ fn make_constructor_and_default(
345390
class: &Class,
346391
ctx: &Context,
347392
) -> (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.
393+
let class_name = class.name();
394+
395+
let godot_class_stringname = make_string_name(&class_name.godot_ty);
396+
// Note: this could use class_name() but is not yet done due to potential future lazy-load refactoring.
351397
//let class_name_obj = quote! { <Self as crate::obj::GodotClass>::class_name() };
352398

353399
let (constructor, construct_doc, has_godot_default_impl);
354-
if ctx.is_singleton(godot_class_name) {
400+
if ctx.is_singleton(class_name) {
355401
// Note: we cannot return &'static mut Self, as this would be very easy to mutably alias.
356402
// &'static Self would be possible, but we would lose the whole mutability information (even if that is best-effort and
357403
// not strict Rust mutability, it makes the API much more usable).

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: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ impl Class {
9393
let is_experimental = special_cases::is_class_experimental(&ty_name.godot_ty);
9494
let is_instantiable =
9595
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);
9697

9798
let mod_name = ModName::from_godot(&ty_name.godot_ty);
9899

@@ -133,6 +134,7 @@ impl Class {
133134
is_refcounted: json.is_refcounted,
134135
is_instantiable,
135136
is_experimental,
137+
is_final,
136138
inherits: json.inherits.clone(),
137139
api_level: get_api_level(json),
138140
constants,

godot-codegen/src/special_cases/special_cases.rs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,24 @@ pub fn is_class_instantiable(class_ty: &TyName) -> Option<bool> {
224224
None
225225
}
226226

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+
227245
/// Whether a method is available in the method table as a named accessor.
228246
#[rustfmt::skip]
229247
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)