The following is an overview of the problems we faced when getting a Rust plugin to work with our C++ SDK. More specifically, we needed our C++ application to be able to call into a Rust plugin as if it were native C++. Additionally, we needed that Rust plugin to be able to make calls back into the C++ application as if it were native Rust code. If you’re just here for the code samples feel free to skip ahead. For all the gritty details, read on.
Why are you doing this?
A fair question which we received numerous times from various colleagues. In our case, it was a requirement of the project, a requirement which we couldn’t work around. Why might someone else do this? Rust is a great language, and many people see it as poised to replace C++ in many ways. Getting Rust and C++ to play well together would allow you to do a piecemeal port of your existing C++ code base, like what Mozilla is working on.
Why don’t you just use inter-process communication?
Another fair question we received even more than the first. As much as I wish we could, we need this to run in a single process on a mobile device, so IPC was a no-go.
Before reinventing the wheel, we did some research to see if anyone else had already solved our problem. Here’s what we found.
SWIG (Simplified Wrapper and Interface Generator)
I could try to summarize swig, but I’ll let them. Here’s a blurb from their website.
SWIG is great, and it does an amazing job at providing a generic solution to an otherwise very tedious process. Sadly, they do not have Rust support yet.
Let’s see what Mozilla is up to
This blog post was a huge help in wrapping our heads around what we needed to do. It provides a great example of writing a library in Rust and calling into it from C++. However, in our case we also needed the Rust library to be able to asynchronously call back into C++. The search continues.
Formerly named rust_swig (which sounded very promising). Unfortunately again, it only provided wrappers for calling from C++ into Rust, and we need bidirectionality.
This project has matured quite a bit since we initially researched it in early 2020. At the time it didn’t have support for member functions which we most definitely needed; however, recently CXX has become a potentially viable candidate, but more research would be needed on our end. CXX has focused on a safe interop between the two languages. CXX, unlike a lot of other FFI’s, implements code generators to ensure both languages are working idiomatically with their respective types. The C ABI that both languages share, makes this easier as CXX utilizes static analysis against types to match the data structures on both sides, leading to no copying and pasting, limiting overhead.
Since we couldn’t find any existing solution that met our extensive list of requirements we ended up rolling our own from scratch using what we’ve since referred to as the hourglass approach. The approach goes as follows.
- C++ doesn’t know Rust, but it knows C.
- Rust doesn’t know C++, but it knows C.
- Ergo, let’s dumb it all down to C so they can talk to each other.
The implementation, as you may have guessed, is verbose. Instead of trying to squeeze sample code snippets into this post, a git repo has been made available with sample code and can be found here.
Use it. Given how manual this process is, it is expectedly error prone. You will forget to free memory that you manually allocated at one point or another. Valgrind has saved me more times than I care to admit, and for that, it deserves its own section.
The nice thing about this approach is that we had full control over how everything was being done. This allowed us to really fine tune the solution in terms of memory management. While SWIG is great, it’s providing a highly generic solution to a difficult problem, so the code it provides isn’t always to most performant or efficient.
And hey, who doesn’t love writing C code?
This process is admittedly tedious and error-prone. With a growing, changing API (which our’s has turned out to be over the past 18 months), this can be a pain. For a more mature code base this may be less of an issue.
Furthermore, to use this approach, you need a decent understanding of C. While this obviously isn’t a huge jump from C++, manual memory management is not for the faint of heart (I still get chills anytime I see the use of malloc). There’s a reason why the programming community at large has moved away from manual memory management.
And lastly, the code is ugly, and riddled with the dreaded “unsafe” keyword. This was a necessary evil snce our design required us to deal with raw pointers. If you’re a diehard Rust programmer who’s horrified by the code samples, my apologies. We did what we had to.
Is this the best solution out there? Maybe. I hope not. For our use case I’d like to think it was. For most people with more reasonable requirements there is likely a better approach that’s more user friendly.
Is this something you’re passionate about and actively working on? I’m sure the SWIG people would love your help in adding Rust support.
Best of luck and may Valgrind have mercy on your soul.
Sample code examples are available on Github.