Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for typed arrays #101

Merged
merged 1 commit into from
Feb 9, 2023
Merged

Conversation

ttencate
Copy link
Contributor

No description provided.

@Bromeon
Copy link
Member

Bromeon commented Feb 1, 2023

Possibly interesting: commit 2ff08c8 added the array_set_typed function in the C header.

bors try

bors bot added a commit that referenced this pull request Feb 1, 2023
@bors
Copy link
Contributor

bors bot commented Feb 1, 2023

try

Build failed:

Copy link
Member

@Bromeon Bromeon left a comment

Choose a reason for hiding this comment

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

Thanks a lot for another great pull request! 😀


1. I thought a bit more about the naming, especially about your comments on Discord:

I hope that typed arrays will be the majority, so #2 is nicer to work with. It also aligns with gdextension's VariantArray. [...]

At that boundary, in godot-core/src/gen, I count 87 places where Array is used as argument or return type, and 209 places where TypedArray is used instead. And some of those Array uses might be cases where the Godot devs haven't added extra type annotations yet.

Idiomatic would probably be to make the "right" way (type-safe) easier to use? That is,

struct Array<T> {...}
type VariantArray = Array<Variant>;

I think this might be worth a try. If it doesn't work, we can still do something like VArray or even rename everything.

(Don't bother with all the text re-alignment if you replace identifiers 😬)


2. The current design does mean that in case of type Variant, we create extra copies by calling Variant::to_variant(). Which I think is fine for now, they're designed to be cheaply copyable, but I thought it would be worth mentioning.


3. The reordering of the methods makes it quite hard to see actual changes, is there any particular reason for that?

Comment on lines +619 to +689
impl<T: VariantMetadata> PartialEq for TypedArray<T> {
#[inline]
fn eq(&self, other: &Self) -> bool {
unsafe {
let mut result = false;
sys::builtin_call! {
array_operator_equal(self.sys(), other.sys(), result.sys_mut())
};
result
}
}
}

impl<T: VariantMetadata> PartialOrd for TypedArray<T> {
#[inline]
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
let op_less = |lhs, rhs| unsafe {
let mut result = false;
sys::builtin_call! {
array_operator_less(lhs, rhs, result.sys_mut())
};
result
};

if op_less(self.sys(), other.sys()) {
Some(std::cmp::Ordering::Less)
} else if op_less(other.sys(), self.sys()) {
Some(std::cmp::Ordering::Greater)
} else if self.eq(other) {
Some(std::cmp::Ordering::Equal)
} else {
None
}
}
}
Copy link
Member

Choose a reason for hiding this comment

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

It's a pity we can't use the macro for generics, but it's probably also quite a big effort to support...
Let's leave it as is, and reconsider in case this happens in many places.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It's not too bad to support that actually. Just did it the quick and dirty way for now, but I can DRY it up before we merge this.

Copy link
Member

Choose a reason for hiding this comment

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

As you wish. For me, it's also fine to do that later.

Comment on lines 201 to 225
/// Changes the generic type on this array, without changing its contents. Needed for API
/// functions that return a variant array even though we know its type.
fn assume_type<U: VariantMetadata>(self) -> TypedArray<U> {
// SAFETY: The memory layout of `TypedArray<T>` does not depend on `T`.
unsafe { std::mem::transmute(self) }
}
Copy link
Member

@Bromeon Bromeon Feb 1, 2023

Choose a reason for hiding this comment

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

This should probably call check_type().

Even if the layout is the same, the entire invariant of the class is that each element has type U.
It isn't unimaginable that future code relies on that property, without thinking of this (unchecked yet marked safe) boundary.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Note that assume_typed is not public. I think we should have a public wrapper like to_typed() that does check the type (and returns Result).

Copy link
Member

@Bromeon Bromeon Feb 2, 2023

Choose a reason for hiding this comment

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

Public/private is generally orthogonal to safe/unsafe. This method can be used to subvert the type system in a way that can cause UB. Not in the current implementation (as far as I can see at least), but as soon as any code starts relying on the fact that a Array<T> holds indeed variants of type T. Which is a generally reasonable assumption*.

Imagine we optimize one of the accesses in the future:

pub fn get(&self, index: usize) -> T {
    ...
    T::from_variant(variant)
}

becomes

pub fn get(&self, index: usize) -> T {
    ...
    // SAFETY: every Variant in Array<T> has T
    unsafe { T::from_variant_unchecked(variant) }
}

Now you can argue that this unsafe block introduced UB. But it's not true, it merely triggered UB. Its // SAFETY assumption relies on the invariant of the type Array<T>. The function that can violate the invariant in the first place is assume_typed(), if used wrongly.

So there are two options:

  • assume_typed() must be unsafe, as the caller must ensure that it is always called on arrays with the correct type (an external invariant).
  • assume_typed() verifies that invariant and is thus safe.

