FFI with structured enums in Rust

Hello.

I have trouble binding C structs/unions with this structured enum in https://rust-unofficial.github.io/patterns/idioms/ffi/errors.html#structured-enums. That is, my question is how to implement them in C.

pub mod errors {
    pub enum DatabaseError {
        IsReadOnly,
        IOError(std::io::Error),
        FileCorrupted(String),
    }
}

I've written a simplified version (with a flat enum):

lib.rs

pub mod errors {
    #[derive(Clone, Copy)]
    #[repr(C)]
    pub enum DatabaseError {
        IsReadOnly = 1,
        IOError = 2,
        FileCorrupted = 3,
    }
}

impl From<errors::DatabaseError> for libc::c_int {
    fn from(error: errors::DatabaseError) -> libc::c_int {
        (error as i8).into()
    }
}

pub mod c_api {
    use super::errors::DatabaseError;

    #[no_mangle]
    pub unsafe extern "C" fn db_error_code(error: *const DatabaseError) -> libc::c_int {
        let db_error: &DatabaseError = unsafe {
            // SAFETY: the pointer's lifetime is greater than the current stack frame.
            &*error
        };

        let res: libc::c_int = libc::c_int::from(*db_error);

        res
    }
}

#[cfg(test)]
mod tests {
    use super::{c_api, errors::DatabaseError};

    fn assert_db_error_code(error: DatabaseError, expected_result: i32) {
        let result = unsafe { c_api::db_error_code(&error) };

        assert_eq!(expected_result, result);
    }

    #[test]
    fn db_error_as_is_read_only() {
        assert_db_error_code(DatabaseError::IsReadOnly, 1);
    }

    #[test]
    fn db_error_as_io_error() {
        assert_db_error_code(DatabaseError::IOError, 2);
    }

    #[test]
    fn db_error_as_file_corrupted() {
        assert_db_error_code(DatabaseError::FileCorrupted, 3);
    }
}

client.c

#include <assert.h>
#include <inttypes.h>
#include <stdio.h>

enum DatabaseError {
    IsReadOnly = 1,
    IOError = 2,
    FileCorrupted = 3,
};

extern int32_t
    db_error_code(enum DatabaseError *);

void assert_db_error_code(enum DatabaseError arg, int32_t expected_result)
{
    int32_t result = db_error_code(&arg);

    assert(expected_result == result);
}

void test_api()
{
    {
        enum DatabaseError arg = IsReadOnly;
        int32_t expected_result = 1;
        assert_db_error_code(arg, expected_result);
    }
    {
        enum DatabaseError arg = IOError;
        int32_t expected_result = 2;
        assert_db_error_code(arg, expected_result);
    }
    {
        enum DatabaseError arg = FileCorrupted;
        int32_t expected_result = 3;
        assert_db_error_code(arg, expected_result);
    }
}

int main(void)
{
    enum DatabaseError arg = IsReadOnly;
    int32_t result = db_error_code(&arg);
    printf("(result: %" PRIu32 ")\n",
           result);

    test_api();

    return 0;
}

The layout of repr(C) enums with fields is defined here.

be very careful, for example, #[repr(C, u32)] "field-less" enums are ffi safe, however, the equivalent C code should use explicit int types instead of C enums, because there's no equivalent of #[repr(u32)] in C for enums.

see this warning section in the reference:

https://doc.rust-lang.org/reference/type-layout.html?highlight=repr(C)#reprc-field-less-enums

for enum with fields, the enum must be repr(C), and all the variants must all be repr(C), otherwise, it's still not ffi safe. in your example:

neither std::io::Error nor String is ffi safe. so essentially, in your C code, only the discriminants of the enum have know memory layout.

there are limitations to access the discriminants for enums with fields though, see:

https://doc.rust-lang.org/reference/items/enumerations.html?highlight=discrim#accessing-discriminant

Why don't you use cbindgen?

Thanks for GitHub - mozilla/cbindgen: A project for generating C bindings from Rust code. It helped with initial code, which I manually adjusted to be close to the article's Rust code (Idiomatic Errors - Rust Design Patterns).

Thanks for an explanation. I see that.

My adjusted C code:

#include <inttypes.h>

typedef struct FileCorruptedInfo {
  uint32_t code;
} FileCorruptedInfo;

typedef struct IoErrorInfoDetails {
  uint32_t code;
} IoErrorInfoDetails;

typedef struct IoErrorInfo {
  struct IoErrorInfoDetails details;
} IoErrorInfo;

typedef enum DatabaseError_Tag {
  IsReadOnly,
  IoError,
  FileCorrupted,
} DatabaseError_Tag;

typedef struct DatabaseError {
  DatabaseError_Tag tag;
  union {
    struct IoErrorInfo io_error;
    struct FileCorruptedInfo file_corrupted;
  };
} DatabaseError;

char* db_error_description(const struct DatabaseError* error);