Submind YouTube summaries
Thumbnail for Mind The Gap: Rust/C++ Interop

Mind The Gap: Rust/C++ Interop

Watch on YouTube

Video summary

The session addresses the growing necessity for seamless interoperability between C++ and Rust, driven by an increasing prevalence of hybrid codebases where both languages coexist. While various techniques exist to bridge these two systems—from using plain C as a universal glue to specialized tools—the core challenge lies not just in making function calls work, but in ensuring API stability, debuggability, packaging efficiency, and overall developer ergonomics across the entire toolchain. The speaker emphasizes that current solutions often force developers into trade-offs where they must choose between high performance with poor usability or ergonomic ease at the cost of speed, highlighting that a general-purpose interoperability solution without sacrificing either aspect has yet to be fully realized. Several specific tools and methodologies are examined as attempts to solve these integration problems. Traditional approaches like `cbindgen` generate bindings directly from source code but struggle with complex C++ features such as classes or standard types, often requiring unsafe Rust code and manual scaffolding. In contrast, macro-based IDL (Interface Definition Language) solutions like `cxx`, `zinker`, and the ambitious `kubrit` offer more structured ways to define shared interfaces. While tools like `cxx` are popular for their robustness and support for standard types including smart pointers and strings, they impose restrictive assumptions about memory layout that can lead to excessive use of Rust's `pinning`. Newer projects aim to improve upon this by allowing explicit control over type layouts or leveraging compiler introspection to enable pass-by-value semantics without the heavy overhead typically associated with heap allocation. A critical technical hurdle discussed is the fundamental semantic difference between how C++ and Rust handle object movement, specifically regarding relocatability. C++ conservatively treats most objects as non-relocatable due to potential self-references or complex constructors, whereas Rust moves data by default via bitwise copying. This mismatch prevents placing many C++ types directly on the Rust stack without risking memory safety violations if those objects are moved unexpectedly. The speaker notes that while efforts like trivial relocatability were considered for future versions of C++, they faced significant hurdles and remain a work in progress, meaning developers must often rely on `pinning` or other non-idiomatic patterns to safely interact with movable types across the language boundary. Looking forward, the talk concludes by advocating for deeper compiler integration and standardization as key paths toward solving these interoperability issues. The speaker highlights C++26 reflection as a potential game-changer that could replace plain C as the lingua franca of FFI, allowing compilers to introspect layouts and guarantee ABI stability across different toolchains. Ultimately, no single solution fits all diverse project needs; instead, developers must carefully evaluate whether they require surgical precision with IDL-based tools or broad coverage via compiler-integrated approaches like `kubrit`. The community is encouraged to engage in ongoing discussions regarding these challenges, particularly around function overloading and memory semantics, to collectively build more coherent and efficient hybrid ecosystems.
Read the full video transcript
Welcome folks to this session uh on C++ and Rust intro. So uh why are we discussing about these two things? Why C++ and Rust? Well, Rust code everywhere is increasing at an accelerated rate. Um but so does C++ and that's on top of gazillion lives of C++ out there anyway. So hybrid code bases are quickly becoming a thing. uh whether we like it or not, this is where we're going to be for a while. So we need to learn to play nice together. Having seamless intro between C++ and Rust components is essential for the success of this symbiosis. There are many challenges in this process but people have found various ways to make things work from dealing with API issues incompatibility platform tool tool chain guarantees um around languages uh going now to see FFI various techniques and coordination crates and glue in between the two languages but the problem is not fully solved in general when you ask people about what do you think um Rust C++ interrop would mean they would sort of throw a bunch of these keywords at you. Uh and depending on their project needs and what they care about, it might be closer to one uh than the other. So um I think it's more than that actually. Uh I think all these pieces matter. Um you can't just focus on the interop library itself or just the compiler bits or just the linker. uh you have to think about API guarantees, you have to think about uh debugability of of these hybrid projects. Uh you have to think about packaging and deployment and build systems and how we integrate uh these things together so they have a smooth coherent experience. So all these pieces matter. Uh so I'm going to try to see uh how much we can cover today. We have just short time together on this topic but there's much more and I can tease out uh more things you can look at. So what are you going to get out of this talk? I aim to highlight uh some of the major interrog challenges we face today, existing solutions out there and there are a bunch of them. Uh and I'm going to tease out some of the avenues at the forefront of this pursuit as in active experiments and um new um projects that try to solve this problem. I would say that general purpose interoperability that is not tied to a specific tool chain IR uh without loss of performance because nobody wants that has yet to be achieved. Just making things work in the problem space where C++ and Rust activate it's not enough. uh as such I think most of the solutions explored so far by the community fail to deliver on all the requirements swinging a vast range from either being ergonomic but not as performant or being very performance-minded but not very economic to use. So I try to uh walk you through some of these things. Uh so let's see what's out there. Well, the original duct tape for this is C. Of course, the lingua frana of FFI systems languages. Every API consumable from most languages goes through some kind of C uh barrier, right? Is the only truly AI stable universal interrop glue and everyone has been learning to use this over many many years from various languages. But alas, it's not a very great abstraction. uh it doesn't provide any safety um and you're sort of forced to use naked strcts everywhere public fields only have to deal with row pointers manual management of lifetime and so on. So it's definitely lowers the semantics level at which you can operate but many thing many projects uh make it work so for some interop needs this might be just what you need. So let me introduce bin. U many of you probably heard of this tool uh and you might be successfully using it. So this allows Rust code to call into C APIs that will take C headers and produce Rust FI bindings out of there. You have a very simple example here on the slide and this is a source generation. So it requires a special build step to do these transformations on your source code. Right? And we have the other direction with Cbinden which allows C code to call Rust APIs. So it will take your uh Rust RS files and generate C headers out of them. Of this again is a very similar process of source code generation part of your build. Uh but it does require that you take special care around your uh Rust FFI types and functions. So you have to deal with API and representation details. It can be a bit messy but many people make it work. So it is both C ben ben is a solution that serves well many projects. Let's look a bit into what you can do with it. So like I mentioned earlier it works directly on source code files. So no ideal required no separately managed uh description files. uh you have to integrate it as part of your build step of course and it can in terms of types deal with reper C AI representation uh and it does allow pass by value this will be a recurring uh topic that we'll analyze with each solution so we didn't require uh it allows pass by value for cypes basic things u but it's not enough for most cases we see a lot of things here that are just not available in this toolbox. Strs with private fields, C++ classes, um vocabulary types such as unique pointer, optional or their Rust counterparts counterparts box and option. Uh you cannot use um really um prevalent Rust features such as Rust enums or the equivalent in C++ would be standard variant. you can't use standard string or string slices or slices in general uh from rust because slice representation is not guaranteed. So this leads to potentially lots of complicated unsafe code on the rust side. Uh some unsafe bits required here and there. Uh and of course you need to sort of convert to and from this uh C representation if you're actually uh have your code in in C++. So it requires some scaffolding to make decent C++ interfaces. It's again it it lowers the semantic level at which you have to operate but it might be just enough uh for some projects depending on on your uh interrop surface that you have. Enter a new project uh CXX or CXX RS. This one is a macrobased ideal thing. So you have to have a separately maintained some uh files where you describe through this uh special idea language. You describe your FFI module where you define the types that are sitting at the boundary of your in uh interface layer. Right? So uh this is where you define your strcts uh and everything you plan to marshall across and the tool will generate uh both representation both for the C++ side and the rust side just like you see on this example. But this is a manually maintained thing. So you have to um describe this uh interox surface that you have and you need to be very uh explicit about uh this API that you plan to share right uh so you can be very surgical and say okay I'm only exposing the bits that I care on both sides and you can choose how you uh u manage to present them right so that they can be consumed uh from either direction so it's a birectional tool So what does CXX get you uh out of the box? So again you can deal with uh most standard types uh slices everything you put in your ideal definition in terms of strcts including userdefined things u C++ classes in general. Um you can have the usual suspects vocabulary types that you would use like unique pointer optional and the Rust counterparts like box and option. You can use standard strings on both sides of the interrop boundary, slices, vectors, everything you would expect. So it sort of checks a lot of boxes and that's what it makes uh for a very popular solution for many intro projects that sort of expect to operate at a higher semantic level than the uh ben cbinden solution that we s saw earlier. So uh it it is very very popular for this uh for for this reason. So and it's proven as in both uh like C binden and ben um all these solutions have been battle tested. A lot of projects out there are actually using so they're robust. What what CXX is intentionally restrictive and a bit opinionated many people say uh and as in it makes some choices about how it uh re tends to represent things and how it chooses to map semantic concepts from one language to another. So if you don't like these choices or you find that you want to sort of stray from from from these hardcoded assumptions, it it makes it almost impossible. So it sort of you have to take it all as as you have to accept all the choices and trade-offs that the library has made. Uh so CXs does not know about memory layout of user types for example and it doesn't allow because of this pass by value. So in many cas you have to um use uh boxing sort of put things on the hip uh because you can't sort of directly uh put things on on the stack or pass by pass them by value and this sort of makes it uh heavily rely on pinning uh on the rust side which sort of reduces ergonomics and makes it difficult to deal with callbacks allocators and and similar things. uh I I should mention for people who are not very aware of this uh pinning in Rust ensures that an object's address remains stable uh to support self-referential structures uh or foreign code assumptions right so it sort of forces Rust programmers to work in a pin style constraining the natural idioms um that are expected in rust semantics around uh move and borrowing so it's and it's sort of infectious right when you sort of start to pin things around it sort of infects the code around it. Uh so it reduces ergonomics but it can definitely be done right. So enter Zengar. Zengar is a relatively newer um interrupt solution out there. And again it's a custom ideal based solution. You can see it there boxed on the right hand side. And again you describe your types and and functions. But it has a sort of a new thing and that is that layout annotation where you can describe the layout of custom types size alignment and and things of this nature. Uh and then again the tool generates all the required scaffolding on both sides. So again this needs to be separately maintained and manually edited. So it's ideal for when you want to be very surgical about the the uh interrop uh surface area where you don't expect to just lift everything. Um it works with all the standard types slices everything you put in that ideal and yes we check the box finally for pass by value because we have added this annotation for types uh and uh now the tool knows the layout and size uh of userdefined types. So no need for indirection or boxing and a heap allocation. So yay performance. It also reduces the need for excessive pinning like we've seen with CX. So I would say overall Zengar favors more Rust friendly APIs and developer experience accepting some occasional runtime cost here and there. Um and it's also a newer solution in the inrops uh space. So although it has many attractive qualities, it's not as battle tested as the previous ones. So uh enter kubit. Uh this again is a bold new project with the goal of high fidelity language interop between rust and C++. It's a very tall order. uh and the the aim here is that you don't want to be sort of uh very surgical about what types you declare at the interop boundary and say these are my shared things where you say I want to lift the whole API right I want to have both languages have equal footing and lift uh everything to be accessible equally from both sides so it's sort of blur the line language wise this is a very complicated uh thing because of C++ mostly uh and it needs native compiler integration on both sides. Um so it works directly on source files no ideal required to be manually edited and it covers like I said the whole API surface whereas the ideal based solutions tend to be very targeted. Uh in terms of compiler diversity right now just clang is supported uh but there is an open uh window for uh other C++ compilers such as MSBC and GCC and this is via the optional uh auto IDL facility opening the C++ A query interface to new compilers. It allows pass by value because the compiler can introspect layout information. Uh so that's good. Uh in terms of um processing again um this intermediate ideal layer it's sort of bring your own compiler kind of uh deal where you can uh have kubit use this uh as an intermediate step to do um as queries and decouple this from the current uh prototype implementation which is based on clank. So it sort of uh is it is open for other C++ compilers. I would say projects in general have a very diverse range of interrop needs. So there's it's very hard to find uh one solution fits all and I would say we have on the one side what I call the ideal based solutions like C++ and Zengar which tend to be tool chain agnostic. Uh you can be very specific about API bindings. you can uh reason about API stability and version resilience and dynamic linking and all these things that are attractive to many projects whereas on the other side of the spectrum you have things like crewit which require tighter compiler integrations so it's not one sizefits-all and I think most of the complication comes out of the fact that uh many C++ features have no direct rust equivalent uh so I have here uh hence the name of the Uh I have here just a couple of examples that can be problematic. So there's profound semantic differences between these language constructs. Uh Rust semantics is a very much a subset of C++ semantics. Uh and generally Rust is less expressive than C++. So that makes that using Rust code from C++ is easier but using C++ code from Rust is much harder. So again calling Rust from C++ much easier. You can do it. Rust semantics are a subset. Rust strong type system provides guarantees. Easy to grasp intended semantics of function and types. Uh the compiler can be queried for all the um information that you need for this sort of introspect everything uh and derive the the required tra uh traits and and information on both sides. So this is definitely the happy path of the interrupt story. the other way calling C++ from Rust it's much harder uh sometimes even impossible uh C++ features that don't have direct Rust equivalent like for example function overloading uh you have to deal with unsafe some of the lifetimes complications uh some aliasing for references that can cause all sorts of pains and in general dealing with movable types that are non-me copy um just focusing on function overloading as a one example of this um many people say this is a sort of table stakes requirement being able to call overload sets on the C++ side and some folks say that we really need to have a way to semantically identify C++ overloads from Rust at language level. Uh my take on this is that we have to resist temptation to complicate Rust too much for the sake of interrop and interrop alone and I would urge you to look at how carbon and swift approach this uh matters. they're slightly different. Uh and we have to think can we really solve this outside the core language uh so that we don't hinder uh rust powerful type inference with overloading. So again many people claim that we already have a clanky way of simulating overloading in rust so we might as well go all the way and do it right. uh and there's experiments uh underway using traits and the splat feature and the overloaded macro that you can use uh to get this done. Again, these are in very early experimentation phase. Uh so uh nothing you can use right away but that's just to frame the problem. Another very important problem in this mix is movement or teleportation of in memory. So object relocation this is a particularly sensitive topic about handling C++ values uh because C++ in general treats uh all objects as conservatively considered non-relocatable. Um in contrast a relocatable value would preserve its invariant even if its bits are moved arbitrarily in memory. So for example a 32-bit integer is the same if you move the four bytes someplace else. uh C++ assumption of non-relocatable values hurts everyone for the benefit of a few questionable designs in code out there. I'm not saying they're not there, but in general I think we can do without. So only a minority of C++ objects are generally non-relocatable. Some examples here. So relocating an object to a distinct physical location is a destructive move, right? You create the new object having the original value at the destination and you have to destroy the original source object. sort of a two-step process for a lot of types such as containers. Copying the bytes and discarding the source uh if you have constructions that have no self- references or any funny business, no code needs to run when moving the inline portion of these values to a new location. So there basically bitwise relocatable types and that's a good thing. Many C++ libraries already optimize for such types including standard library implementations. So 3D relocation mostly tries to standardize this optimization that's already common practice. Alas we tried to do it for C++ 26. Um and we ran into many problems. Uh so this was actually pulled out of uh the 26 draft at the very last minute. Uh they prop a bunch of proposals actually around trivial relocatability. Um some problems were identified a bit late in the process. So uh we aired on the cautious side. Uh but there's still hope to get it someday. So C++ and Rust have opposite ways of handling moves. Rust very much likes to move by default and does me copy of the bytes of the type regardless of the type and it renders the move from object inaccessible through the type system. That's a hard no. Whereas C++ likes to copy by default as we all know. Uh and by default you have these special move functions constructors and assignment. Uh and for example standard string cannot be mecopied due to small string optimization. Uh there's it's self-referential in some implementations not all. So again it leaves the move from value accessible to be destroyed at the end of the scope which is slightly different semantics than the rest side. Rust pin solves this issue with self-represential types in a different way. Definitely not ergonomic, but it's sort of outside the the type system directly. Why do we care about this? We care about this because you cannot place a C++ object on the Rust stack because it cannot be safely me copy moved or relocated in memory in general. That's why we needed these trivially relocatable or bitwise relocatable types uh that we aimed to introduce in C++ 26 and maybe we'll get in 29. Uh that's that's the goal. You want to get to a place where we have the standard library to be mostly relocatable. Most of the stuff there containers be relocatable and that would allow us to put most C++ types on the Rust stack. Rust loves the stack. So that leads to uh high efficiency at this interrop boundary. And now sort of uh at the end let's talk about the compilers a bit because they play a big role in this story. Uh there's many tricks that require deep compiler involvement. I mentioned earlier um layout information. High fidelity language semantics and mapping of vocabulary types require involvements from front ends both on C++ side and Rust side. And of course we want to aim for tool chain independent IR. We don't want to sort of tie an interrop solution to a specific compiler representation. We want to favor open solutions, extendable solutions that allow all compilers out there to interact uh on on C++ and Rust side. So of course we care about binary level fidelity API codegen linking dynamic libraries and all sorts of other things that require a deep compiler integration. uh on the language uh side uh I believe that C++ 26 reflection could be a gamecher for lang interrupt and I'm not the only one uh like uh I highly recommend you watch Herb Sutter's uh keynote from last year's CPPCON where he makes this as one of the working uh cases for C++ 26 reflection having C++ sort of the new lingua frankoff interrop across languages instead of C because of reflection ction facilities that the language provides. Now, of course, we cannot complete round off the whole interrupt story without addressing the uh AI elephant in the room. Uh I don't have time to go into this now, but you can definitely uh search for my name online and ABI and you'll find more than you want to see. Um so, if you care about these things uh and you want to get involved, there's been a lot of effort recently uh around this. So uh I can point you to uh where you can get involved. There's uh rust lang zulip chat and a dedicated channel for inrop. You'll find lots of familiar rust and C++ names there. Uh people that you might know are interested in this topic on both sides of the languages. And we've had we had already a bunch of meetings. One of them uh very recently. Uh there's extensive notes from these previous meetings so you can catch up if you want. They're all all accessible online and if you care about this topic do get involved. We welcome expertise for people and opinions and we would like to build this uh solutions better together. Some final recommendations that you must watch uh fine grain C++ Rust interrop. Um this is sort of you kubit's take on the story. Uh very good presentation from u last year's rust comp and uh deep dive into zengar which I didn't have time to cover in depth today. Uh but you can definitely learn a lot uh from this presentation by David Sank. All right. Uh now I'm curious what you folks think about uh interrop and I sort of welcome you in um in in chat and sort of interact with me either now or uh later online uh and discuss some of these things.