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

Improve forwards compatibility of input API #131

Merged
merged 1 commit into from
Oct 16, 2023

Conversation

rib
Copy link
Collaborator

@rib rib commented Oct 15, 2023

This adds a #[doc(hidden)] __Unknown(u32) variant to the various enums to keep them extensible without requiring API breaks.

We need to consider that most enums that are based on Android SDK enums may be extended across different versions of Android (i.e. effectively at runtime) or extended in new versions of android-activity when we pull in the latest NDK/SDK constants.

In particular in the case that there is some unknown variant we at least want to be able to preserve the integer value to allow the values to be either passed back into the SDK (it doesn't always matter whether we know the semantics of a variant at compile time) or passed on to something downstream that could be independently updated to know the semantics.

We don't want it to be an API break to extend these enums in future releases of android-activity.

It's not enough to rely on #[non-exhaustive] because that only really helps when adding new variants in sync with android-activity releases.

On the other hand we also can't rely on a catch-all Unknown(u32) that only really helps with unknown variants seen at runtime. (If code were to have an exhaustive match that would include matching on Unknown(_) values then they wouldn't be compatible with new versions of android-activity that would promote unknown values to known ones).

What we aim for instead is to have a hidden catch-all variant that is considered (practically) unmatchable so code is forced to have a unknown => {} catch-all pattern match that will cover unknown variants either in the form of Rust variants added in future versions or in the form of an __Unknown(u32) integer that represents an unknown variant seen at runtime.

Any unknown => {} pattern match can rely on IntoPrimitive to convert the unknown variant to the integer that comes from the Android SDK in case that values needs to be passed on, even without knowing it's semantic meaning at compile time.

Instead of adding an __Unknown(u32) variant to the Class enum though this enum has been removed in favour of adding methods like is_button_class() and is_pointer_class() to the Source type, since the class flags aren't guaranteed to be mutually exclusive and since they are an attribute of the Source.

This removes some reliance try_into().unwrap() that was put in place anticipating that we would support into() via num_enum, once we could update our rust-version.

@rib rib force-pushed the rib/pr/input-enums-forwards-compat branch from b006c2d to 37dc94a Compare October 15, 2023 19:45
@rib rib requested a review from MarijnS95 October 15, 2023 19:45
@rib rib mentioned this pull request Oct 15, 2023
@rib rib force-pushed the rib/pr/input-enums-forwards-compat branch from 37dc94a to eb7dfb1 Compare October 15, 2023 19:48
// semantic meaning at compile time.
#[doc(hidden)]
#[num_enum(catch_all)]
__Unknown(u32),
Copy link
Member

Choose a reason for hiding this comment

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

Hmm, on the ndk I had to assign a bogus discriminant here.

Comment on lines +50 to +65
// What we aim for instead is to have a hidden catch-all variant that
// is considered (practically) unmatchable so code is forced to have
// a `unknown => {}` catch-all pattern match that will cover unknown variants
// either in the form of Rust variants added in future versions or
// in the form of an `__Unknown(u32)` integer that represents an unknown
// variant seen at runtime.
Copy link
Member

Choose a reason for hiding this comment

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

I like this idea and I should have done it this way in the ndk. Because for now we have both:

  • non_exhaustive to allow extending enums (mostly formats and error codes) without breaking changes;
  • Unknown variant that isn't properly hidden with public i32 value, so that missing values are at least properly Debug-printed, easing the process of adding unknown members in the future.

Comment on lines +347 to +349
// XXX: we use `AInputEvent_getAction` directly since we have our own
// `KeyAction` enum that we share between backends, which may also
// capture unknown variants added in new versions of Android.
Copy link
Member

Choose a reason for hiding this comment

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

Yikes, none of these enums have really been marked appropriately.

Copy link
Member

@MarijnS95 MarijnS95 left a comment

Choose a reason for hiding this comment

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

