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

Is there a more ergonomic way of accepting either a Bitflags<MyEnum> or just MyEnum? #29

Closed
joseluis opened this issue Aug 3, 2020 · 11 comments
Labels
question Further information is requested

Comments

@joseluis
Copy link

joseluis commented Aug 3, 2020

I have a structure that accepts Bitflags for a flags field. And it works beautifully well except in the case of wanting to use just one flag:

// OK
let a = MyStruct::new(MyEnum::Flag1 | MyEnum::Flag2);

// Fails
let b = MyStruct::new(MyEnum::Flag1);
                   // ^^^^^^^^^^^^^ expected struct `enumflags2::BitFlags`, found enum `mycrate::MyEnum`

// the ugly solution 
let b = MyStruct::new(enumflags2::BitFlags::from(MyEnum::Flag1));

Do you happen know of a more ergonomic way for the user to provide just one flag? Thank you

@meithecatte
Copy link
Owner

The standard way would be to take impl Into<BitFlags<Enum>>. This is similar to what you would do with Option arguments that you don't want to wrap with Some.

@joseluis
Copy link
Author

joseluis commented Aug 3, 2020

Thanks. My intention is to wrap an unsafe FFI binded structure, and I was already using Option like this:

pub fn new(/*...,*/ flags: Option<BitFlags<NcOptionFlag>>) -> Self {
    MyStruct {
        flags.unwrap_or(BitFlags::empty()).bits() as u64
    }
}

Which works well, and accepts either None, or Some(BitFlags). The only only "problem" was the ergonomics of providing a single flag.

Now, when trying to make it work with the Into Trait I'm encountering a wall, which is that I no longer seem to be able to cast as u64 nor using u64::from(X), since I'm not the creator of either BitFlags or u64 ... So I can't crete the necessary impl Into...

I think going this way is making it too complicated. Among the little information I've found regarding this trick, there's gtk-rs removing the usage of the Into trait for reasons I tend to agree with.

But maybe I'm missing something and I'm open to keep trying. Do you have any more ideas?

@meithecatte
Copy link
Owner

meithecatte commented Aug 3, 2020 via email

@joseluis
Copy link
Author

joseluis commented Aug 4, 2020

What's the motivation behind using Option here? Type-wise, I don't think there should be a distinction between None and Some(BitFlags::empty()).

Merely because since the original C API accepted null as a parameter for several fields, at first I wanted to remain as similar as possible by being able to pass None, but I surely prefer the most ergonomic option.

Somewhat recently, support for NcOptionFlag::empty() landed on master too. Would that help here?

Yes, that would be fantastic, but unfortunately I'm not being able to make it work in a standalone crate

EDIT: I'm sorry I just realized it does work with the master version, which is more recent than the one published in crates.io

Are you still using .bits()? AFAICS, this should work. Also, if you add #[repr(u64)] onto your enum, you won't need the cast ;)

Ha! I completely overlooked that fact. You're right. I just did that and it now works for a single flag :D

pub fn new(/*...,*/ flags: impl Into<BitFlags<NcOptionFlag>>) -> Self {
    MyStruct {
        flags.into().bits()
    }
}

@joseluis
Copy link
Author

joseluis commented Aug 4, 2020

BTW I just found out that it's imposible to derive BitFlags for an enum with a value of 0:

The following code only works if I remove the BitFlags derive:

#[repr(u32)]
#[derive(BitFlags, Copy, Clone, Debug, PartialEq)]
pub enum NcAlign {
    Empty = 0 as u32,
    One = 1 as u32,
    Two = 2 as u32,
}

It gives the error:

error[E0080]: evaluation of constant value failed
   |     ^^^^ attempt to subtract with overflow"

If that worked it would be the best solution for specifying an empty flag, since it would avoid the need of having to include the RawBitFlags trait into scope.

Is there an unavoidable reason why that isn't allowed?

@meithecatte
Copy link
Owner

The semantics of a flag that doesn't have exactly one bit set are unclear. In particular, should .iter include it? Good arguments can be made in both directions, and there doesn't seem to be a use case. Even here, Empty isn't a true yes-or-no flag, but a separate value. I think it would be better defined as an associated constant instead:

impl NcAlign {
    pub const EMPTY: BitFlags<NcAlign> = BitFlags::empty();
}

This makes me realize that {BitFlags, RawBittFlags}::empty and {BitFlags, RawBitFlags}::all should probably be constants, and not functions, too. What do you think? I'm planning one last breaking change release that switches to an attribute macro to fix #21, so it'd be a perfect moment to improve the API.

(side note: RawBitFlags is, in hindsight, a terrible name, which is why master tentatively renames it to BitFlag - values of a type that implement it represent a single flag)

Also, what is the reason for the as u32? This shouldn't be necessary, and it triggers the workaround enabled with not_literal, where assertions are emitted to supplement expansion-time checks.

