Greetings.
I am currently exploring the Foreign Function Interface (FFI) implementation between Rust and C++, specifically in the context of using a Rust struct from the C++ side. Assuming I have a Rust struct named SomeClient with the following implementation:
pub trait Event{
fn event_name(&self) -> &str;
}
pub trait EventHandler{
fn event<T:Event>(&self, event: &T)->bool;
}
pub struct SomeClient;
impl SomeClient{
pub fn add_event_listener(&self,handler:impl EventHandler)->bool{
//...some logic
todo!()
}
}
As you can see, the implementation relies on Rust generics, which complicates its usage from C++. I've come across a potential solution that involves manually implementing a vtable from the C++ side, as shown below:
class Event {
public:
virtual ~Event() = default;
virtual const char* name() const = 0;
};
class Listener {
public:
virtual ~Listener() = default;
virtual void eventHandle(const Event& event) = 0;
};
extern "C" {
typedef const char* (*GetNameFunc)(void*);
typedef void (*DestructorFunc)(void*);
void c_function(Event* event, GetNameFunc getName, DestructorFunc destructor) {
//build EventObj from rust function
}
}
//wrapper for Event*
void pass_to_c_function(Event* event) {
auto getName = [](void* ptr) -> const char* {
return static_cast<Event*>(ptr)->name();
};
auto destructor = [](void* ptr) {
delete static_cast<Event*>(ptr);
};
c_function(event, getName, destructor);
}
On the Rust side, the corresponding code is:
#[repr(C)]
pub struct EventVtable{
get_evt_name:*const unsafe extern "C" fn (*const c_void)->*const c_char
}
#[repr(C)]
pub struct EventObj{
ptr:*mut c_void,
vtable:*const EventVtable,
deleter:*const unsafe extern "C" fn(*const c_void),
}
//implement traits
impl Event for EventObj{
fn event_name(&self) -> &str {
let s=unsafe{
let chars=(*(*self.vtable).get_evt_name)(self.ptr);
CStr::from_ptr(chars)
};
//do not mind the `unwrap`, this is only a demo
s.to_str().unwrap()
}
}
impl Drop for EventObj{
fn drop(&mut self) {
unsafe{(*(self.deleter))(self.ptr)}
}
}
#[repr(C)]
pub struct EventHandlerVtable{
handle_event:*const unsafe extern "C" fn(*const c_void,*const c_void)->c_int,
}
#[repr(C)]
pub struct EventHandlerObj{
ptr:*mut c_void,
vtable:*const EventHandlerVtable,
deleter:*const unsafe extern "C" fn(*const c_void),
}
//implement traits
impl EventHandler for EventHandlerObj{
fn event<T:Event>(&self, event: &T) -> bool {
unsafe{
(*(*self.vtable).handle_event)(self.ptr,event as *const T as *const c_void)
}.eq(&1)
}
}
impl Drop for EventHandlerObj{
fn drop(&mut self) {
unsafe{(*(self.deleter))(self.ptr)}
}
}
//for cpp side
type EventNameFn = unsafe extern "C" fn(*const c_void) -> *const c_char;
type EventHandlerFn=unsafe extern "C" fn(*const c_void,*const c_void)->c_int;
type Deleter=unsafe extern "C" fn(*const c_void);
//build vtable obj
pub unsafe extern "C" fn build_event_obj(ptr:*mut c_void,c_func:*const EventNameFn,deleter:*const Deleter )->EventObj {
let vtable=EventVtable{
get_evt_name: c_func,
};
let vtable_ptr=Box::leak(Box::new(vtable)) as *const _;
EventObj{
ptr,
vtable:vtable_ptr ,
deleter,
}
}
//...other builder functions
//add listener
pub unsafe extern "C" fn add_listener(ptr: *mut c_void,listener:EventHandlerObj){
let client=ptr as *mut SomeClient;
client.as_ref().unwrap().add_event_listener(listener);
}
This approach simplifies the interaction between Rust and C++ by implementing some virtual methods. Moreover, the FFI code in both C++ and Rust can be easily generated using macros or a code generator.
However, I've noticed that few existing FFI tools (such as cxx) adopt this approach. I'm curious to understand why. One potential downside I can think of is the performance loss due to vtable calling. Are there other drawbacks that I might be overlooking?
Lastly, I would like to note that my expertise lies more in Rust than in C++. If there are any misunderstandings or beginner mistakes in my C++ code, I kindly ask for your patience and guidance.
Thank you in advance for your time and insights.