Skip to content

Conversation

Yarwin
Copy link
Contributor

@Yarwin Yarwin commented Oct 6, 2025

What Problem does this PR solve?

One mentioned in #1348. Long story short, we are responsible for initializing a return value from function; instead of initializing Array<T> we were initializing VariantArray and cached its type as untyped. Oopsie.

In other words, this code would panic:

#[itest]
fn array_inner_type() {
    let primary = Array::<Dictionary>::new();
    let secondary = primary.duplicate_shallow();
    assert_eq!(secondary.element_type(), primary.element_type()); // Panics!
    let secondary = primary.duplicate_deep();
    assert_eq!(secondary.element_type(), primary.element_type()) // Panics!
}
   -- array_inner_type ... ERROR: [panic /home/irwin/apps/godot/opensource-contr/missing_docs/gdext/godot-core/src/meta/element_type.rs:141]
  assertion `left == right` failed: Array/Dictionary element type validation failed: cached as Untyped but FFI reports DICTIONARY. This indicates that another extension modified the type after godot-rust cached it.              
    left: DICTIONARY                                                                                                                                                                                                                
   right: NIL                                                                                                                                                                                                                       
  Context: itest `array_inner_type` failed                                                                                                                                                                                          

This PR tweaks codegen, so instead of generating:

    pub unsafe fn duplicate(&self, deep: bool,) -> VariantArray {
        type CallRet = VariantArray;
        type CallParams = (bool,);
        let args = (deep,);
        unsafe {
            let method_bind = sys::builtin_method_table() . fptr_by_index(...);
            Signature::< CallParams, CallRet > ::out_builtin_ptrcall(method_bind, "Array", "duplicate", self.sys_ptr, args)
        }
    }

we generate:

    pub fn duplicate < Ret > (&self, deep: bool,) -> Array < Ret > where Ret: crate::meta::ArrayElement, {
        type CallRet < Ret > = Array < Ret >;
        type CallParams = (bool,);
        let args = (deep,);
        unsafe {
            let method_bind = sys::builtin_method_table() . fptr_by_index(705usize);
            Signature::< CallParams, CallRet < Ret > > ::out_builtin_ptrcall(method_bind, "Array", "duplicate", self.sys_ptr, args)
        }
    }

I haven't been tweaking code too much (albeit I removed some dead code). I think it is OK and should hold for dictionaries as well, as long as nothing drastically changes.

@Yarwin Yarwin added bug c: core Core components labels Oct 6, 2025
@GodotRust
Copy link

API docs are being generated and will be shortly available at: https://godot-rust.github.io/docs/gdext/pr-1357

Comment on lines 201 to 202
&& (&method.return_value().type_tokens().to_string() == "VariantArray"
|| method.is_generic())
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should swap || operands, as is_generic is cheaper to compute.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

side note – it might be good to check if we can set optimization level 1\2 for codegen or macros – it would allow compiler to optimize such things automatically and properly recognize no-ops 🤔.

