zzeroo
zzeroo

Reputation: 5636

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

Upvotes: 1

Views: 737

Answers (2)

Shepmaster
Shepmaster

Reputation: 430711

As DK. mentioned:

  • C's enum and Rust's enum have different restrictions.
  • It's not valid to have a Rust enum that isn't one of the enum variants.
  • What you have is called "bitflags"

Luckily, Bindgen understands bitflags. If you generate your headers while passing the bitfield-enum flag or by using Builder::bitfield_enum:

bindgen --bitfield-enum modbus_error_recovery_mode fake-modbus.h

Bindgen will generate constants for each C enum value, a newtype wrapper, and implementations of the Bit* traits:

// Many implementation details removed 

pub struct modbus_error_recovery_mode(pub ::std::os::raw::c_uint);

pub const modbus_error_recovery_mode_MODBUS_ERROR_RECOVERY_NONE: modbus_error_recovery_mode =
    modbus_error_recovery_mode(0);    
pub const modbus_error_recovery_mode_MODBUS_ERROR_RECOVERY_LINK: modbus_error_recovery_mode =
    modbus_error_recovery_mode(2);
pub const modbus_error_recovery_mode_MODBUS_ERROR_RECOVERY_PROTOCOL: modbus_error_recovery_mode =
    modbus_error_recovery_mode(4);

impl ::std::ops::BitOr<modbus_error_recovery_mode> for modbus_error_recovery_mode {}
impl ::std::ops::BitOrAssign for modbus_error_recovery_mode {}
impl ::std::ops::BitAnd<modbus_error_recovery_mode> for modbus_error_recovery_mode {} 
impl ::std::ops::BitAndAssign for modbus_error_recovery_mode {}

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

How do I expose the bindgen generated constants to the public

Of course, creating an idiomatic Rust API to non-Rust code is the hard part. I might try something like this:

#[derive(Debug)]
struct Modbus(*mut raw::modbus_t);

#[derive(Debug)]
struct Error;

#[derive(Debug, Copy, Clone)]
enum ErrorRecovery {
    Link,
    Protocol,
}

impl ErrorRecovery {
    fn as_raw(&self) -> raw::modbus_error_recovery_mode {
        use ErrorRecovery::*;

        match *self {
            Link => raw::modbus_error_recovery_mode_MODBUS_ERROR_RECOVERY_LINK,
            Protocol => raw::modbus_error_recovery_mode_MODBUS_ERROR_RECOVERY_PROTOCOL,
        }
    }
}

impl Modbus {
    fn set_error_recovery(&mut self, flags: Option<&[ErrorRecovery]>) -> Result<(), Error> {
        let flag = flags.unwrap_or(&[]).iter().fold(
            raw::modbus_error_recovery_mode_MODBUS_ERROR_RECOVERY_NONE,
            |acc, v| acc | v.as_raw(),
        );

        let res = unsafe { raw::modbus_set_error_recovery(self.0, flag) };
        Ok(()) // real error checking
    }
}

Upvotes: 4

DK.
DK.

Reputation: 59005

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.

Upvotes: 2

Related Questions