Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
e420f5f
Provide error context for typed array clone check
mivort Sep 30, 2025
fe94254
Fix secondary api blocks not registering docs.
Houtamelo Oct 3, 2025
663535e
Add main loop callbacks to ExtensionLibrary
beicause Oct 8, 2025
7187618
Codegen: Support sys types in outgoing Ptrcalls.
Yarwin Oct 10, 2025
ce2cf79
Merge pull request #1363 from Yarwin/support-sys-types-in-codegen
Bromeon Oct 12, 2025
3db89e8
Merge pull request #1313 from beicause/main-loop-callbacks
Bromeon Oct 12, 2025
88de7c8
Codegen for `Array<T>` - required to properly initialize and cache Ar…
Yarwin Oct 6, 2025
66e74c9
Remove `func_general_lifetime` from FnParamTokens since it was never …
Yarwin Oct 6, 2025
e0d30f9
Merge pull request #1357 from Yarwin/fix-wrongly-assumed-array-type
Yarwin Oct 12, 2025
2dde78e
Merge pull request #1348 from mivort/clone-validation-more-context
Bromeon Oct 13, 2025
ae07e11
Doc bugfixes and improvements
Yarwin Oct 4, 2025
245ef3a
Merge pull request #1355 from Yarwin/fix-docs-in-godot-api-secondary
Bromeon Oct 13, 2025
f08c483
Retain spans in newly generated TokenStreams when possible
Bromeon Jul 20, 2025
fff255c
Move virtual definitions: sys::godot_virtual_consts -> private::virtuals
Bromeon Jul 20, 2025
854897b
Declare virtual signatures in central place
Bromeon Jul 20, 2025
e3f67f6
Store signature of virtual methods
Bromeon Oct 17, 2025
a5d6428
Merge pull request #1370 from godot-rust/qol/spans
Bromeon Oct 19, 2025
ee2b1fa
Add rustfmt to clippy job in CI to format files generated by itest.
Yarwin Oct 21, 2025
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
3 changes: 2 additions & 1 deletion .github/workflows/full-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,8 @@ jobs:
- name: "Install Rust"
uses: ./.github/composite/rust
with:
components: clippy
# Rustfmt is needed to format generated code in itest.
components: clippy,rustfmt

# Note: could use `-- --no-deps` to not lint dependencies, however it doesn't really speed up and also skips deps in workspace.
- name: "Check clippy"
Expand Down
3 changes: 2 additions & 1 deletion .github/workflows/minimal-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,8 @@ jobs:
- name: "Install Rust"
uses: ./.github/composite/rust
with:
components: clippy
# Rustfmt is needed to format generated code in itest.
components: clippy,rustfmt

- name: "Check clippy"
run: |
Expand Down
18 changes: 18 additions & 0 deletions godot-codegen/src/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ use quote::{format_ident, ToTokens};

