Search code examples
rustffi

How do I create FFI bindings to C functions expecting OR-ed bytes?


I tried to create FFI bindings to libmodbus, written in C. Here I stumble upon this function

modbus_set_error_recovery(ctx,
                          MODBUS_ERROR_RECOVERY_LINK |
                          MODBUS_ERROR_RECOVERY_PROTOCOL);

The second parameter is defined as

typedef enum
{
    MODBUS_ERROR_RECOVERY_NONE          = 0,
    MODBUS_ERROR_RECOVERY_LINK          = (1<<1),
    MODBUS_ERROR_RECOVERY_PROTOCOL      = (1<<2)
} modbus_error_recovery_mode;

My bindgen-generated bindings are these:

#[repr(u32)]
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
pub enum modbus_error_recovery_mode {
    MODBUS_ERROR_RECOVERY_NONE = 0,
    MODBUS_ERROR_RECOVERY_LINK = 2,
    MODBUS_ERROR_RECOVERY_PROTOCOL = 4,
}

and

extern "C" {
    pub fn modbus_set_error_recovery(ctx: *mut modbus_t,
                                     error_recovery:
                                         modbus_error_recovery_mode)
     -> ::std::os::raw::c_int;
}

My safe interface looks like this, so far:

pub fn set_error_recovery(&mut self, error_recovery_mode: ErrorRecoveryMode) -> Result<()> {

    unsafe {
        match ffi::modbus_set_error_recovery(self.ctx, error_recovery_mode.to_c()) {
            -1 => bail!(Error::last_os_error()),
            0 => Ok(()),
            _ => panic!("libmodbus API incompatible response"),
        }
    }
}

and

use std::ops::BitOr;

#[derive(Clone, Copy, PartialEq, Eq)]
pub enum ErrorRecoveryMode {
    NONE = 0,
    Link = 2,
    Protocol = 4,
}

impl ErrorRecoveryMode {
    pub fn to_c(self) -> ffi::modbus_error_recovery_mode {
        match self {
            NONE => ffi::modbus_error_recovery_mode::MODBUS_ERROR_RECOVERY_NONE,
            Link => ffi::modbus_error_recovery_mode::MODBUS_ERROR_RECOVERY_LINK,
            Protocol => ffi::modbus_error_recovery_mode::MODBUS_ERROR_RECOVERY_PROTOCOL,
        }
    }
}

impl BitOr for ErrorRecoveryMode {
    type Output = Self;
    fn bitor(self, rhs: ErrorRecoveryMode) -> ErrorRecoveryMode {
        self | rhs
    }
}

This triggered a stack overflow if I call set_error_recovery like this

assert!(modbus.set_error_recovery(ErrorRecoveryMode::Link | ErrorRecoveryMode::Protocol).is_ok())

The error is

thread 'set_error_recovery' has overflowed its stack
fatal runtime error: stack overflow

Solution

  • The problem is that C's enum and Rust's enum are very different things. In particular, C allows an enum to have absolutely any value whatsoever, whether or not that value corresponds to a variant.

    Rust does not. Rust relies on enums only ever having a single value of a defined variant, or you run the risk of undefined behaviour.

    What you have is not an enumeration (in the Rust sense), you have are bit flags, for which you want the bitflags crate.

    As for the stack overflow, that's just because you defined the BitOr implementations in terms of itself; that code is unconditionally recursive.