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.