* As you brought up, there are edge cases where "Array<T> always holds T variants" is hard to enforce, but I'd like to try. Even if this turned out to be wrong, I wouldn't see it as a reason to open more doors that can violate invariants.

It's also depending on how we define the invariants. If we shift from "Array<T> holds T variants" towards "each access to variants is separately runtime-checked", things look again different. But I'd like to give up strong guarantees only if there's absolutely no way around it.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fair point. In an ideal world, I would mark assume_typed() as unsafe because all current call sites actually do uphold the invariant. However, we don't know what curve balls Godot's typing will throw us in the future, so an extra check_type() is the safer route.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Turns out, we actually need to break the invariant sometimes. InnerArray::append_array() for example takes any Array, not necessarily a TypedArray of the same type as self. So I'm going to mark this unsafe instead.

Copy link
Member

Choose a reason for hiding this comment

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

But why does that make it unsafe? The method assume_typed() would just panic on check_type(), no?

In other words, it's a logic error if we don't allow to append Array<String> to Array<Variant>, but not a memory safety issue.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We can't call check_type() inside assume_typed() because it would panic for a legitimate, but internal, use case: conversion from TypedArray<T> to TypedArray<Variant> in order to pass it to InnerArray::append_array(). You mentioned two options above; we can't use the second, so I went with the first.

godot-core/src/builtin/array.rs Outdated Show resolved Hide resolved
godot-core/src/builtin/array.rs Outdated Show resolved Hide resolved
godot-core/src/builtin/array.rs Outdated Show resolved Hide resolved
godot-core/src/builtin/meta/class_name.rs Show resolved Hide resolved
godot-core/src/builtin/variant/impls.rs Show resolved Hide resolved
godot/src/lib.rs Outdated Show resolved Hide resolved
@ttencate
Copy link
Contributor Author

ttencate commented Feb 2, 2023

2. The current design does mean that in case of type Variant, we create extra copies by calling Variant::to_variant(). Which I think is fine for now, they're designed to be cheaply copyable, but I thought it would be worth mentioning.

True, I didn't think of that. Agreed that we can live with it for now.

3. The reordering of the methods makes it quite hard to see actual changes, is there any particular reason for that?

There are three impl blocks now: one for T: VariantMetadata, one for T: VariantMetadata + FromVariant and one for T: VariantMetadata + ToVariant. Seemed like the Rusty way to do it. Not sure how useful it is; maybe it would allow e.g. TypedArray<&str>, but of course it would be write-only.

If you think it's not worth it, I can easily put it back in a single impl in the original order.

@ttencate ttencate force-pushed the feature/typed_array branch 2 times, most recently from 6a147fd to f145278 Compare February 2, 2023 08:43
@ttencate
Copy link
Contributor Author

ttencate commented Feb 2, 2023

Looks like set_typed was removed from the JSON API, but there's still array_set_typed in the header file. I'll update this when I have more time.

They also added array_ref, which just points the array's data to another array without checking or touching any types. Not sure when you'd want that, but it's good to know it exists.

@Bromeon Bromeon added feature Adds functionality to the library c: core Core components labels Feb 3, 2023
@Bromeon
Copy link
Member

Bromeon commented Feb 3, 2023

There are three impl blocks now: one for T: VariantMetadata, one for T: VariantMetadata + FromVariant and one for T: VariantMetadata + ToVariant. Seemed like the Rusty way to do it. Not sure how useful it is; maybe it would allow e.g. TypedArray<&str>, but of course it would be write-only.

If you think it's not worth it, I can easily put it back in a single impl in the original order.

Ah, good thinking! We may not be able to keep that up in case we try to tighten the invariants, but for this PR it should be fine.


Looks like set_typed was removed from the JSON API, but there's still array_set_typed in the header file.

Yep, you might have missed my reply here 🙂
This was changed in godotengine/godot#71989, motivation being that it shouldn't be accessible from GDScript.


Any further inputs about the name?
From my earlier review:

1. I thought a bit more about the naming, especially about your comments on Discord:

I hope that typed arrays will be the majority, so #2 is nicer to work with. It also aligns with gdextension's VariantArray. [...]
At that boundary, in godot-core/src/gen, I count 87 places where Array is used as argument or return type, and 209 places where TypedArray is used instead. And some of those Array uses might be cases where the Godot devs haven't added extra type annotations yet.

Idiomatic would probably be to make the "right" way (type-safe) easier to use? That is,

struct Array<T> {...}
type VariantArray = Array<Variant>;

I think this might be worth a try. If it doesn't work, we can still do something like VArray or even rename everything.

