Deriving ReprC for custom enums

C enums

A C enum is a field-less enum, i.e., an enum that only has unit variants.

Examples
enum Ordering {
    Less    /* = 0 */,
    Equal   /* = 1 */,
    Greater /* = 2 */,
}

/// The following discriminants are all guaranteed to be > 0.
enum ErrorKind {
    NotFound            = 1,
    PermissionDenied /* = 2 */,
    TimedOut         /* = 3 */,
    Interrupted      /* = 4 */,
    Other            /* = 5 */,
    // ...
}

See the reference for more info about them.

Usage

use ::safer_ffi::prelude:*;

#[derive_ReprC] // <- `::safer_ffi`'s attribute
#[repr(u8)]     // <- explicit integer `repr` is mandatory!
pub
enum LogLevel {
    Off = 0,    // <- explicit discriminants are supported
    Error,
    Warning,
    Info,
    Debug,
}
Generated C header
typedef uint8_t LogLevel_t; enum {
    LOGLEVEL_OFF = 0,
    LOGLEVEL_ERROR,
    LOGLEVEL_WARNING,
    LOGLEVEL_INFO,
    LOGLEVEL_DEBUG,
};

Layout of C enums

These enums are generally used to define a closed set of distinct integral constants in a type-safe fashion.

But when used from C, the type safety is kind of lost, given how loosely C converts back and forth between enums and integers.

This leads to a very important point:

What is the integer type of the enum discriminants?

With no #[repr(...)] annotation whatsoever, Rust reserves the right to choose whatever it wants: no defined C layout, so not FFI-safe.

With #[repr(Int)] (where Int can be u8, i8, u32, etc.) Rust is forced to use that very Int type.

With #[repr(C)], Rust will pick what C would pick if it were given an equivalent definition.

#[repr(C)] enums can cause UB when used across FFI ⚠️

Click for more info

It turns out C itself does not really define a concrete integer layout for its enums. Indeed, the C standard only states that:

  • the discriminants are ints.

  • the enum itself represents an integer type that must fit in an int.

    • Very often this is an int too.

    • but since there is no explicit guarantee that it must be exactly an int too, compiler flags such as -fshort-enums can lead to smaller integer types.

      This means that when you link against a library that was compiled with a different set of flags, such as a system-wide shared library or a Rust generated staticlib / cdylib, then such mismatch is very likely to cause Undefined Behavior!

In practice, when C defines an enum to be used by Rust, there is no other choice but to use #[repr(C)] and pray / ensure that the C library is compiled with the same semantics that Rust expects (e.g., no -fshort-enums flag).

But when doing FFI in the other direction, there is no reason whatsoever to use #[repr(C)]: picking a fixed-size integer is then the most sensible thing to do for a well-defined and thus robust FFI interface.

That's why #[derive_ReprC] makes the opinionated choice of refusing to handle an enum definition that does not provide an explicit fixed-size integer representation.

More complex enums

Are not supported yet.