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.