C-like enum's implementation for Rust

I’m trying to implement safe c-like enum’s for Rust, because I’m hurt by incompatibility between C enum’s and Rust #[repr(C)] enum’s. I’m looking for feeback. Here is my PoC.

Playground: https://play.rust-lang.org/?gist=6131d0fcadd0bf2bba48a3efe26278f0

// Author: Volodymyr M. Lisivka <vlisivka@gmail.com>

pub trait IsEnumValid {
    fn is_value_valid(repr: i32) -> bool;
}

/// The wrapper for C-like enum, which has same size as C enum.
#[derive(Copy, Clone)]
pub union CEnumI32Union<E>
where
    E: Copy + IsEnumValid,
{
    enum_repr: E,
    int_repr: i32,
}

impl<E: Copy + IsEnumValid> CEnumI32Union<E> {
    pub fn new(e: E) -> Self {
        Self { enum_repr: e }
    }

    pub fn from_i32(i: i32) -> Self {
        Self { int_repr: i }
    }

    pub fn from_safe(e: SafeCEnum<E>) -> Self {
        match e {
            SafeCEnum::ValidEnum(e) => Self { enum_repr: e },
            SafeCEnum::UnknownValue(i) => Self { int_repr: i },
        }
    }

    pub fn to_safe(&self) -> SafeCEnum<E> {
        SafeCEnum::from(*self)
    }

    pub fn to_int(&self) -> i32 {
        unsafe { self.int_repr }
    }

    pub fn is_valid(&self) -> bool {
        unsafe { E::is_value_valid(self.int_repr) }
    }

    pub fn ok(&self) -> Option<E> {
        unsafe {
            if E::is_value_valid(self.int_repr) {
                Some(self.enum_repr)
            } else {
                None
            }
        }
    }

    pub fn unwrap(&self) -> E {
        self.ok().unwrap()
    }

    pub fn unwrap_or(&self, e: E) -> E {
        self.ok().unwrap_or(e)
    }
}

impl<E: Copy + IsEnumValid> PartialEq for CEnumI32Union<E> {
    fn eq(&self, other: &Self) -> bool {
        unsafe { self.int_repr == other.int_repr }
    }
}

impl<E: Copy + IsEnumValid> Eq for CEnumI32Union<E> {}

impl<E: Copy + IsEnumValid> From<E> for CEnumI32Union<E> {
    fn from(e: E) -> Self {
        Self { enum_repr: e }
    }
}

impl<E: Copy + IsEnumValid> From<i32> for CEnumI32Union<E> {
    fn from(i: i32) -> Self {
        CEnumI32Union { int_repr: i }
    }
}

impl<E: Copy + IsEnumValid> From<CEnumI32Union<E>> for i32 {
    fn from(e: CEnumI32Union<E>) -> Self {
        unsafe { e.int_repr }
    }
}

/// Safe represenation for C-like enum, which uses 2x size to C enum.
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum SafeCEnum<E>
where
    E: Copy + IsEnumValid,
{
    ValidEnum(E),
    UnknownValue(i32),
}

impl<E: Copy + IsEnumValid> SafeCEnum<E> {
    pub fn new(e: E) -> Self {
        SafeCEnum::ValidEnum(e)
    }

    pub fn from_i32(i: i32) -> Self {
        CEnumI32Union::from_i32(i).to_safe()
    }

    pub fn to_union(&self) -> CEnumI32Union<E> {
        match *self {
            SafeCEnum::ValidEnum(e) => CEnumI32Union::new(e),
            SafeCEnum::UnknownValue(i) => CEnumI32Union::from_i32(i),
        }
    }

    pub fn is_valid(&self) -> bool {
        match self {
            SafeCEnum::ValidEnum(_) => true,
            SafeCEnum::UnknownValue(_) => false,
        }
    }

    pub fn ok(&self) -> Option<E> {
        match *self {
            SafeCEnum::ValidEnum(e) => Some(e),
            SafeCEnum::UnknownValue(_) => None,
        }
    }

    pub fn unwrap(&self) -> E {
        self.ok().unwrap()
    }

    pub fn unwrap_or(&self, e: E) -> E {
        self.ok().unwrap_or(e)
    }
}

impl<E: Copy + IsEnumValid> From<CEnumI32Union<E>> for SafeCEnum<E> {
    fn from(e: CEnumI32Union<E>) -> Self {
        unsafe {
            if E::is_value_valid(e.int_repr) {
                SafeCEnum::ValidEnum(e.enum_repr)
            } else {
                SafeCEnum::UnknownValue(e.int_repr)
            }
        }
    }
}

#[repr(C)]
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
enum Enum1 {
    Z = 0,
    A = 1,
    B = 2,
    C = 4,
}

impl IsEnumValid for Enum1 {
    fn is_value_valid(i: i32) -> bool {
        i == Enum1::Z as i32 || i == Enum1::A as i32 || i == Enum1::B as i32 || i == Enum1::C as i32
    }
}

