Skip to content

Commit 3a75a3b

Browse files
authored
Merge pull request #1383 from godot-rust/feature/required-object-apis
Experimental support for required parameters/returns in Godot APIs
2 parents 8ad4daf + 096af2e commit 3a75a3b

File tree

13 files changed

+152
-41
lines changed

13 files changed

+152
-41
lines changed

godot-codegen/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ api-custom = ["godot-bindings/api-custom"]
2020
api-custom-json = ["godot-bindings/api-custom-json"]
2121
experimental-godot-api = []
2222
experimental-threads = []
23+
experimental-required-objs = []
2324

2425
[dependencies]
2526
godot-bindings = { path = "../godot-bindings", version = "=0.4.1" }

godot-codegen/src/conv/type_conversions.rs

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,11 @@ fn to_hardcoded_rust_ident(full_ty: &GodotTy) -> Option<&str> {
6767
("real_t", None) => "real",
6868
("void", None) => "c_void",
6969

70-
(ty, Some(meta)) => panic!("unhandled type {ty:?} with meta {meta:?}"),
70+
// meta="required" is a special case of non-null object parameters/return types.
71+
// Other metas are unrecognized.
72+
(ty, Some(meta)) if meta != "required" => {
73+
panic!("unhandled type {ty:?} with meta {meta:?}")
74+
}
7175

7276
_ => return None,
7377
};
@@ -244,13 +248,30 @@ fn to_rust_type_uncached(full_ty: &GodotTy, ctx: &mut Context) -> RustTy {
244248
arg_passing: ctx.get_builtin_arg_passing(full_ty),
245249
}
246250
} else {
247-
let ty = rustify_ty(ty);
248-
let qualified_class = quote! { crate::classes::#ty };
251+
let is_nullable = if cfg!(feature = "experimental-required-objs") {
252+
full_ty.meta.as_ref().is_none_or(|m| m != "required")
253+
} else {
254+
true
255+
};
256+
257+
let inner_class = rustify_ty(ty);
258+
let qualified_class = quote! { crate::classes::#inner_class };
259+
260+
// Stores unwrapped Gd<T> directly in `gd_tokens`.
261+
let gd_tokens = quote! { Gd<#qualified_class> };
262+
263+
// Use Option for `impl_as_object_arg` if nullable.
264+
let impl_as_object_arg = if is_nullable {
265+
quote! { impl AsArg<Option<Gd<#qualified_class>>> }
266+
} else {
267+
quote! { impl AsArg<Gd<#qualified_class>> }
268+
};
249269

250270
RustTy::EngineClass {
251-
tokens: quote! { Gd<#qualified_class> },
252-
impl_as_object_arg: quote! { impl AsArg<Option<Gd<#qualified_class>>> },
253-
inner_class: ty,
271+
gd_tokens,
272+
impl_as_object_arg,
273+
inner_class,
274+
is_nullable,
254275
}
255276
}
256277
}

godot-codegen/src/generator/functions_common.rs

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -446,23 +446,23 @@ pub(crate) fn make_param_or_field_type(
446446
let mut special_ty = None;
447447

448448
let param_ty = match ty {
449-
// Objects: impl AsArg<Gd<T>>
449+
// Objects: impl AsArg<Gd<T>> or impl AsArg<Option<Gd<T>>>.
450450
RustTy::EngineClass {
451-
impl_as_object_arg,
452-
inner_class,
453-
..
451+
impl_as_object_arg, ..
454452
} => {
455453
let lft = lifetimes.next();
456-
special_ty = Some(quote! { CowArg<#lft, Option<Gd<crate::classes::#inner_class>>> });
454+
455+
// #ty is already Gd<...> or Option<Gd<...>> depending on nullability.
456+
special_ty = Some(quote! { CowArg<#lft, #ty> });
457457

458458
match decl {
459459
FnParamDecl::FnPublic => quote! { #impl_as_object_arg },
460460
FnParamDecl::FnPublicLifetime => quote! { #impl_as_object_arg + 'a },
461461
FnParamDecl::FnInternal => {
462-
quote! { CowArg<Option<Gd<crate::classes::#inner_class>>> }
462+
quote! { CowArg<#ty> }
463463
}
464464
FnParamDecl::Field => {
465-
quote! { CowArg<'a, Option<Gd<crate::classes::#inner_class>>> }
465+
quote! { CowArg<'a, #ty> }
466466
}
467467
}
468468
}
@@ -615,16 +615,20 @@ pub(crate) fn make_virtual_param_type(
615615
function_sig: &dyn Function,
616616
) -> TokenStream {
617617
match param_ty {
618-
// Virtual methods accept Option<Gd<T>>, since we don't know whether objects are nullable or required.
619-
RustTy::EngineClass { .. }
620-
if !special_cases::is_class_method_param_required(
618+
RustTy::EngineClass { gd_tokens, .. } => {
619+
if special_cases::is_class_method_param_required(
621620
function_sig.surrounding_class().unwrap(),
622621
function_sig.godot_name(),
623622
param_name,
624-
) =>
625-
{
626-
quote! { Option<#param_ty> }
623+
) {
624+
// For special-cased EngineClass params, use Gd<T> without Option.
625+
gd_tokens.clone()
626+
} else {
627+
// In general, virtual methods accept Option<Gd<T>>, since we don't know whether objects are nullable or required.
628+
quote! { Option<#gd_tokens> }
629+
}
627630
}
631+
628632
_ => quote! { #param_ty },
629633
}
630634
}

godot-codegen/src/generator/signals.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -291,9 +291,10 @@ impl SignalParams {
291291
for param in params.iter() {
292292
let param_name = safe_ident(&param.name.to_string());
293293
let param_ty = &param.type_;
294+
let param_ty_tokens = param_ty.tokens_non_null();
294295

295-
param_list.extend(quote! { #param_name: #param_ty, });
296-
type_list.extend(quote! { #param_ty, });
296+
param_list.extend(quote! { #param_name: #param_ty_tokens, });
297+
type_list.extend(quote! { #param_ty_tokens, });
297298
name_list.extend(quote! { #param_name, });
298299

299300
let formatted_ty = match param_ty {

godot-codegen/src/lib.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,17 @@ pub const IS_CODEGEN_FULL: bool = false;
5151
#[cfg(feature = "codegen-full")]
5252
pub const IS_CODEGEN_FULL: bool = true;
5353

54+
#[cfg(all(feature = "experimental-required-objs", before_api = "4.6"))]
55+
fn __feature_warning() {
56+
// Not a hard error, it's experimental anyway and allows more flexibility like this.
57+
#[must_use = "The `experimental-required-objs` feature needs at least Godot 4.6-dev version"]
58+
fn feature_has_no_effect() -> i32 {
59+
1
60+
}
61+
62+
feature_has_no_effect();
63+
}
64+
5465
fn write_file(path: &Path, contents: String) {
5566
let dir = path.parent().unwrap();
5667
let _ = std::fs::create_dir_all(dir);

godot-codegen/src/models/domain.rs

Lines changed: 37 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -676,15 +676,8 @@ impl FnReturn {
676676

677677
pub fn type_tokens(&self) -> TokenStream {
678678
match &self.type_ {
679-
Some(RustTy::EngineClass { tokens, .. }) => {
680-
quote! { Option<#tokens> }
681-
}
682-
Some(ty) => {
683-
quote! { #ty }
684-
}
685-
_ => {
686-
quote! { () }
687-
}
679+
Some(ty) => ty.to_token_stream(),
680+
_ => quote! { () },
688681
}
689682
}
690683

@@ -726,6 +719,7 @@ pub struct GodotTy {
726719
pub enum RustTy {
727720
/// `bool`, `Vector3i`, `Array`, `GString`
728721
BuiltinIdent { ty: Ident, arg_passing: ArgPassing },
722+
729723
/// Pointers declared in `gdextension_interface` such as `sys::GDExtensionInitializationFunction`
730724
/// used as parameters in some APIs.
731725
SysPointerType { tokens: TokenStream },
@@ -761,16 +755,19 @@ pub enum RustTy {
761755

762756
/// `Gd<Node>`
763757
EngineClass {
764-
/// Tokens with full `Gd<T>` (e.g. used in return type position).
765-
tokens: TokenStream,
758+
/// Tokens with full `Gd<T>`, never `Option<Gd<T>>`.
759+
gd_tokens: TokenStream,
766760

767-
/// Signature declaration with `impl AsArg<Gd<T>>`.
761+
/// Signature declaration with `impl AsArg<Gd<T>>` or `impl AsArg<Option<Gd<T>>>`.
768762
impl_as_object_arg: TokenStream,
769763

770-
/// only inner `T`
771-
#[allow(dead_code)]
772-
// only read in minimal config + RustTy::default_extender_field_decl()
764+
/// Only inner `Node`.
773765
inner_class: Ident,
766+
767+
/// Whether this object parameter/return is nullable in the GDExtension API.
768+
///
769+
/// Defaults to true (nullable). Only false when meta="required".
770+
is_nullable: bool,
774771
},
775772

776773
/// Receiver type of default parameters extender constructor.
@@ -789,9 +786,20 @@ impl RustTy {
789786

790787
pub fn return_decl(&self) -> TokenStream {
791788
match self {
792-
Self::EngineClass { tokens, .. } => quote! { -> Option<#tokens> },
793789
Self::GenericArray => quote! { -> Array<Ret> },
794-
other => quote! { -> #other },
790+
_ => quote! { -> #self },
791+
}
792+
}
793+
794+
/// Returns tokens without `Option<T>` wrapper, even for nullable engine classes.
795+
///
796+
/// For `EngineClass`, always returns `Gd<T>` regardless of nullability. For other types, behaves the same as `ToTokens`.
797+
// TODO(v0.5): only used for signal params, which is a bug. Those should conservatively be Option<Gd<T>> as well.
798+
// Might also be useful to directly extract inner `gd_tokens` field.
799+
pub fn tokens_non_null(&self) -> TokenStream {
800+
match self {
801+
Self::EngineClass { gd_tokens, .. } => gd_tokens.clone(),
802+
other => other.to_token_stream(),
795803
}
796804
}
797805

@@ -849,7 +857,18 @@ impl ToTokens for RustTy {
849857
} => quote! { *mut #inner }.to_tokens(tokens),
850858
RustTy::EngineArray { tokens: path, .. } => path.to_tokens(tokens),
851859
RustTy::EngineEnum { tokens: path, .. } => path.to_tokens(tokens),
852-
RustTy::EngineClass { tokens: path, .. } => path.to_tokens(tokens),
860+
RustTy::EngineClass {
861+
is_nullable,
862+
gd_tokens: path,
863+
..
864+
} => {
865+
// Return nullable-aware type: Option<Gd<T>> if nullable, else Gd<T>.
866+
if *is_nullable {
867+
quote! { Option<#path> }.to_tokens(tokens)
868+
} else {
869+
path.to_tokens(tokens)
870+
}
871+
}
853872
RustTy::ExtenderReceiver { tokens: path } => path.to_tokens(tokens),
854873
RustTy::GenericArray => quote! { Array<Ret> }.to_tokens(tokens),
855874
RustTy::SysPointerType { tokens: path } => path.to_tokens(tokens),

godot-codegen/src/models/json.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,7 @@ pub struct JsonMethodArg {
238238
pub name: String,
239239
#[nserde(rename = "type")]
240240
pub type_: String,
241+
/// Extra information about the type (e.g. which integer). Value "required" indicates non-nullable class types (Godot 4.6+).
241242
pub meta: Option<String>,
242243
pub default_value: Option<String>,
243244
}
@@ -247,6 +248,7 @@ pub struct JsonMethodArg {
247248
pub struct JsonMethodReturn {
248249
#[nserde(rename = "type")]
249250
pub type_: String,
251+
/// Extra information about the type (e.g. which integer). Value "required" indicates non-nullable class types (Godot 4.6+).
250252
pub meta: Option<String>,
251253
}
252254

godot-codegen/src/special_cases/special_cases.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -758,6 +758,10 @@ pub fn is_class_method_param_required(
758758
godot_method_name: &str,
759759
param: &Ident, // Don't use `&str` to avoid to_string() allocations for each check on call-site.
760760
) -> bool {
761+
// TODO(v0.5): this overlaps now slightly with Godot's own "required" meta in extension_api.json.
762+
// Having an override list can always be useful, but possibly the two inputs (here + JSON) should be evaluated at the same time,
763+
// during JSON->Domain mapping.
764+
761765
// Note: magically, it's enough if a base class method is declared here; it will be picked up by derived classes.
762766

763767
match (class_name.godot_ty.as_str(), godot_method_name) {

godot-core/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ codegen-lazy-fptrs = [
2222
double-precision = ["godot-codegen/double-precision"]
2323
experimental-godot-api = ["godot-codegen/experimental-godot-api"]
2424
experimental-threads = ["godot-ffi/experimental-threads", "godot-codegen/experimental-threads"]
25+
experimental-required-objs = ["godot-codegen/experimental-required-objs"]
2526
experimental-wasm-nothreads = ["godot-ffi/experimental-wasm-nothreads"]
2627
debug-log = ["godot-ffi/debug-log"]
2728
trace = []

godot/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ custom-json = ["api-custom-json"]
1919
double-precision = ["godot-core/double-precision"]
2020
experimental-godot-api = ["godot-core/experimental-godot-api"]
2121
experimental-threads = ["godot-core/experimental-threads"]
22+
experimental-required-objs = ["godot-core/experimental-required-objs"]
2223
experimental-wasm = []
2324
experimental-wasm-nothreads = ["godot-core/experimental-wasm-nothreads"]
2425
codegen-rustfmt = ["godot-core/codegen-rustfmt"]

0 commit comments

Comments
 (0)