I guess this works (but haven't really reviewed in too great detail - got a tight schedule and am about to leave), unfortunate that the ndk::event types are so rigid.

@rib rib changed the base branch from rib/pr/update-ndk-0.8.0 to main October 15, 2023 21:37
@rib rib force-pushed the rib/pr/input-enums-forwards-compat branch from 8049355 to 4de6d47 Compare October 15, 2023 21:41
@rib
Copy link
Collaborator Author

rib commented Oct 16, 2023

Thanks for the quick review.

Yeah I'm not 100% sure whether we might come up with a better strategy for allowing for this extensibility but for now this feels like a reasonably viable approach.

One specific limitation of this approach is that it's not compatible with #[non_exhaustive] which is unfortunate. I guess that the combination of repr(u32) and the code generated by num_enum convinces the compiler that the full range of variants have been covered if you match against __Unknown(u32) and so you aren't strictly forced to have a catch-all in your pattern matching.

I.e. Any code that ends up exhaustively matching __Unknown(u32) could break without using #[non_exhaustive] forcing them to add a _ => {} catch all when pattern matching.

If we were to implement the From<u32> / Into<u32> manually then we could probably get slightly better ergonomics that would be compatible with #[non_exhaustive] by avoiding the repr(u32) constraint.

Still though, the intent is hopefully quite obvious with the #[doc(hidden)] and the __ prefix, so it won't be considered an API break to add new variants to these enums in the future. Maybe in the future there will be a way to also use #[non_exhaustive] to be even more explicit here.

@rib rib force-pushed the rib/pr/input-enums-forwards-compat branch from 4de6d47 to 471b2be Compare October 16, 2023 15:45
@MarijnS95
Copy link
Member

One specific limitation of this approach is that it's not compatible with #[non_exhaustive] which is unfortunate. I guess that the combination of repr(u32) and the code generated by num_enum convinces the compiler that the full range of variants have been covered if you match against __Unknown(u32) and so you aren't strictly forced to have a catch-all in your pattern matching.

Not exactly sure what you are referring to, since we have exactly such a setup in the ndk.

https://github.com/rust-mobile/ndk/blob/f414cabc12211ebcba4edb3da90950967d4a1933/ndk/src/media_error.rs#L16-L20

Maybe it had to do with ordering of the attributes?

@MarijnS95
Copy link
Member

By the way having a non-unit enum variant appropriately disallows MyEnum::Value as u32, forcing users to use the generated Into<u32> for MyEnum by num_enum which will appropriately unpack the __Unknown(u32), whichever discriminant it ends up receiving (typically prev+1).

This adds a `#[doc(hidden)]` `__Unknown(u32)` variant to the various
enums to keep them extensible without requiring API breaks.

We need to consider that most enums that are based on Android SDK enums
may be extended across different versions of Android (i.e. effectively
at runtime) or extended in new versions of `android-activity` when we
pull in the latest NDK/SDK constants.

In particular in the case that there is some unknown variant we at least
want to be able to preserve the integer value to allow the values to be
either passed back into the SDK (it doesn't always matter whether we
know the semantics of a variant at compile time) or passed on to
something downstream that could be independently updated to know the
semantics.

We don't want it to be an API break to extend these enums in future
releases of `android-activity`.

It's not enough to rely on `#[non-exhaustive]` because that only really
helps when adding new variants in sync with android-activity releases.

On the other hand we also can't rely on a catch-all `Unknown(u32)` that
only really helps with unknown variants seen at runtime. (If code were
to have an exhaustive match that would include matching on `Unknown(_)`
values then they wouldn't be compatible with new versions of
android-activity that would promote unknown values to known ones).

What we aim for instead is to have a hidden catch-all variant that is
considered (practically) unmatchable so code is forced to have a
`unknown => {}` catch-all pattern match that will cover unknown variants
either in the form of Rust variants added in future versions or in the
form of an `__Unknown(u32)` integer that represents an unknown variant
seen at runtime.

Any `unknown => {}` pattern match can rely on `IntoPrimitive` to convert
the `unknown` variant to the integer that comes from the Android SDK in
case that values needs to be passed on, even without knowing it's
semantic meaning at compile time.

Instead of adding an `__Unknown(u32)` variant to the `Class` enum though
this enum has been removed in favour of adding methods like
`is_button_class()` and `is_pointer_class()` to the `Source` type, since
the class flags aren't guaranteed to be mutually exclusive and since
they are an attribute of the `Source`.

This removes some reliance `try_into().unwrap()` that was put in place
anticipating that we would support `into()` via `num_enum`, once we
could update our rust-version.
@rib rib force-pushed the rib/pr/input-enums-forwards-compat branch from 471b2be to 288ed09 Compare October 16, 2023 18:13
@rib
Copy link
Collaborator Author

rib commented Oct 16, 2023

By the way having a non-unit enum variant appropriately disallows MyEnum::Value as u32, forcing users to use the generated Into<u32> for MyEnum by num_enum which will appropriately unpack the __Unknown(u32), whichever discriminant it ends up receiving (typically prev+1).

Yep I think it's reasonable that you have to use .into() instead of as. It's probably not the most common thing to need to be converting to the primitive type.

It's sort of unfortunate that there's no magic compiler trick for being able to avoid having the separate discriminant / value for this case so we could really have a true repr(u32) enum here but it also doesn't seem like a big deal that we lose out on that.

@rib
Copy link
Collaborator Author

rib commented Oct 16, 2023

One specific limitation of this approach is that it's not compatible with #[non_exhaustive] which is unfortunate. I guess that the combination of repr(u32) and the code generated by num_enum convinces the compiler that the full range of variants have been covered if you match against __Unknown(u32) and so you aren't strictly forced to have a catch-all in your pattern matching.

Not exactly sure what you are referring to, since we have exactly such a setup in the ndk.

Yeah I didn't see that it cause any error to use #[non_exhaustive] it's just that it didn't seem to have any effect (no matter the order) - but now it's just occurred to me my testing was probably flawed because I forget that it has different behaviour for code in the same crate! hmmm 🤦

So actually we probably can add #[non_exhaustive] too so the compiler will force apps to add a catch-all pattern.

@rib rib merged commit 969ba5a into main Oct 16, 2023
3 checks passed
rib added a commit that referenced this pull request Oct 16, 2023
This was change as meant to be squashed into #131 before landing
rib added a commit that referenced this pull request Oct 16, 2023
This change was meant to be squashed into #131 before landing
rib added a commit that referenced this pull request Oct 16, 2023
This change was meant to be squashed into #131 before landing
@MarijnS95 MarijnS95 deleted the rib/pr/input-enums-forwards-compat branch October 16, 2023 21:50
@MarijnS95
Copy link
Member

It's sort of unfortunate that there's no magic compiler trick for being able to avoid having the separate discriminant / value for this case so we could really have a true repr(u32) enum here but it also doesn't seem like a big deal that we lose out on that.

That was my main concern with this system/implementation; while "opaque to the user" it just feels ugly to now have to come up with a bogus discriminant (that could possibly be mapped in the future) just to have __Unknown, as well as spend extra bytes on that tuple-value while it "is" a discriminant.

That said, if there was builtin support for such a #[catch_all] __Unknown(u32), there would also be boilerplate - and/or it would suddenly be valid - to cast any u32 to a #[repr(u32)] enum (all without any help from num_enum).

In the end I might even like the associated-enum-constants representation from bindgen the most.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants