Transparent Rust bindings for C++ structs

This is a short note about the things I discovered about creating transparent bindings for C++ structs into Rust using cxx.

C types

For scalar types like DWORD, and pointer types that reslve down to void*, a ctype wrapper macro can be used to generate a “new-type” that transparently resolves to the underlying type on the C++ side without having to use Box or unique_ptr.

This is can be further simplified using a macro adapted from the one at https://github.com/dtolnay/cxx/issues/254#issuecomment-747860504. The macro looks something like this:

#[macro_export]
macro_rules! ctype_wrapper {
    ($r:ident, $c:ty) => {
        /// Newtype wrapper for a `$c` 
        #[derive(Debug, Eq, Clone, PartialEq, Hash, Default, Copy)]
        #[allow(non_camel_case_types)]
        #[repr(transparent)]
        pub struct $r(pub $c);
        unsafe impl cxx::ExternType for $r {
            type Id = cxx::type_id!($r);
            type Kind = cxx::kind::Trivial;
        }
    };
    ($r:ident, $c:ty, $nice_name:ident) => {
        /// Newtype wrapper for a `$c`
        #[derive(Debug, Eq, Clone, PartialEq, Hash, Default, Copy)]
        #[allow(non_camel_case_types)]
        #[repr(transparent)]
        pub struct $r(pub $c);
        unsafe impl cxx::ExternType for $r {
            type Id = cxx::type_id!($r);
            type Kind = cxx::kind::Trivial;
        }
        type $nice_name = $r;
    };
}

The macro has two variants that are almost the same. They generate a newtype wrapping the given underling type and also implements the cxx::ExternType trait for it allowing to pass it by value to/from C++ code. The variant with 3 arguments can be used to create a type alias for at the same time. For example:

ctype_wrapper!(THRUSTER_HANDLE, usize, ThrusterHandle);

will generate a new-type called THRUSTER_HANDLE wrapping usize as well as an alias called ThrusterHandle. Eithe the type or its alias now be used within an extern "C++" section as:

...
extern "C++" {
    type THRUSTER_HANDLE = crate::ThrusterHandle;
}

Structs

It is possible to define a struct in Rust (outside of the FFI module), with all of the fields (assuming they are all compatible with CXX’s restrictions). A simple example is shown below for a 3-element vector type.

#[derive(Debug, Default)]
pub struct VECTOR3([f64; 3]);
impl VECTOR3 {
    pub fn new(x: f64, y: f64, z: f64) -> Self {
        Self([x, y, z])
    }
}
unsafe impl cxx::ExternType for VECTOR3 {
    type Id = cxx::type_id!("VECTOR3");
    type Kind = cxx::kind::Trivial;
}
type Vector3 = VECTOR3; // Alias

This wraps an underlying VECTOR3 struct on the C++ side as

typedef union {
    double data[3];
    struct {double x, y, z;};
} VECTOR3;

This type can also now be passed by value to/from C++ code without requiring any other wrapper around it.

Backlinks

  • Software Development