Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions godot-codegen/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ api-custom = ["godot-bindings/api-custom"]
api-custom-json = ["godot-bindings/api-custom-json"]
experimental-godot-api = []
experimental-threads = []
experimental-required-objs = []

[dependencies]
godot-bindings = { path = "../godot-bindings", version = "=0.4.1" }
Expand Down
33 changes: 27 additions & 6 deletions godot-codegen/src/conv/type_conversions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,11 @@ fn to_hardcoded_rust_ident(full_ty: &GodotTy) -> Option<&str> {
("real_t", None) => "real",
("void", None) => "c_void",

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

_ => return None,
};
Expand Down Expand Up @@ -244,13 +248,30 @@ fn to_rust_type_uncached(full_ty: &GodotTy, ctx: &mut Context) -> RustTy {
arg_passing: ctx.get_builtin_arg_passing(full_ty),
}
} else {
let ty = rustify_ty(ty);
let qualified_class = quote! { crate::classes::#ty };
let is_nullable = if cfg!(feature = "experimental-required-objs") {
full_ty.meta.as_ref().is_none_or(|m| m != "required")
} else {
true
};

let inner_class = rustify_ty(ty);
let qualified_class = quote! { crate::classes::#inner_class };

// Stores unwrapped Gd<T> directly in `gd_tokens`.
let gd_tokens = quote! { Gd<#qualified_class> };

// Use Option for `impl_as_object_arg` if nullable.
let impl_as_object_arg = if is_nullable {
quote! { impl AsArg<Option<Gd<#qualified_class>>> }
} else {
quote! { impl AsArg<Gd<#qualified_class>> }
};

RustTy::EngineClass {
tokens: quote! { Gd<#qualified_class> },
impl_as_object_arg: quote! { impl AsArg<Option<Gd<#qualified_class>>> },
inner_class: ty,
gd_tokens,
impl_as_object_arg,
inner_class,
is_nullable,
}
}
}
Expand Down
30 changes: 17 additions & 13 deletions godot-codegen/src/generator/functions_common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -446,23 +446,23 @@ pub(crate) fn make_param_or_field_type(
let mut special_ty = None;

let param_ty = match ty {
// Objects: impl AsArg<Gd<T>>
// Objects: impl AsArg<Gd<T>> or impl AsArg<Option<Gd<T>>>.
RustTy::EngineClass {
impl_as_object_arg,
inner_class,
..
impl_as_object_arg, ..
} => {
let lft = lifetimes.next();
special_ty = Some(quote! { CowArg<#lft, Option<Gd<crate::classes::#inner_class>>> });

// #ty is already Gd<...> or Option<Gd<...>> depending on nullability.
special_ty = Some(quote! { CowArg<#lft, #ty> });

match decl {
FnParamDecl::FnPublic => quote! { #impl_as_object_arg },
FnParamDecl::FnPublicLifetime => quote! { #impl_as_object_arg + 'a },
FnParamDecl::FnInternal => {
quote! { CowArg<Option<Gd<crate::classes::#inner_class>>> }
quote! { CowArg<#ty> }
}
FnParamDecl::Field => {
quote! { CowArg<'a, Option<Gd<crate::classes::#inner_class>>> }
quote! { CowArg<'a, #ty> }
}
}
}
Expand Down Expand Up @@ -615,16 +615,20 @@ pub(crate) fn make_virtual_param_type(
function_sig: &dyn Function,
) -> TokenStream {
match param_ty {
// Virtual methods accept Option<Gd<T>>, since we don't know whether objects are nullable or required.
RustTy::EngineClass { .. }
if !special_cases::is_class_method_param_required(
RustTy::EngineClass { gd_tokens, .. } => {
if special_cases::is_class_method_param_required(
function_sig.surrounding_class().unwrap(),
function_sig.godot_name(),
param_name,
) =>
{
quote! { Option<#param_ty> }
) {
// For special-cased EngineClass params, use Gd<T> without Option.
gd_tokens.clone()
} else {
// In general, virtual methods accept Option<Gd<T>>, since we don't know whether objects are nullable or required.
quote! { Option<#gd_tokens> }
}
}

_ => quote! { #param_ty },
}
}
Expand Down
5 changes: 3 additions & 2 deletions godot-codegen/src/generator/signals.rs
Original file line number Diff line number Diff line change
Expand Up @@ -291,9 +291,10 @@ impl SignalParams {
for param in params.iter() {
let param_name = safe_ident(&param.name.to_string());
let param_ty = &param.type_;
let param_ty_tokens = param_ty.tokens_non_null();

param_list.extend(quote! { #param_name: #param_ty, });
type_list.extend(quote! { #param_ty, });
param_list.extend(quote! { #param_name: #param_ty_tokens, });
type_list.extend(quote! { #param_ty_tokens, });
name_list.extend(quote! { #param_name, });

let formatted_ty = match param_ty {
Expand Down
11 changes: 11 additions & 0 deletions godot-codegen/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,17 @@ pub const IS_CODEGEN_FULL: bool = false;
#[cfg(feature = "codegen-full")]
pub const IS_CODEGEN_FULL: bool = true;

#[cfg(all(feature = "experimental-required-objs", before_api = "4.6"))]
fn __feature_warning() {
// Not a hard error, it's experimental anyway and allows more flexibility like this.
#[must_use = "The `experimental-required-objs` feature needs at least Godot 4.6-dev version"]
fn feature_has_no_effect() -> i32 {
1
}

feature_has_no_effect();
}

fn write_file(path: &Path, contents: String) {
let dir = path.parent().unwrap();
let _ = std::fs::create_dir_all(dir);
Expand Down
55 changes: 37 additions & 18 deletions godot-codegen/src/models/domain.rs
Original file line number Diff line number Diff line change
Expand Up @@ -676,15 +676,8 @@ impl FnReturn {

pub fn type_tokens(&self) -> TokenStream {
match &self.type_ {
Some(RustTy::EngineClass { tokens, .. }) => {
quote! { Option<#tokens> }
}
Some(ty) => {
quote! { #ty }
}
_ => {
quote! { () }
}
Some(ty) => ty.to_token_stream(),
_ => quote! { () },
}
}

Expand Down Expand Up @@ -726,6 +719,7 @@ pub struct GodotTy {
pub enum RustTy {
/// `bool`, `Vector3i`, `Array`, `GString`
BuiltinIdent { ty: Ident, arg_passing: ArgPassing },

/// Pointers declared in `gdextension_interface` such as `sys::GDExtensionInitializationFunction`
/// used as parameters in some APIs.
SysPointerType { tokens: TokenStream },
Expand Down Expand Up @@ -761,16 +755,19 @@ pub enum RustTy {

/// `Gd<Node>`
EngineClass {
/// Tokens with full `Gd<T>` (e.g. used in return type position).
tokens: TokenStream,
/// Tokens with full `Gd<T>`, never `Option<Gd<T>>`.
gd_tokens: TokenStream,

/// Signature declaration with `impl AsArg<Gd<T>>`.
/// Signature declaration with `impl AsArg<Gd<T>>` or `impl AsArg<Option<Gd<T>>>`.
impl_as_object_arg: TokenStream,

/// only inner `T`
#[allow(dead_code)]
// only read in minimal config + RustTy::default_extender_field_decl()
/// Only inner `Node`.
inner_class: Ident,

/// Whether this object parameter/return is nullable in the GDExtension API.
///
/// Defaults to true (nullable). Only false when meta="required".
is_nullable: bool,
},

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

pub fn return_decl(&self) -> TokenStream {
match self {
Self::EngineClass { tokens, .. } => quote! { -> Option<#tokens> },
Self::GenericArray => quote! { -> Array<Ret> },
other => quote! { -> #other },
_ => quote! { -> #self },
}
}

/// Returns tokens without `Option<T>` wrapper, even for nullable engine classes.
///
/// For `EngineClass`, always returns `Gd<T>` regardless of nullability. For other types, behaves the same as `ToTokens`.
// TODO(v0.5): only used for signal params, which is a bug. Those should conservatively be Option<Gd<T>> as well.
// Might also be useful to directly extract inner `gd_tokens` field.
pub fn tokens_non_null(&self) -> TokenStream {
match self {
Self::EngineClass { gd_tokens, .. } => gd_tokens.clone(),
other => other.to_token_stream(),
}
}

Expand Down Expand Up @@ -849,7 +857,18 @@ impl ToTokens for RustTy {
} => quote! { *mut #inner }.to_tokens(tokens),
RustTy::EngineArray { tokens: path, .. } => path.to_tokens(tokens),
RustTy::EngineEnum { tokens: path, .. } => path.to_tokens(tokens),
RustTy::EngineClass { tokens: path, .. } => path.to_tokens(tokens),
RustTy::EngineClass {
is_nullable,
gd_tokens: path,
..
} => {
// Return nullable-aware type: Option<Gd<T>> if nullable, else Gd<T>.
if *is_nullable {
quote! { Option<#path> }.to_tokens(tokens)
} else {
path.to_tokens(tokens)
}
}
RustTy::ExtenderReceiver { tokens: path } => path.to_tokens(tokens),
RustTy::GenericArray => quote! { Array<Ret> }.to_tokens(tokens),
RustTy::SysPointerType { tokens: path } => path.to_tokens(tokens),
Expand Down
2 changes: 2 additions & 0 deletions godot-codegen/src/models/json.rs
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,7 @@ pub struct JsonMethodArg {
pub name: String,
#[nserde(rename = "type")]
pub type_: String,
/// Extra information about the type (e.g. which integer). Value "required" indicates non-nullable class types (Godot 4.6+).
pub meta: Option<String>,
pub default_value: Option<String>,
}
Expand All @@ -247,6 +248,7 @@ pub struct JsonMethodArg {
pub struct JsonMethodReturn {
#[nserde(rename = "type")]
pub type_: String,
/// Extra information about the type (e.g. which integer). Value "required" indicates non-nullable class types (Godot 4.6+).
pub meta: Option<String>,
}

Expand Down
4 changes: 4 additions & 0 deletions godot-codegen/src/special_cases/special_cases.rs
Original file line number Diff line number Diff line change
Expand Up @@ -758,6 +758,10 @@ pub fn is_class_method_param_required(
godot_method_name: &str,
param: &Ident, // Don't use `&str` to avoid to_string() allocations for each check on call-site.
) -> bool {
// TODO(v0.5): this overlaps now slightly with Godot's own "required" meta in extension_api.json.
// Having an override list can always be useful, but possibly the two inputs (here + JSON) should be evaluated at the same time,
// during JSON->Domain mapping.

// Note: magically, it's enough if a base class method is declared here; it will be picked up by derived classes.

match (class_name.godot_ty.as_str(), godot_method_name) {
Expand Down
1 change: 1 addition & 0 deletions godot-core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ codegen-lazy-fptrs = [
double-precision = ["godot-codegen/double-precision"]
experimental-godot-api = ["godot-codegen/experimental-godot-api"]
experimental-threads = ["godot-ffi/experimental-threads", "godot-codegen/experimental-threads"]
experimental-required-objs = ["godot-codegen/experimental-required-objs"]
experimental-wasm-nothreads = ["godot-ffi/experimental-wasm-nothreads"]
debug-log = ["godot-ffi/debug-log"]
trace = []
Expand Down
1 change: 1 addition & 0 deletions godot/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ custom-json = ["api-custom-json"]
double-precision = ["godot-core/double-precision"]
experimental-godot-api = ["godot-core/experimental-godot-api"]
experimental-threads = ["godot-core/experimental-threads"]
experimental-required-objs = ["godot-core/experimental-required-objs"]
experimental-wasm = []
experimental-wasm-nothreads = ["godot-core/experimental-wasm-nothreads"]
codegen-rustfmt = ["godot-core/codegen-rustfmt"]
Expand Down
24 changes: 23 additions & 1 deletion godot/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,13 @@
//! * **`experimental-godot-api`**
//!
//! Access to `godot::classes` APIs that Godot marks "experimental". These are under heavy development and may change at any time.
//! If you opt in to this feature, expect breaking changes at compile and runtime.
//! If you opt in to this feature, expect breaking changes at compile and runtime.<br><br>
//!
//! * **`experimental-required-objs`**
//!
//! Enables _required_ objects in Godot function signatures. When GDExtension advertises parameters or return value as required (non-null), the
//! generated code will use `Gd<T>` instead of `Option<Gd<T>>` for type safety. This will undergo many breaking changes as the API evolves;
//! we are explicitly excluding this from any SemVer guarantees. Needs Godot 4.6-dev. See <https://github.com/godot-rust/gdext/pull/1383>.
//!
//! _Rust functionality toggles:_
//!
Expand Down Expand Up @@ -220,3 +226,19 @@ pub use godot_core::private;

/// Often-imported symbols.
pub mod prelude;

/// Tests for code that must not compile.
// Do not add #[cfg(test)], it seems to break `cargo test -p godot --features godot/api-custom,godot/experimental-required-objs`.
mod no_compile_tests {
/// ```compile_fail
/// use godot::prelude::*;
/// let mut node: Gd<Node> = todo!();
/// let option = Some(node.clone());
/// let option: Option<&Gd<Node>> = option.as_ref();
///
/// // Following must not compile since `add_child` accepts only required (non-null) arguments. Comment-out for sanity check.
/// node.add_child(option);
/// ```
#[cfg(feature = "experimental-required-objs")]
fn __test_invalid_patterns() {}
}
2 changes: 1 addition & 1 deletion itest/rust/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ crate-type = ["cdylib"]
# Default feature MUST be empty for workflow reasons, even if it differs from the default feature set in upstream `godot` crate.
default = []
codegen-full = ["godot/__codegen-full"]
codegen-full-experimental = ["codegen-full", "godot/experimental-godot-api"]
codegen-full-experimental = ["codegen-full", "godot/experimental-godot-api", "godot/experimental-required-objs"]
experimental-threads = ["godot/experimental-threads"]
register-docs = ["godot/register-docs"]
serde = ["dep:serde", "dep:serde_json", "godot/serde"]
Expand Down
24 changes: 24 additions & 0 deletions itest/rust/src/engine_tests/node_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -74,3 +74,27 @@ fn node_call_group(ctx: &TestContext) {
tree.call_group("group", "remove_meta", vslice!["something"]);
assert!(!node.has_meta("something"));
}

// Experimental required parameter/return value.
/* TODO(v0.5): enable once https://github.com/godot-rust/gdext/pull/1383 is merged.
#[cfg(all(feature = "codegen-full-experimental", since_api = "4.6"))]
#[itest]
fn node_required_param_return() {
use godot::classes::Tween;
use godot::obj::Gd;

let mut parent = Node::new_alloc();
let child = Node::new_alloc();

// add_child() takes required arg, so this still works.
// (Test for Option *not* working anymore is in godot > no_compile_tests.)
parent.add_child(&child);

// create_tween() returns now non-null instance.
let tween: Gd<Tween> = parent.create_tween();
assert!(tween.is_instance_valid());
assert!(tween.to_string().contains("Tween"));

parent.free();
}
*/
Loading