for example – herestd::mem::take will be no-op for -C opt-level=2 (handy link: https://rust.godbolt.org/) and almost no-op for opt-level 1 (don't ask me why almost). (black box and indirection because compiler was behaving silly and differently to normal usecases)

fn foobaz(somefoo: &mut SomeFoo) {
    let mut vec = std::mem::take(&mut somefoo.foo);
    for val in &mut vec {
        *val += 1;
    }

    somefoo.foo = vec;
}


pub fn main(vec: Vec<u32>) {
    
    std::hint::black_box({
        let mut foo = SomeFoo {foo: vec, bar: 4};
        foobaz(&mut foo)
        });
}

let receiver = functions_common::make_receiver(method.qualifier(), ffi_arg_in);
let object_ptr = &receiver.ffi_arg;

let generic_params = method.return_value().generic_params();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

if it's often empty, that makes this a bit clearer...

Comment on lines -62 to -69
// ExBuilder::new() constructor signature.
let FnParamTokens {
func_general_lifetime: simple_fn_lifetime,
..
} = fns::make_params_exprs(
required_fn_params.iter().cloned(),
FnKind::ExBuilderConstructor,
);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change is unrelated (the dead code you mentioned), right? Why exactly?

Could you extract it to its own commit so it's easier to isolate the changes in case of regressions?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why exactly?

func_general_lifetime was never set, thus it was always empty – effectively this code was doing nothing.

Comment on lines +325 to +335
fn is_generic(&self) -> bool {
matches!(self.return_value().type_, Some(RustTy::GenericArray))
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Always empty lines between multi-line fn decls

Comment on lines 719 to 722
/// `Array<T>`
///
/// Set by [`builtin_method_generic_ret`](crate::special_cases::builtin_method_generic_ret)
GenericArray,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe make clear that this is actually generic in the generated source, and T is not just a placeholder here 🤔


/// Returns some generic type – such as `GenericArray` representing `Array<T>` – if method is marked as generic, `None` otherwise.
///
/// Usually required to properly initialize the return value & cache its type.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/// Usually required to properly initialize the return value & cache its type.
/// Usually required to initialize the return value and cache its type (see also https://github.com/godot-rust/gdext/pull/1357).

Comment on lines 788 to 795
match (
class_name.rust_ty.to_string().as_str(),
method.name.as_str(),
) {
("Array", "duplicate") | ("Array", "slice") => Some(FnReturn {
decl: RustTy::GenericArray.return_decl(),
type_: Some(RustTy::GenericArray),
}),
_ => None,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might be worth dedicated constructor maybe? FnReturn::with_generic_builtin()

Comment on lines 536 to 537
// SAFETY: We never write to the duplicated array, and all values read are read as `Variant`.
let duplicate: VariantArray = unsafe { self.as_inner().duplicate(false) };
let duplicate: Self = unsafe { self.as_inner().duplicate(false) };
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Safety statement needs update.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ended rewriting all the methods, since std:mem::transmute was going from Array<T> to Array<T>, and cache was already initialized 😄

assert_eq!(script.get_global_name(), "CustomScriptForArrays".into());
}

// Test that proper type has been set&cached while creating new Array.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For regression tests, add link to PR/issue, here e.g.

https://github.com/godot-rust/gdext/pull/1357

Comment on lines 681 to 697
#[itest]
fn array_inner_type() {
let primary = Array::<Dictionary>::new();
let secondary = primary.duplicate_shallow();
assert_eq!(secondary.element_type(), primary.element_type());
let secondary = primary.duplicate_deep();
assert_eq!(secondary.element_type(), primary.element_type());
let subarray = primary.subarray_deep(.., None);
assert_eq!(subarray.element_type(), primary.element_type());
let subarray = primary.subarray_shallow(.., None);
assert_eq!(subarray.element_type(), primary.element_type());
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nitpick, group subtests by introducing empty lines:

Suggested change
#[itest]
fn array_inner_type() {
let primary = Array::<Dictionary>::new();
let secondary = primary.duplicate_shallow();
assert_eq!(secondary.element_type(), primary.element_type());
let secondary = primary.duplicate_deep();
assert_eq!(secondary.element_type(), primary.element_type());
let subarray = primary.subarray_deep(.., None);
assert_eq!(subarray.element_type(), primary.element_type());
let subarray = primary.subarray_shallow(.., None);
assert_eq!(subarray.element_type(), primary.element_type());
}
#[itest]
fn array_inner_type() {
let primary = Array::<Dictionary>::new();
let secondary = primary.duplicate_shallow();
assert_eq!(secondary.element_type(), primary.element_type());
let secondary = primary.duplicate_deep();
assert_eq!(secondary.element_type(), primary.element_type());
let subarray = primary.subarray_deep(.., None);
assert_eq!(subarray.element_type(), primary.element_type());
let subarray = primary.subarray_shallow(.., None);
assert_eq!(subarray.element_type(), primary.element_type());
}

@Bromeon
Copy link
Member

Bromeon commented Oct 6, 2025

Nice work, thank you!

Note that this requires all generic functions to be unsafe, as they're only sound with one specific generic argument, and UB with all others.

Yarwin added 2 commits October 6, 2025 20:01
…ray runtime type.

-----

Methods such as `duplicate_...` were initializing and caching typed `VariantArray` as a return value for outgoing calls. We fix it by initializing proper `Array<T>` in the first place.
…set. Remove `ExBuilderConstructor` – it was doing nothing, and in practice it was supposed to do the same thing as `ExBuilderConstructorLifetimed`.
@Yarwin Yarwin force-pushed the fix-wrongly-assumed-array-type branch from d8577c9 to aaffcb5 Compare October 6, 2025 18:01
@Yarwin
Copy link
Contributor Author

Yarwin commented Oct 6, 2025

Note that this requires all generic functions to be unsafe, as they're only sound with one specific generic argument, and UB with all others.

That's correct, I also created/moved separate safety docs for generated generic methods returning Array<T> which is a little more specific.

///
/// You must ensure that the returned array fulfils the safety invariants of [`Array`](crate::builtin::Array).
});
if class_name.godot_ty == "Array" {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is quite nice, but does anyone ever see these docs?
I guess the show up in IDE hints? 🤔

Copy link
Contributor Author

@Yarwin Yarwin Oct 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(currently) contributors are the only ones seeing it.

Still nice to have and might come in handy one day – I think it might be useful providing some context while debugging some UB (if any ever happens) too

screenshoot from previous version image

@Yarwin Yarwin added this pull request to the merge queue Oct 10, 2025
@github-merge-queue github-merge-queue bot removed this pull request from the merge queue due to failed status checks Oct 10, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug c: core Core components

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants