Using C enum with FFI (question about enum size)

According to the C standard, C enum size doesn't have a fixed size.

Each enumerated type shall be compatible with char, a signed integer type, or an unsigned integer type. The choice of type is implementation-defined, but shall be capable of representing the values of all the members of the enumeration.

Also, C enum size can change depending on how its compiled, e.g. -fshort-enum may be used.

That's why I wonder if it's safe to use for example a core::ffi::c_int to type a C enum argument on Rust side.
My use case is to export a Rust enum as a C enum and use it as a function parameter. I'm compiling a cdylib and exporting a manually written C header (using cbindgen is off-topic, as I'm questioning the way cbindgen is exporting enums). Would the following trick be a "safe" way to pass a C enum as parameter?

enum Foo {
    Bar,
    Baz,
}
#[no_mangle]
pub extern "C" fn _use_c_enum(foo: core::ffi::c_int) {
    let foo = match foo as isize {
        i if i == Foo::Bar as isize => Foo::Bar,
        i if i == Foo::Baz as isize => Foo::Baz,
        _ => std::process::abort(), 
    }
    todo!()
}
/* exported header */
enum foo { BAR, BAZ };
void _use_c_enum(foo: int);
static inline void use_c_enum(foo: enum foo) {
    _use_c_enum((int)foo);
}

EDIT: added precisions about the fact I'm compiling a cdylib and modified the code example accordingly.

as you figured out, it depends how the C code is compiled. it probably would work most of the time, but it is not guaranteed to work.

the recommended way is to use bindgen, and pass the same CFLAGS when parsing the C headers. bindgen uses libclang to parse C definitions, and libclang should be able to understand gcc command line flags (although I'm not certain whether it is an explicitly stated goal to be 100% compatible with gcc).

1 Like

Why isn't the cast (int)foo guaranteed to work? I know the C enum and I know that all its value are compatible with an int.

It wasn't clear enough in my post, so I will edit it, but I don't use bindgen because I'm compiling and cdylib and exporting C headers to use it, not the opposite. So I want to be able to expose a Rust enum as a C enum and use it as a parameter of a function I'm exporting.

sorry I was mis-interpreting your question. rust enums can have explicit representations, for example, a fieldless enum annotated with #[repr(i32)] attribute is guaranteed to use i32 for its discriminant value, and cbindgen will use this attribute to generate the suitable C (or C++) definition.

for enum with fields (tuple-like or struct-like), you need #[repr(C)] to make it ffi safe, as long all the fields are ffi safe. the memory layout is defined in rfc2195

as for the (int)foo cast,I think it is OK under the condition that you know all its values are representable with an int. in fact, it should be redundent, as C has this weird integer promotion rule, so arguments are automatically promoted to int if they are smaller than int. after all, C enums are just symbolic integer constants with implementation defined representation.

but for the general case, I'm not entirely sure. the explicit cast might be wrong if the enum value cannot fit into an int (I know C++ allows enum values to be larger than int, so I would assume C also support this)

1 Like

With repr(Int), cbindgen does indeed generate a typedef with appropriate integer type, but it doesn't with repr(C), so there could be an issue with it. But the question is not about cbindgen, as i may not be able to use it if my Rust enum is defined in a dependency crate.

Also, I may have to use the C enum directly in the signature instead of a typedef, as advised by the Linux coding style for example (I know it's a matter of taste, I'm not paid to follow mine :slight_smile:). That's why I'm forced to come with this trick; otherwise, I could use a typedef foo_t int; too. I'm confident enough in the compiler to optimize the redundant cast in a static inline function. And because I'm exporting the type myself, I know its discriminant values, so I'm sure it fits into an int.

So, based on your answers, I can conclude than this static inline wrapper is justified to be able to use the enum directly in the signature (as long theenum values fit in the type I choose for the cast). Thank you for your insights.

Both C and C++ support enums larger than int, but only in C++ you may control integer type that is backing enum (similarly to Rust).

In C you have to either verify that enum have expected size or use various tricks (e.g. Vulkan adds VK_enumname_MAX_ENUM = 0x7FFFFFFF to each enum to ensure that 32bit int would be chosen).

This should work while passing raw C enums is notoriously unreliable. E.g. some ABIs would pass small enums that fit into 8bits in low 8bits of register while keeping garbage in the rest of register. This way you end up with very hard to debug and fix errors (any tiny change to your code may fix it or break it).

One trick is to use what Vulkan does.

Thank you for the Vulkan trick, I didn't know about it.

This should work while passing raw C enums is notoriously unreliable.

That's why I'm not passing raw C enum in the real ABI, only in the static inline function. As long as the cast is done in the inlined function, where the size of the enum is fixed by the compiler and will be the same than in the rest of the translation unit.

I've just realized that my static inline may work for passing an enum as a parameter, but if the enum is embedded as a struct field, everything collapse.

So yes, Vulkan trick seems to be the best thing to do to be able to use enum in ABI. Thank you again.

Note that technically it's not portable and doesn't work on systems with strange int's, like 64bit ones or 56bit ones.

But today these are more of a historical significance, I don't know of any Rust-supported platform where it wouldn't work.

C23 actually now allows declaring an enum with fixed underlying type, e.g.

enum foo : int { BAR, BAZ };

Any halfway sane C compiler will give fixed enumerated types identical layout and ABI as the underlying type.

The primary limitation of #[repr(Int)] is that you have to specify a specific size, you can't specify a type alias like c_int. But that's what #[repr(C)] is for — it matches whatever layout/ABI the target C ABI uses for an identically declared enum, without any extra flags that impact that choice.

But it's also important to keep in mind that for Rust enums it is UB to have a value not in the listed options, whereas it is conditionally allowed in C. Using the underlying type for the FFI is thus generally the safest option.

1 Like

But there's pesky -fshort-enums. How to handle that one?

Nice. Only took them 12 years to lift that feature from C++. I wonder how quickly would it become available on important platforms.