Is importing the trait something worth avoiding? I feel like this should be made opt-in with something like #[bitflags(inherent_empty_all)] - I checked, and inherent items shadow any trait impls, so this wouldn't introduce any usability issues.

Thoughts?

@joseluis
Copy link
Author

joseluis commented Aug 4, 2020

The semantics of a flag that doesn't have exactly one bit set are unclear. In particular, should .iter include it? Good arguments can be made in both directions, and there doesn't seem to be a use case. Even here, Empty isn't a true yes-or-no flag, but a separate value.

I'm not really sure of the trade-offs... I just miss a comfortable way of dealing with 0 / empty as part of the provided abstraction. I didn't imagine it would be so hairy. For my use-case I'd be the most happy with any solution that works without forcing the user to import anything more than the enum itself.

I think it would be better defined as an associated constant instead:

impl NcAlign {
    pub const EMPTY: BitFlags<NcAlign> = BitFlags::empty();
}

That looked promising but unfortunately it doesn't seem to compile, even in nightly.:

error[E0015]: calls in constants are limited to constant functions, tuple structs and tuple variants

This makes me realize that {BitFlags, RawBittFlags}::empty and {BitFlags, RawBitFlags}::all should probably be constants, and not functions, too. What do you think? I'm planning one last breaking change release that switches to an attribute macro to fix #21, so it'd be a perfect moment to improve the API.

If you are able to make it const I'd say go for it, since I don't think it can have any disadvantages... And maybe then the previous code will work. 😄

(side note: RawBitFlags is, in hindsight, a terrible name, which is why master tentatively renames it to BitFlag - values of a type that implement it represent a single flag)

Agree. And if you already intend to do some breaking changes, it's better to do them all together, better than getting stuck with an uglier API than it could have been once it's more used.

Also, what is the reason for the as u32? This shouldn't be necessary, and it triggers the workaround enabled with not_literal, where assertions are emitted to supplement expansion-time checks.

Well I'm sharing simplifications of the real code, the original code is more like:

#[repr(u64)]
#[derive(BitFlags, Copy, Clone, Debug, PartialEq)]
pub enum NcOptionFlag {
    InhibitSetlocale = nc::NCOPTION_INHIBIT_SETLOCALE as u64,
    VerifySixel = nc::NCOPTION_VERIFY_SIXEL as u64,
    // ....
}

I could use #[repr(u32)], remove the as u64 in each line, and move it to the flags.into().bits() as u64 part so that everything works the same, but... It doesn't seem to achieve much in that regard since then I've tried using the crates.io version and disabling the not_literal feature (I saw it's automatic now in master), but it fails to compile:

error: This kind of discriminant expression is not supported.
hint: Enable the "not_literal" feature to use a workaround.
note: This is not enabled by default due to the high potential for confusing error messages (see documentation).
  --> src/options.rs:28:24
   |
28 |     InhibitSetlocale = nc::NCOPTION_INHIBIT_SETLOCALE,

Is importing the trait something worth avoiding? I feel like this should be made opt-in with something like #[bitflags(inherent_empty_all)] - I checked, and inherent items shadow any trait impls, so this wouldn't introduce any usability issues.

Depends on the trade-offs I suppose. The only thing I'm sure of is that if importing the trait wasn't needed, we would never think of making it so.

I haven't seen anything like what you propose. How would it work? Isn't it related to this pending issue?

EDIT: I also found this procedural macro.

@meithecatte
Copy link
Owner

meithecatte commented Aug 4, 2020 via email

@joseluis
Copy link
Author

joseluis commented Aug 4, 2020

Ok now I understand. Well, I don't think I can be indicative of what is widely known since I'm learning Rust while I try to program what I need... and there's a lot I don't know yet. At the very least I've learned several very important things more in this thread.

I'll let you to it then, and I'll be very happy to use the new release when it's ready. Thank you very much for your time.

@meithecatte meithecatte added the question Further information is requested label Aug 4, 2020
@mokeyish
Copy link

@joseluis Is this https://github.com/mokeyish/enum-flags a better implementation for your question?😅

@joseluis
Copy link
Author

@joseluis Is this https://github.com/mokeyish/enum-flags a better implementation for your question?sweat_smile

thank you! the library looks really good, small and on point.

ximon18 added a commit to NLnetLabs/kmip-protocol that referenced this issue Aug 5, 2021
…e first request of the KMIP spec 1.0 use case 11.1 to verify that it is serialized correctly. Update types to use the new "Transparent" Serde naming pattern where necessary from the krill-kmip-ttlv crate to ensure types are correctly serialized as TTLV structures or primitives in ambiguous newtype cases. Adds some missing #[non_exhaustive] attributes to enums which may gain additional member values in higher KMIP spec versions. Use new enum-flags crate for a simpler cleaner syntax than the enumflags2 crate (see: meithecatte/enumflags2#29 for context). Adds a test that works against a Kryptus KMIP HSM using TTLV over TLS.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
question Further information is requested
Projects
None yet
Development

No branches or pull requests

3 participants