(Don't bother with all the text re-alignment if you replace identifiers 😬)

@ttencate
Copy link
Contributor Author

ttencate commented Feb 4, 2023

Any further inputs about the name?

I would like to rename it as you suggested, but would prefer to do that in a separate PR if that's okay.

@ttencate
Copy link
Contributor Author

ttencate commented Feb 4, 2023

Current status: need to add more integration tests; also blocked on #108 which is causing the first few integration tests to fail.

@Bromeon
Copy link
Member

Bromeon commented Feb 4, 2023

Sorry for merge conflicts, doctests were broken on master and somehow slipped CI.

I would like to rename it as you suggested, but would prefer to do that in a separate PR if that's okay.

Fine with me. With your proposal of UnknownArray, we might also consider how that would possibly integrate into our naming scheme.

Current status: need to add more integration tests; also blocked on #108 which is causing the first few integration tests to fail.

I have a branch that fixes the issue, unfortunately it introduces memory leaks elsewhere 🙈 I'll keep debugging.

@ttencate
Copy link
Contributor Author

ttencate commented Feb 5, 2023

Alright, this PR has been stewing for long enough, so I'm going to leave generics support in impl_builtin_traits! for a future PR. I've added some more integration tests and I have some confidence now that there are some cases in which this stuff actually works 😜

@ttencate ttencate marked this pull request as ready for review February 5, 2023 13:18
@Bromeon
Copy link
Member

Bromeon commented Feb 5, 2023

Integration tests only have access to a limited set of classes, due to compile-time / dev-cycle reasons.

Would it be possible to use some of the available classes here?

@ttencate
Copy link
Contributor Author

ttencate commented Feb 5, 2023

Right, I was still using an old command line (not check.sh itest) locally. Might be difficult to find a replacement... I'll take a closer look tonight (CEST).

Copy link
Member

@Bromeon Bromeon left a comment

Choose a reason for hiding this comment

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

Thanks a lot for the update, and the huge number of tests!

godot-codegen/src/class_generator.rs Outdated Show resolved Hide resolved
godot-codegen/src/util.rs Outdated Show resolved Hide resolved
godot-core/src/builtin/array.rs Outdated Show resolved Hide resolved
godot-core/src/builtin/array.rs Show resolved Hide resolved
godot-core/src/builtin/array.rs Show resolved Hide resolved
godot-core/src/builtin/array.rs Outdated Show resolved Hide resolved
godot-core/src/builtin/array.rs Show resolved Hide resolved
godot-core/src/builtin/array.rs Outdated Show resolved Hide resolved
godot-core/src/builtin/meta/class_name.rs Outdated Show resolved Hide resolved
itest/rust/src/array_test.rs Outdated Show resolved Hide resolved
@ttencate ttencate force-pushed the feature/typed_array branch 4 times, most recently from b7fe10e to f1e85d9 Compare February 5, 2023 21:08
@ttencate ttencate mentioned this pull request Feb 5, 2023
@ttencate ttencate force-pushed the feature/typed_array branch 3 times, most recently from 037e3f3 to cff8ff6 Compare February 6, 2023 08:13
@ttencate ttencate force-pushed the feature/typed_array branch 3 times, most recently from 3577314 to 82a6779 Compare February 7, 2023 16:11
Copy link
Member

@Bromeon Bromeon left a comment

Choose a reason for hiding this comment

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

Thanks a lot for another great addition!

bors r+

bors bot added a commit that referenced this pull request Feb 8, 2023
101: Add support for typed arrays r=Bromeon a=ttencate



Co-authored-by: Thomas ten Cate <ttencate@gmail.com>
@bors
Copy link
Contributor

bors bot commented Feb 8, 2023

Build failed:

Copy link
Member

@Bromeon Bromeon left a comment

Choose a reason for hiding this comment

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

Small suggestion, and hopefully we can additionally integrate godotengine/godot#72926 for type safety. But otherwise we can also make a change that works with current Godot.

godot-core/src/builtin/array.rs Outdated Show resolved Hide resolved
@ttencate ttencate force-pushed the feature/typed_array branch 2 times, most recently from ac0c8c9 to 897ad60 Compare February 9, 2023 08:02
@ttencate
Copy link
Contributor Author

ttencate commented Feb 9, 2023

Let's deal with that later, once it's merged upstream. Applied your suggestion for now.

- Rename `Array` to `TypedArray<T>`
- Check/set runtime type of the underlying Godot `Array`
- Make all parameters and return values `T` instead of `Variant`
- Add `Array` as an alias for `TypedArray<Variant>`
- Add `array!` macro and use it in tests

See godot-rust#33 for design discussion
@ttencate
Copy link
Contributor Author

ttencate commented Feb 9, 2023

Rebased and resolved conflicts.

@Bromeon
Copy link
Member

Bromeon commented Feb 9, 2023

bors r+

@bors
Copy link
Contributor

bors bot commented Feb 9, 2023

Build succeeded:

@bors bors bot merged commit e594365 into godot-rust:master Feb 9, 2023
@Bromeon
Copy link
Member

Bromeon commented Feb 9, 2023

Finally 😁 thanks for the great job!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
c: core Core components feature Adds functionality to the library
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants