-
-
Notifications
You must be signed in to change notification settings - Fork 210
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
Conversation
Possibly interesting: commit 2ff08c8 added the bors try |
tryBuild failed: |
There was a problem hiding this 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?
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 | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
godot-core/src/builtin/array.rs
Outdated
/// 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) } | ||
} |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
).
There was a problem hiding this comment.
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 beunsafe
, 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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
True, I didn't think of that. Agreed that we can live with it for now.
There are three impl blocks now: one for If you think it's not worth it, I can easily put it back in a single impl in the original order. |
6a147fd
to
f145278
Compare
Looks like They also added |
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.
Yep, you might have missed my reply here 🙂 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. |
Current status: need to add more integration tests; also blocked on #108 which is causing the first few integration tests to fail. |
Sorry for merge conflicts, doctests were broken on
Fine with me. With your proposal of
I have a branch that fixes the issue, unfortunately it introduces memory leaks elsewhere 🙈 I'll keep debugging. |
74b729a
to
8950852
Compare
Alright, this PR has been stewing for long enough, so I'm going to leave generics support in |
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? |
Right, I was still using an old command line (not |
There was a problem hiding this 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!
b7fe10e
to
f1e85d9
Compare
037e3f3
to
cff8ff6
Compare
3577314
to
82a6779
Compare
There was a problem hiding this 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+
Build failed: |
There was a problem hiding this 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.
ac0c8c9
to
897ad60
Compare
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
897ad60
to
42f12f5
Compare
Rebased and resolved conflicts. |
bors r+ |
Build succeeded: |
Finally 😁 thanks for the great job! |
No description provided.