fn main() {
    println!(
        "Size of CEnumI32Union<Enum1>: {}. Size of SafeCEnum<Enum1>: {}.",
        std::mem::size_of::<CEnumI32Union<Enum1>>(),
        std::mem::size_of::<SafeCEnum<Enum1>>(),
    );

    {
        let en: CEnumI32Union<Enum1> = Enum1::A.into();
        let int = en.to_int();
        let safe_enum = en.to_safe();
        let opt = en.ok();
        println!(
            "Safe enum: {:?}, int: {:?}, Option: {:?}, is_valid: {}, unwrap_or(Z): {:?}.",
            safe_enum,
            int,
            opt,
            en.is_valid(),
            en.unwrap_or(Enum1::Z),
        );
    }

    {
        let en: CEnumI32Union<Enum1> = 123.into();
        let int = en.to_int();
        let safe_enum = en.to_safe();
        let opt = en.ok();
        println!(
            "Safe enum: {:?}, int: {:?}, Option: {:?}, is_valid: {}, unwrap_or(Z): {:?}.",
            safe_enum,
            int,
            opt,
            en.is_valid(),
            en.unwrap_or(Enum1::Z),
        );
    }
}

I’m not sure, what’s your original problem you want to solve?

I’m trying to parse log produced by Debug formatter {:?} back into structs. I use Prost (Protobuf implementation for Rust). Rust uses i32 for enum’s, because Protobuf requires forward compatibility: code must be able to cope with values outside of initial enumeration range. Because of this, information about field type is no longer available for compiler, so deserialization is not possible.

Example of code generated by prost:

    #[derive(Clone, PartialEq, Message)]
    pub struct NotificationSettings {
        #[prost(string, tag="1")]
        pub id: String,
        #[prost(enumeration="NotificationSetting", tag="2")]
        pub lap_notifications: i32,
        #[prost(enumeration="NotificationSetting", tag="3")]
        pub ride_detected_notifications: i32,
        #[prost(enumeration="NotificationSetting", tag="4")]
        pub ride_paused_notifications: i32,
        #[prost(enumeration="NotificationSetting", tag="5")]
        pub gps_notifications: i32,
        #[prost(enumeration="NotificationSetting", tag="6")]
        pub sensor_notifications: i32,
        #[prost(enumeration="NotificationSetting", tag="7")]
        pub key_notifications: i32,
        #[prost(enumeration="NotificationSetting", tag="8")]
        pub battery_notifications: i32,
        #[prost(enumeration="NotificationSetting", tag="9")]
        pub step_notifications: i32,
        #[prost(enumeration="NotificationSetting", tag="10")]
        pub off_course_notifications: i32,
        #[prost(enumeration="NotificationSetting", tag="11")]
        pub phone_notifications: i32,
        #[prost(enumeration="NotificationSetting", tag="12")]
        pub navigation_notifications: i32,
        #[prost(enumeration="NotificationSetting", tag="13")]
        pub poi_notifications: i32,
        #[prost(uint64, tag="14")]
        pub created_at: u64,
        #[prost(uint64, tag="15")]
        pub updated_at: u64,
        #[prost(bool, tag="16")]
        pub deleted: bool,
    }

Does macro-generated TryFrom impl fits your use case?

https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=843c48d92bebe6c8389af893b8ca4223

No, I have no idea how to implement deserialization from enum constant as text, e.g. NOTIFICATION_DISABLED, into i32 using TryFrom. :-/ I forked RON for that: https://github.com/vlisivka/rond .

@Hyeonu It looks like you are pointed me to the easy way to auto-generate is_valid() method for an enum. Yes, it helpful, but prost already generates is_valid() and from_i32() methods for Protobuf enum’s, so it’s not necessary in my case. I need to preserve semantics of C-enum (which is just int with labeled values, so i32 can be used) AND information about field type to use in deserialization (so i32 cannot be used).

Anyway, thank you for your help.

This is very fragile. The debug format (of Rust/libstd types) is unspecified and can change at any time. Relying on debug format will cause problems for you and the Rust project. Please don’t parse it.

If you want to get information about structs, use custom derive macros or leverage Serde.

1 Like

@kornel I’m sorry, but I need to parse log, produced by application. It’s my need, and I’m trying to solve it using Rust.

Yes, I can change log of my application at any time.

Yes, I’m trying to use Serde and modified version of RON. No, it doesn’t work, because Rust #[repr(C)] enumerations are incompatible with C enumerations, so they are replaced by i32 by authors of Prost crate, so deserialization with Serde is not working because information about filed type is lost. See example above.

IMHO, solution for the problem is to implement compatibility layer between Rust and C for enumeration, until #[non_exhaustive] tag will be implemented properly, then fork and update Prost, to replace i32 with my wrapper, then use Serde with ROND to parse debug log back into structs, then implement statistic.

To be clear, @kornel is not just saying that you can change your Debug implementations at any time - the Rust compiler itself is free to change the output of {:?} for standard library types at any time (although it’s not massively common, as far as I know).

If you have no other option, fair enough, but just be aware that there is no guarentee that it’ll keep working with later versions of the Rust compiler.

1.35 changed the output of {:#?} (added trailing ,s). (How do I know? I had to fix a pile of tests which were comparing it against expected output.)

3 Likes