use crate::generator::method_tables::MethodTableKey;
use crate::generator::notifications;
use crate::generator::sys::SYS_PARAMS;
use crate::models::domain::{ArgPassing, GodotTy, RustTy, TyName};
use crate::models::json::{
JsonBuiltinClass, JsonBuiltinMethod, JsonClass, JsonClassConstant, JsonClassMethod,
Expand All @@ -28,6 +29,9 @@ pub struct Context<'a> {
/// Which interface traits are generated (`false` for "Godot-abstract"/final classes).
classes_final: HashMap<TyName, bool>,
cached_rust_types: HashMap<GodotTy, RustTy>,
/// Various pointers defined in `gdextension_interface`, for example `GDExtensionInitializationFunction`.
/// Used in some APIs that are not exposed to GDScript.
sys_types: HashSet<&'a str>,
notifications_by_class: HashMap<TyName, Vec<(Ident, i32)>>,
classes_with_signals: HashSet<TyName>,
notification_enum_names_by_class: HashMap<TyName, NotificationEnum>,
Expand All @@ -43,6 +47,8 @@ impl<'a> Context<'a> {
ctx.singletons.insert(class.name.as_str());
}

Self::populate_sys_types(&mut ctx);

ctx.builtin_types.insert("Variant"); // not part of builtin_classes
for builtin in api.builtin_classes.iter() {
let ty_name = builtin.name.as_str();
Expand Down Expand Up @@ -152,6 +158,14 @@ impl<'a> Context<'a> {
ctx
}

/// Adds Godot pointer types to [`Context`].
///
/// Godot pointer types, for example `GDExtensionInitializationFunction`, are defined in `gdextension_interface`
/// but aren't described in `extension_api.json` – despite being used as parameters in various APIs.
fn populate_sys_types(ctx: &mut Context) {
ctx.sys_types.extend(SYS_PARAMS.iter().map(|p| p.type_()));
}

fn populate_notification_constants(
class_name: &TyName,
constants: &[JsonClassConstant],
Expand Down Expand Up @@ -293,6 +307,10 @@ impl<'a> Context<'a> {
self.native_structures_types.contains(ty_name)
}

pub fn is_sys(&self, ty_name: &str) -> bool {
self.sys_types.contains(ty_name)
}

pub fn is_singleton(&self, class_name: &TyName) -> bool {
self.singletons.contains(class_name.godot_ty.as_str())
}
Expand Down
9 changes: 9 additions & 0 deletions godot-codegen/src/conv/type_conversions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,15 @@ fn to_rust_type_uncached(full_ty: &GodotTy, ctx: &mut Context) -> RustTy {

// .trim() is necessary here, as Godot places a space between a type and the stars when representing a double pointer.
// Example: "int*" but "int **".
if ctx.is_sys(ty.trim()) {
let ty = rustify_ty(&ty);
return RustTy::RawPointer {
inner: Box::new(RustTy::SysIdent {
tokens: quote! { sys::#ty },
}),
is_const,
};
}
let inner_type = to_rust_type(ty.trim(), None, ctx);
return RustTy::RawPointer {
inner: Box::new(inner_type),
Expand Down
33 changes: 24 additions & 9 deletions godot-codegen/src/generator/builtins.rs
Original file line number Diff line number Diff line change
Expand Up @@ -197,14 +197,27 @@ fn make_special_builtin_methods(class_name: &TyName, _ctx: &Context) -> TokenStr

/// Get the safety docs of an unsafe method, or `None` if it is safe.
fn method_safety_doc(class_name: &TyName, method: &BuiltinMethod) -> Option<TokenStream> {
if class_name.godot_ty == "Array"
&& &method.return_value().type_tokens().to_string() == "VariantArray"
{
return Some(quote! {
/// # Safety
///
/// You must ensure that the returned array fulfils the safety invariants of [`Array`](crate::builtin::Array).
});
if class_name.godot_ty == "Array" {
if method.is_generic() {
return Some(quote! {
/// # Safety
/// You must ensure that the returned array fulfils the safety invariants of [`Array`](crate::builtin::Array), this being:
/// - Any values written to the array must match the runtime type of the array.
/// - Any values read from the array must be convertible to the type `T`.
///
/// If the safety invariant of `Array` is intact, which it must be for any publicly accessible arrays, then `T` must match
/// the runtime type of the array. This then implies that both of the conditions above hold. This means that you only need
/// to keep the above conditions in mind if you are intentionally violating the safety invariant of `Array`.
///
/// In the current implementation, both cases will produce a panic rather than undefined behavior, but this should not be relied upon.
});
} else if &method.return_value().type_tokens().to_string() == "VariantArray" {
return Some(quote! {
/// # Safety
///
/// You must ensure that the returned array fulfils the safety invariants of [`Array`](crate::builtin::Array).
});
}
}

None
Expand Down Expand Up @@ -252,11 +265,13 @@ fn make_builtin_method_definition(
let receiver = functions_common::make_receiver(method.qualifier(), ffi_arg_in);
let object_ptr = &receiver.ffi_arg;

let maybe_generic_params = method.return_value().generic_params();

let ptrcall_invocation = quote! {
let method_bind = sys::builtin_method_table().#fptr_access;


Signature::<CallParams, CallRet>::out_builtin_ptrcall(
Signature::<CallParams, CallRet #maybe_generic_params>::out_builtin_ptrcall(
method_bind,
#builtin_name_str,
#method_name_str,
Expand Down
4 changes: 4 additions & 0 deletions godot-codegen/src/generator/central_files.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ use quote::{format_ident, quote, ToTokens};

use crate::context::Context;
use crate::conv;
use crate::generator::sys::make_godotconvert_for_systypes;
use crate::generator::{enums, gdext_build_struct};
use crate::models::domain::ExtensionApi;
use crate::util::ident;
Expand Down Expand Up @@ -60,6 +61,7 @@ pub fn make_core_central_code(api: &ExtensionApi, ctx: &mut Context) -> TokenStr

let (global_enum_defs, global_reexported_enum_defs) = make_global_enums(api);
let variant_type_traits = make_variant_type_enum(api, false);
let sys_types_godotconvert_impl = make_godotconvert_for_systypes();

// TODO impl Clone, Debug, PartialEq, PartialOrd, Hash for VariantDispatch
// TODO could use try_to().unwrap_unchecked(), since type is already verified. Also directly overload from_variant().
Expand Down Expand Up @@ -121,6 +123,8 @@ pub fn make_core_central_code(api: &ExtensionApi, ctx: &mut Context) -> TokenStr
use crate::sys;
#( #global_reexported_enum_defs )*
}

#( #sys_types_godotconvert_impl )*
}
}

Expand Down
13 changes: 2 additions & 11 deletions godot-codegen/src/generator/default_parameters.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ use quote::{format_ident, quote};

use crate::generator::functions_common;
use crate::generator::functions_common::{
make_arg_expr, make_param_or_field_type, FnArgExpr, FnCode, FnKind, FnParamDecl, FnParamTokens,
make_arg_expr, make_param_or_field_type, FnArgExpr, FnCode, FnKind, FnParamDecl,
};
use crate::models::domain::{FnParam, FnQualifier, Function, RustTy, TyName};
use crate::util::{ident, safe_ident};
Expand Down Expand Up @@ -59,15 +59,6 @@ pub fn make_function_definition_with_defaults(
&default_fn_params,
);

// ExBuilder::new() constructor signature.
let FnParamTokens {
func_general_lifetime: simple_fn_lifetime,
..
} = fns::make_params_exprs(
required_fn_params.iter().cloned(),
FnKind::ExBuilderConstructor,
);

let return_decl = &sig.return_value().decl;

// If either the builder has a lifetime (non-static/global method), or one of its parameters is a reference,
Expand Down Expand Up @@ -119,7 +110,7 @@ pub fn make_function_definition_with_defaults(
// Lifetime is set if any parameter is a reference.
#[doc = #default_parameter_usage]
#[inline]
#vis fn #simple_fn_name #simple_fn_lifetime (
#vis fn #simple_fn_name (
#simple_receiver_param
#( #class_method_required_params, )*
) #return_decl {
Expand Down
75 changes: 42 additions & 33 deletions godot-codegen/src/generator/functions_common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,6 @@ pub struct FnParamTokens {
/// Generic argument list `<'a0, 'a1, ...>` after `type CallSig`, if available.
pub callsig_lifetime_args: Option<TokenStream>,
pub arg_exprs: Vec<TokenStream>,
pub func_general_lifetime: Option<TokenStream>,
}

pub fn make_function_definition(
Expand Down Expand Up @@ -142,7 +141,6 @@ pub fn make_function_definition(
callsig_param_types: param_types,
callsig_lifetime_args,
arg_exprs: arg_names,
func_general_lifetime: fn_lifetime,
} = if sig.is_virtual() {
make_params_exprs_virtual(sig.params().iter(), sig)
} else {
Expand Down Expand Up @@ -175,11 +173,14 @@ pub fn make_function_definition(
default_structs_code = TokenStream::new();
};

let maybe_func_generic_params = sig.return_value().generic_params();
let maybe_func_generic_bounds = sig.return_value().where_clause();

let call_sig_decl = {
let return_ty = &sig.return_value().type_tokens();

quote! {
type CallRet = #return_ty;
type CallRet #maybe_func_generic_params = #return_ty;
type CallParams #callsig_lifetime_args = (#(#param_types,)*);
}
};
Expand Down Expand Up @@ -279,10 +280,12 @@ pub fn make_function_definition(

quote! {
#maybe_safety_doc
#vis #maybe_unsafe fn #primary_fn_name #fn_lifetime (
#vis #maybe_unsafe fn #primary_fn_name #maybe_func_generic_params (
#receiver_param
#( #params, )*
) #return_decl {
) #return_decl
#maybe_func_generic_bounds
{
#call_sig_decl

let args = (#( #arg_names, )*);
Expand Down Expand Up @@ -357,10 +360,7 @@ pub(crate) enum FnKind {
/// `call()` forwarding to `try_call()`.
DelegateTry,

/// Default extender `new()` associated function -- optional receiver and required parameters.
ExBuilderConstructor,

/// Same as [`ExBuilderConstructor`], but for a builder with an explicit lifetime.
/// Default extender `new()` associated function -- optional receiver and required parameters. Has an explicit lifetime.
ExBuilderConstructorLifetimed,

/// Default extender `new()` associated function -- only default parameters.
Expand Down Expand Up @@ -489,6 +489,7 @@ pub(crate) fn make_param_or_field_type(
..
}
| RustTy::BuiltinArray { .. }
| RustTy::GenericArray
| RustTy::EngineArray { .. } => {
let lft = lifetimes.next();
special_ty = Some(quote! { RefArg<#lft, #ty> });
Expand Down Expand Up @@ -572,7 +573,6 @@ pub(crate) fn make_params_exprs<'a>(
// Methods relevant in the context of default parameters. Flow in this order.
// Note that for builder methods of Ex* structs, there's a direct call in default_parameters.rs to the parameter manipulation methods,
// bypassing this method. So one case is missing here.
FnKind::ExBuilderConstructor => (FnParamDecl::FnPublic, FnArgExpr::StoreInField),
FnKind::ExBuilderConstructorLifetimed => {
(FnParamDecl::FnPublicLifetime, FnArgExpr::StoreInField)
}
Expand Down Expand Up @@ -603,6 +603,32 @@ pub(crate) fn make_params_exprs<'a>(
ret
}

/// Returns the type for a virtual method parameter.
///
/// Generates `Option<Gd<T>>` instead of `Gd<T>` for object parameters (which are currently all nullable).
///
/// Used for consistency between virtual trait definitions and `type Sig = ...` type-safety declarations
/// (which are used to improve compile-time errors on mismatch).
pub(crate) fn make_virtual_param_type(
param_ty: &RustTy,
param_name: &Ident,
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(
function_sig.surrounding_class().unwrap(),
function_sig.godot_name(),
param_name,
) =>
{
quote! { Option<#param_ty> }
}
_ => quote! { #param_ty },
}
}

/// For virtual functions, returns the parameter declarations, type tokens, and names.
pub(crate) fn make_params_exprs_virtual<'a>(
method_args: impl Iterator<Item = &'a FnParam>,
Expand All @@ -614,30 +640,13 @@ pub(crate) fn make_params_exprs_virtual<'a>(
let param_name = &param.name;
let param_ty = &param.type_;

match &param.type_ {
// 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(
function_sig.surrounding_class().unwrap(),
function_sig.godot_name(),
param_name,
) =>
{
ret.param_decls
.push(quote! { #param_name: Option<#param_ty> });
ret.arg_exprs.push(quote! { #param_name });
ret.callsig_param_types.push(quote! { #param_ty });
}
// Map parameter types (e.g. virtual functions need Option<Gd> instead of Gd).
let param_ty_tokens = make_virtual_param_type(param_ty, param_name, function_sig);

// All other methods and parameter types: standard handling.
// For now, virtual methods always receive their parameter by value.
//_ => ret.push_regular(param_name, param_ty, true, false, false),
_ => {
ret.param_decls.push(quote! { #param_name: #param_ty });
ret.arg_exprs.push(quote! { #param_name });
ret.callsig_param_types.push(quote! { #param_ty });
}
}
ret.param_decls
.push(quote! { #param_name: #param_ty_tokens });
ret.arg_exprs.push(quote! { #param_name });
ret.callsig_param_types.push(quote! { #param_ty });
}

ret
Expand Down
11 changes: 3 additions & 8 deletions godot-codegen/src/generator/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,9 @@ pub mod method_tables;
pub mod native_structures;
pub mod notifications;
pub mod signals;
pub mod sys;
pub mod utility_functions;
pub mod virtual_definition_consts;
pub mod virtual_definitions;
pub mod virtual_traits;

// ----------------------------------------------------------------------------------------------------------------------------------------------
Expand All @@ -56,7 +57,6 @@ pub fn generate_sys_module_file(sys_gen_path: &Path, submit_fn: &mut SubmitFn) {
pub mod central;
pub mod gdextension_interface;
pub mod interface;
pub mod virtual_consts;
};

submit_fn(sys_gen_path.join("mod.rs"), code);
Expand Down Expand Up @@ -86,12 +86,6 @@ pub fn generate_sys_classes_file(
submit_fn(sys_gen_path.join(filename), code);
watch.record(format!("generate_classes_{}_file", api_level.lower()));
}

// From 4.4 onward, generate table that maps all virtual methods to their known hashes.
// This allows Godot to fall back to an older compatibility function if one is not supported.
let code = virtual_definition_consts::make_virtual_consts_file(api, ctx);
submit_fn(sys_gen_path.join("virtual_consts.rs"), code);
watch.record("generate_virtual_consts_file");
}

pub fn generate_sys_utilities_file(
Expand Down Expand Up @@ -131,6 +125,7 @@ pub fn generate_core_mod_file(gen_path: &Path, submit_fn: &mut SubmitFn) {
pub mod builtin_classes;
pub mod utilities;
pub mod native;
pub mod virtuals;
};

submit_fn(gen_path.join("mod.rs"), code);
Expand Down
Loading
Loading