-
-
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
Implement support for RID #171
Conversation
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.
Thank you for the PR!
As suggested in comments, I would start with a smaller, less redundant API surface.
Also, some of the tests you add do a lot of bruteforcing over a large design space. Do they increase overall test duration (in debug mode), and if so, how by much?
godot-core/src/builtin/rid.rs
Outdated
/// # Layout: | ||
/// | ||
/// `Rid` has the same layout as a [`u64`], where [`Rid::Invalid`] is 0, and [`Rid::Valid(i)`] is `i`. |
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.
I don't think we should expose this implementation detail, it only limits us in the future (e.g. if we ever add a debug-only field for extra validation or so). We already have new
and to_u64
functions that are reasonably cheap.
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.
Also, as far as I know, without #[repr(C)]
and #[repr(transparent)]
, no layout is guaranteed, even if with the enum optimization you mentioned. It's very unlikely to change, but a compiler is theoretically free to do so.
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.
Also, as far as I know, without
#[repr(C)]
and#[repr(transparent)]
, no layout is guaranteed, even if with the enum optimization you mentioned. It's very unlikely to change, but a compiler is theoretically free to do so.
The rustnomicon explicitly says the nullable pointer optimization is guaranteed:
This is called an "optimization", but unlike other optimizations it is guaranteed to apply to eligible types.
And considering the fields of the enum, there is only one possible layout.
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.
I also just ran the itests with -Zrandomize-layout
and it works. If the layout wasn't guaranteed here, then rid_equiv
should fail under that. Since it checks that Rid::Invalid
maps to 0, and Rid::Valid(i)
maps to i
(for one specific i
at least)
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.
-Zrandomize-layout
passing is a necessary but not sufficient criterion for correct layout -- it only reorders fields and doesn't change fields etc. But to my understanding, it's conforming for a Rust compiler to e.g. add padding bytes at the end, even if the enum optimization is available. But we're really talking about theory here, I also don't see why this would happen.
Anyway, it doesn't matter much: the point is that a guaranteed layout limits potential implementation changes in the future, so we shouldn't document it. There's the existing API for such conversions.
/// Returns `true` if this is an invalid RID. | ||
#[inline] | ||
pub const fn is_invalid(&self) -> bool { | ||
matches!(self, Rid::Invalid) | ||
} |
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.
Providing negated versions of accessors is not something we typically do. I don't think we should start a precedent here, as this will lead to discussions in which cases it's OK or not. Given that RIDs validity checks should ideally not be very common, it's fine if a user has to explicitly type a !
.
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.
oh fair, i think the c++ code had it so i just added it. But it does follow the precedent of Option
which has both is_some
and is_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.
But it does follow the precedent of Option which has both is_some and is_none.
Yes, and it does not follow the precedent of Vec::is_empty()
which has no such opposite. Or any other type in godot-rust, which is much more relevant here 🙂
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.
Vec
isn't an enum though, i chose to compare it with Option
because they're both similar enums, and having an is_x
function for each enum variant is not uncommon.
godot-core/src/builtin/rid.rs
Outdated
impl From<u64> for Rid { | ||
#[inline] | ||
fn from(id: u64) -> Self { | ||
Self::new(id) | ||
} | ||
} | ||
|
||
impl From<NonZeroU64> for Rid { | ||
#[inline] | ||
fn from(id: NonZeroU64) -> Self { | ||
Self::Valid(id) | ||
} | ||
} | ||
|
||
impl From<Rid> for u64 { | ||
#[inline] | ||
fn from(rid: Rid) -> Self { | ||
rid.to_u64() | ||
} | ||
} | ||
|
||
impl TryFrom<Rid> for NonZeroU64 { | ||
type Error = (); | ||
|
||
#[inline] | ||
fn try_from(rid: Rid) -> Result<Self, Self::Error> { | ||
rid.to_non_zero_u64().ok_or(()) | ||
} | ||
} | ||
|
||
impl From<Rid> for Option<NonZeroU64> { | ||
#[inline] | ||
fn from(rid: Rid) -> Self { | ||
match rid { | ||
Rid::Invalid => None, | ||
Rid::Valid(id) => Some(id), | ||
} | ||
} | ||
} | ||
|
||
impl From<Option<NonZeroU64>> for Rid { | ||
#[inline] | ||
fn from(id: Option<NonZeroU64>) -> Self { | ||
match id { | ||
Some(id) => Rid::Valid(id), | ||
None => Rid::Invalid, | ||
} | ||
} | ||
} |
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.
Again, those are redundant with the inherent methods, and I'd prefer to have explicit versions rather than 323.into()
. It's also not very common that a user is actually interested in the integer itself. Rid
is mostly intended to be an opaque ID, and there are already named conversions for the other cases.
itest/rust/src/rid_test.rs
Outdated
/// Disable printing errors from Godot. Ideally we should catch and handle errors, ensuring they happen. | ||
/// But that isn't possible, so for now we just disable printing the error to avoid spamming the terminal. | ||
fn should_error(mut f: impl FnMut()) { | ||
Engine::singleton().set_print_error_messages(false); | ||
f(); | ||
Engine::singleton().set_print_error_messages(true); | ||
} |
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.
Given the semantics, this could be named suppress_godot_print
rather than should_error
.
Would it make sense to move it next to expect_panic
?
itest/rust/src/rid_test.rs
Outdated
let mut v = vec![]; | ||
for _ in 0..1000 { | ||
v.push(server.canvas_item_create()); | ||
} | ||
v |
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 could probably be simplfied by mapping a range 🙂
#[itest] | ||
fn strange_rids() { |
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.
Could you maybe add a quick comment what exactly is being tested here?
I haven't noticed much, checking with |
In gdnative, we use We also explicitly documented that difference in meaning, see Here the situation is a bit different as we also have the |
Honestly to me
As i see it
Disadvantages:
The functions to convert the RID to/from integers are likely not gonna be used much except in the cases where it is useful to maintain the distinction between |
I'm not generally opposed to enums, but it feels a bit strange that But maybe this can be unified later; it's good to already have a first implementation. |
That said, with the currently proposed API it's quite verbose to get a rid.to_non_zero_u64().map(NonZeroU64::get);
if rid.is_valid {
Some(rid.to_u64())
} else {
None
}
match rid {
Valid(nz) => Some(nz.get())
Invalid => None
}
|
So there are three solutions to that i can think of:
Which do you prefer? |
Maybe 1. as I don't think we need Maybe |
What about this? |
Add `RenderingServer` to minimal classes, to be able to test the RID impl against an actual server Add `Engine` to minimal classes, to be able to disable error printing in itests
Thanks! I'll merge this, however expect significant API changes as I'll try to make bors r+ |
Build succeeded:
|
RID is here implemented as an enum
This is to take advantage of null-pointer optimization, while bringing type safety in.
Option would not be usable for this without bigger changes in the code, as we cannot easily implement traits on
Option<Rid>
, and it'd take a lot of work in the codegen to change the right function parameters and return types toOption<Rid>
/Rid
where appropriate.Closes #102