mozilla / uniffi-rs

a multi-language bindings generator for rust
https://mozilla.github.io/uniffi-rs/
Mozilla Public License 2.0
2.77k stars 230 forks source link

Suggestion: generate permissive, C-compatible enums #2021

Open BatmanAoD opened 7 months ago

BatmanAoD commented 7 months ago

It's often useful to interoperate with C libraries that may return enum values; in this case you do not necessarily have a guarantee that the returned value is a valid variant, and it may be useful to preserve the original value, e.g. for round-tripping data.

It's simple enough to represent this in Rust:

#[repr(u32)]
enum Foo {
    KnownValue1 = 1,
    KnownValue2 = 2,
    Unknown(u32),
}

...and then explicitly implement From<u32> for Foo and From<Foo> for u32. Here's a library that provides a macro for defining enums in this way: https://github.com/cimbul/tartan-c-enum

This seems like something that would be useful for UniFFI to support as well.

setoelkahfi commented 1 month ago

@BatmanAoD How feasible is it to implement this feature with uniffi-rs?

I've been using serde-repr for C-like enums and would like to see how I can generate their bindings for other languages, currently Swift.

BatmanAoD commented 1 month ago

@setoelkahfi Sorry, I don't know; I'm not a UniFFI contributor, just a user.

badboy commented 1 month ago

I'm not really sure what's requested here. How do you see this being relevant for UniFFI? What would it look like on the Kotlin/Swift/Python side to use this?

setoelkahfi commented 1 month ago

@BatmanAoD, no worries.

@badboy TLDR: Never mind. I'll change my implementation, and this request is no longer relevant. I have a separate ErrorCode enum representing my integer-based error_code in an ErrorResponse struct, like this:

#[derive(Serialize, Deserialize, Debug)]
#[tsync]
pub struct ErrorResponse {
  pub error_code: ErrorCode,
  pub message: String,
}

#[repr(i32)]
#[derive(Serialize_repr, Deserialize_repr, Debug, EnumIter)]
#[tsync]
pub enum ErrorCode {
    Unknown = 0,
    // User-defined error codes begin from 1000
    UserNotFound = 1000,
    // Rest of the codes

Then I realized I'd not be able to use the throwing errors, as explained here.

BatmanAoD commented 1 month ago

For Python, it would presumably generate an enum with FlagBoundary set to either EJECT (forcing the user to do type checks to handle unknown values) or KEEP with the final variant being Unknown. https://docs.python.org/3/library/enum.html#enum.FlagBoundary.EJECT

I don't know enough about Swift or Kotlin enums to say how they should handle this.

For Go, normal int newtypes already work this way.

setoelkahfi commented 1 month ago

Okay, I take back my word. I'd still want this feature. For a better explanation, see this Serde doc.

I changed my error structure to use a parent enum instead of struct, but I'll still need the int representation of the error code. In Swift, we use raw representable protocol to express this.

An enum with an integer raw value can be expressed like this:

enum ErrorCode: Int {
    case parseError = 1, 
    case networkError = 2,
    // etc
}

Related https://github.com/mozilla/uniffi-rs/issues/2225

mhammond commented 1 month ago

Does https://mozilla.github.io/uniffi-rs/latest/proc_macro/index.html#variant-discriminants help here? The discriminants are exposed to bindings in that scenario.

setoelkahfi commented 1 month ago

Yes, that's what I'm looking for, minus how to do it in a UDL file. At least I couldn't find it here.

mhammond commented 1 month ago

1792 is for making it available to UDL, so I guess I'll close this one (but feel free to reopen if that's not appropriate!)

BatmanAoD commented 1 month ago

@mhammond I don't understand how that helps, actually. My intent in this ticket is to provide a way to handle enum values from C (or other languages) that may actually be invalid variants. In Rust, even with repr, this is immediate undefined behavior.

mhammond commented 1 month ago

Fair enough - I got confused about who the OP was!