There is a growing industry-wide push away from writing code in unsafe languages like C/C++ in favor of modern, safer languages like Rust. This, of course, is a big opportunity for Rust – and one it took part in creating.
The interest in Rust is strong enough that its adoption has started to feel almost "inevitable" in some places. There are, however, big barriers in the way of adoption in a few key areas, namely areas with large existing codebases and libraries in another language. In these places, the local incentive to stay with the existing language is much stronger than any one development team – or even several working together – can overcome. Making Rust succeed in these areas means reducing costs such that adoption becomes feasible.
The language we focus on in this document is C++, the very language Rust set out to replace in Firefox back in the Mozilla days. As we'll see, mismatches between Rust and existing systems languages create a substantial "tax" that must be paid by interop solutions today, one that has so far made it infeasible to create bindings to all but the simplest C++ APIs. This has created credible doubt about Rust's ability to provide robust interop that can compete with languages like Swift that were designed for interop from the ground up.
Rust has the ability to provide robust interop, but achieving it will take a coordinated effort across different parts of the Project as well as with industry partners. At the same time, it's important to do so in a way that is in line with Rust's values. Many of the features that make Rust unique also give it its unique combination of expressiveness, robustness, safety, and performance. We propose sticking to an area of the design space that can allow Rust to preserve its status as a separate language while providing extension points at the boundary that go beyond the language.
Rust's influence on the software industry in recent years has become difficult to overstate. Memory safety in particular has caught the notice of governmental bodies in the European Union and the United States, among others, which have begun accelerating the push to adopt memory safe languages. Fearless concurrency has come at a time when there is growing demand for concurrent systems. Meanwhile, developers and management alike have recognized other benefits of Rust like better tooling, better productivity, and better reliability.
As Google has gained experience with Rust adoption, two major lessons stand out:
The first point is very encouraging: It means that Rust adoption starts showing value right away. The second point means we are faced with a decision. Should Rust become a realistic option for every C/C++ user to adopt, or should it stick to the areas where it has found success so far?
Excerpts from the blog post Eliminating Memory Safety Vulnerabilities at the Source:
Vulnerabilities decay exponentially. They have a half-life. A large-scale study of vulnerability lifetimes published in 2022 in Usenix Security confirmed this phenomenon. Researchers found that the vast majority of vulnerabilities reside in new or recently modified code:
This leads to two important takeaways:
- The problem is overwhelmingly with new code, necessitating a fundamental change in how we develop code.
- Code matures and gets safer with time, exponentially, making the returns on investments like rewrites diminish over time as code gets older.
For example, based on the average vulnerability lifetimes, 5-year-old code has a 3.4x (using lifetimes from the study) to 7.4x (using lifetimes observed in Android and Chromium) lower vulnerability density than new code.
These results are both surprising and encouraging. For a long time there has been a belief that rewriting code is the only way to make it safe. In reality, as the blog post says, "interop is the new rewrite" – making a language cut point and only writing new code in a memory-safe language can have similar benefits without the sometimes exorbitant cost of a rewrite.
In my observation there are three primary axes that dictate the progress of language adoption: Social, Economic, and Technical. Social is about motivation at the individual level. Economic is about managing tradeoffs in resources. Technical defines what is and is not possible within a time and resource horizon.[1]
In some cases, moving along one axis can overcome blockers in the others. In other cases, though, we are hitting a limit where Rust adoption does not look technically feasible within reasonable time and resource constraints. Even in cases where it is feasible, more readily available and robust interop solutions for Rust would significantly tilt the other tradeoffs in favor of Rust.
Rust adoption is feasible today in the following situations:
This includes completely new projects as well as complete rewrites of existing projects, when such rewrites are socially and economically viable.
Projects with a natural interprocess boundary between components are more easily migrated to Rust. Because of the loose coupling enforced by the boundary, the project can be incrementally migrated one component at a time. Microservice architectures with their RPC/HTTP boundaries are one example of this. However, even microservices benefit from common libraries for serialization, databases, and shared business logic.
Projects with a small, simple API surface that can be manually expressed in terms of the C ABI. This boundary, expressed and invoked in unsafe
code, is prone to human error. It can be maintainable when the surface is small enough, but this also means that Rust adoption can decrease safety at the language boundary.
Projects with a limited API vocabulary (only basic types and language features) are able to use one of the existing interop tools like bindgen, cbindgen, or cxx.
The fact that all of the above options exist and undergo active development is a testament to the value developers see in Rust adoption. However, they leave out a large portion of production use cases today: Projects that make rich use of an API in a language like C++, where comparatively limited interop support exists for Rust, and that link in enough code to make rewriting infeasible.
This includes areas like:
Smaller and hobbyist Rust projects exist in many of these areas, and there are even production uses. I get excited every time I see a blog post about one! But these depend on libraries or bindings that are expensive to maintain, and such cost is only taken on because of highly motivated individuals or some existential need of the project.
Transitioning to production uses at scale has remained elusive. It would require a huge coordinated effort on the part of many library maintainers and users, none of whom are locally empowered to act on their own.
Furthermore, the limitations of current interop tooling are not simply a matter of adding features to the existing tools. Many of them stem from a mismatch in the expressiveness of the two languages along various axes. As one example, C++ and Java both support overloading, while Rust does not. In some cases this mismatch is broadly accepted as a missing feature in Rust that will be added in time. In others, Rust's lack of expressiveness may be considered a feature in itself, but nonetheless one that makes interop harder.
As one example, Rust compiler itself interfaces with a number of LLVM C++ APIs through a hand-maintained bindings layer. We are lucky to have a small working group of individuals who own this layer; if we did not, Rust would not exist! Still, it is unlikely Rust would ever benefit from MLIR or expose much of LLVM's highly customizable pass infrastructure because of their use of rich C++ patterns like templates, inheritance, and CRTP.
The two languages with the most promising rich C++ interop are Swift and (the up and coming) Carbon. See the Appendix for others and coverage of the major Rust solutions out there today.
Swift is a memory-safe language owned by Apple that was built to evolve their development ecosystem away from Objective C and toward a more modern, safe language. This bi-directional interop works by integrating Clang as a library into the Swift compiler. Eventually Swift added support for C++ interop with help from engineers at Google. Swift now provides bi-directional interop for both Objective C and C++.
borrowing
to express patterns like fn foo(&self) -> &Field
in Rust. Rust is of course more expressive here.Carbon is a successor language to C++ started by Google. On a technical level, it aims to be to C++ what Swift was to ObjC: A modern language with two-way interop as a central design constraint.
Carbon is experimental and undergoing major development; it is not yet ready for production. It has two areas of focus for 2025:
Like Swift, Carbon will integrate Clang into its compiler to provide rich interop support. Its design goals include:
This is a fractal design space with many choices. We cannot make the correct choices for every use case: Swift/ObjC was successful by focusing in on Apple's API surface. cxx has been successful by focusing on Meta's specific needs, which are different from Google's and Microsoft's.
Therefore, we should take an enabling approach: Extending the language to help solve common problems, and defining the set of extension points we are comfortable with.
This should be done with the intention of moving more APIs from "not possible to bind automatically" to "possible" in both directions (Rust calling C++ and C++ calling Rust). It can also help us bind more APIs safely and ergonomically, but much of that will depend on the specific interop solution, how it is customized for a particular codebase, and what can be expressed in C++ itself.
Amount of effort needed for various use cases, where the size represents how many use cases fall into each category today. The goal is to invert this pyramid.
extern "C"
Note that these are asymptotic: Given the extremely rich, varied and sometimes downright strange surface area of C++ the language, most of these north star goals will never be and should never be met at 100%. That said, if accepted they provide a direction – and an agreement that we would like to move quite a bit farther in that direction than where we are today.
This is an example from the user manual of a hypothetical Rust/C++ interop tool called krusty.
cargo add krusty
cargo add --build krusty-build
// main.rs
krusty::include!("<vector>") as cpp_std;
krusty::include!("mylib.h") as mylib;
struct Item(i32);
struct MyJob {
work_items: cpp_std::vector<Item>,
}
impl MyJob {
fn new() -> MyJob {
let mut work_items = cpp_std::vector::new();
for i in 1..=100 {
work_items.push_back(Item(i));
}
MyJob { work_items }
}
}
impl mylib::Job for MyJob {
fn size(&self) -> usize {
self.work_items.size()
}
fn make_progress(&mut self) {
// ...
}
}
fn main() {
mylib::WorkerPool::new().submit(MyJob::new());
mylib::SpecializedWorkerPool::new().submit(MyJob::new());
}
// mylib.h
class WorkerPool {
public:
WorkerPool();
virtual ~WorkerPool();
void submit(Job job);
void submit(std::function<void()>);
};
// Example of a virtual interface.
struct Job {
virtual size_t size() const = 0;
virtual void make_progress() = 0;
};
// Example of a templated interface.
template<typename J>
class SpecializedWorkerPool {
public:
SpecializedWorkerPool() { /* ... */ }
virtual ~SpecializedWorkerPool() { /* ... */ }
void submit(J job);
};
In this example we see:
WorkerPool::submit()
, based on type information from the Rust sideMany things are not shown, such as:
While we are nowhere near this level of capability today from any Rust-based solution, in the long run we expect many of these features to be important for broad adoption. We would therefore like to get agreement on accepting "in principle" any that we can, and to know where the "big red lines" are, if any, that we should not cross.
In particular:
Finally, the lang team may want a subteam to iterate on these proposals ahead of their review. This would also be a good place to involve other industry players (other companies, Carbon and Swift if they're interested, etc). If we manage to generate enough interest, is there receptiveness to hosting this work within the Rust Project?
@cImport
/@cInclude
ffi
module
unsafe
to a C++ API, or leave it off, in which case it is considered safeHere are the primary points of integration needed to enable rich interop with C++, and potentially other languages, along with examples of big changes we could make to enable it. Note that this is intended to be the starting point for further conversation, not a fait accompli.
Crubit also has a list of existing unstable features it uses or would like to use.
Location::file_with_nul
The divergent evolution of Rust and C++ has created quite a few wrinkles to be addressed by interop solutions. Some of these are imminently fixable; others may take years to unwind.
By making interop a priority and (hopefully) cooperating with the C and C++ standards committees, we can prevent new wrinkles like this from appearing faster than they can be smoothed out.
#[repr(C)]
structs is the opposite of C++, so we will need a way to customize drop order for structs.[[no_unique_address]]
mem::swap
as a memcpy
of size_of::<T>()
bytes. C++ does not allow this: fields of a child class may be placed in tail padding of the base class.
Jon Bauman: I work for the Foundation (for the last 8 months) on the C++ initiative.
Dmytro: I lead up Google's efforts on this. We had previously investigated Swift, and I used to work at Apple on that.
Devin Jeanpierre: I work for Google together with Dmytro, and am the technical lead for Crubit.
Jon Bauman: After having talked with a lot of people, I'm now more optimistic about the C++ committee process than I was before. There's a lot of respect for what Rust has done. Though "profiles" received a positive reaction in Austria, it's not moving forward.
Josh: We should proceed as though nothing will happen on the C++ side, and then if it does, it will be a welcome suprise for us.
TC: Jon, how do you see your work tying in with the work at Google, and broadly how does that tie in with your interactions with the C++ committees?
Jon: The crubit approach seems like something promising in the long term. If it's going to be more than just a Google product, there's probably more work needed to make it more friendly and open. But I don't see the Foundation work here as picking winners, but about building connections.
In the commitee process, I'm not sensing the sort of hostility to Rust that I might have expected. All the implementors are there.
Josh: The apparent direction of the commitee seems to be at odds with where the compiler implementors are going.
Jon: My experience in the committee is that if the implementors stand up and say that something isn't going to fly for them, that's as close to something of a veto as there is.
Josh: Probably I was saying the inverse, but we can discuss later.
NM: It seems that we should talk about the design axioms. There's also a question here about the degree to which this is a lang question versus the rest of the project. My take is that we ought to drive things within lang where we can but we should also be talking about making interop more of a global focus. As we saw with the RfL work, if we can show success, others will become involved.
nikomatsakis: I would like to see this happen. I'd also like to see us identify the problems we are going to tackle first. This is pretty broad, I'd like to get a sense of "what is the first thing we ought to do". I'd also like to see us talk about how much we need to engage broader project – what is doable just in lang vs engaging cargo etc? It may be good to pick some smaller scale things first.
cramertj: One focus point could be Pin ergonomics– Pin
is frequently relevant when working with C++ types. Many improvements to Pin
could also benefit things like CppRef
(aka AliasRef
).
cramertj: Another smaller-scale focus point is FFI interop APIs (e.g. CString/CStr improvements) and guaranteeing layout for common Rust APIs (e.g. the Guarantee slice representation RFC)
cramertj: (aside) WRT cargo specifically, note that folks using C++ today will not be building their code using cargo, so any solution here has to work within an existing multi-language build system (Make, CMake, Bazel, Buck).
NM: So we have some immediate things here. What do we do after that?
tmandry: I agree with your point that we can start with lang work, but that it will later require broader alignment. We can push the direction that lang is excited about, then we can talk about the compiler and the standard library. But all of that conversation is moot if we decide we don't want anything like that in the first place.
cramertj: Agreed this is a broader project effort. What we're hoping for here is solving some of these prioritization questions. There's a long tail of little nits that make interop with C++ hard or unsound. Some of them are fixable. Some are fundamental where we'll need to figure out APIs to work around, unless we make some bigger changes. There are larger things we could talk about doing. E.g. many of the interop tools want to understand the size and alignment and whatnot of various types. Right now at Google we run rustc_driver
to generate our bindings. But maybe there's something more stable we could do. I'm actually a bit skeptical of this. But as we grow and mature it may become more clear what information language binding generators need about Rust types.
NM: What I'm interested in talking about is the impact and effect that we're hoping to get.
Overall reaction: I think interop is huge and C++ is a key target. I would love to cultivate a less adversarial relationship (no Rust Evangelism Strike Force!) with other languages. There's plenty of code to go around in the world, so I love the idea of working closely with C++ committee but also any other languages that will be our friends (Swift, Go, etc) to prioritize bidirectional interop.
I get excited about the big broad vision docs but both this doc and baumann's do leave me wondering – where do we START! There's so much to do! So I would really like to establish a kind of "first goal". My sense is that we should work to make it possible to consume C++ APIs (I see a lot of people who want to use Rust but have to contend with vendor-supplied C++ APIs and things) and that we should be limiting the fancy C++ features to the ones that will require us to establish new compiler patterns. For example I'd look at template instantiation interacting with monomorphization – basically let's sketch out the "overall shape" we expect things to have and some end-to-end things in that space.
I will say the libkrusty::include
stuff looked nifty. =)
(Post speaking TL;DR: while we should pick off the easy stuff like &Cstr
, we should also pick an ambitious target that is achievable and yet maximally reduces ambiguity.)
+1, cultivating a positive relationship where we can coexist with other languages (both technically and from a community standpoint) is super valuable. We've already seen success in this area with people building Python and Javascript packages in Rust.
C++ interop specifically has high-value for large projects that want to adopt Rust but can't afford an all-at-once transition (and which don't have obvious C-ABI "cut points" allowing for pure-Rust sub-libraries).
One significant point that wasn't discussed as much in this doc was build system interaction. C++ projects won't be using cargo (at first), and frequently use a complex multi-language build system (Make, CMake, Bazel, Buck) whose abilities cargo can't replicate.
For me, I'm happy to see a big story here. I'd like for Rust interop with other languages to be so smooth and to advance so fast that projects like Carbon become redundant.
The user experience effect of what Zig did with leaning into using libclang directly, and in that way trying to be a "better C compiler", always seemed kind of inspiring. But of course we probably don't want to do that exactly.
Can we get there? I don't know. But it's worth thinking big for a moment and seeing what we might be able to do. As the document described, Rust seems to me to be really "having its moment".
At the same time, of course, we still need to protect what Rust is. We can't try to make it something it's not in the pursuit of this. It has to preserve its own integrity. But I think we can strike this balance.
We've talked before, for other reasons, about how we might be able to support a more "pluggable" or "hookable" language and compiler. I wonder if perhaps something like that may have a role to play here also. That is, perhaps in making the language generally more powerful in innovative ways, interop could fall naturally out of that.
There's a small chance I might be biased since I wrote the doc, but I think interop is an important step for Rust's growth and development. Not in terms of whether it survives – we are well past that point – but how well it thrives and how big its impact can be over the long term.
I think we need to work across the project on this. I like Niko's idea of having an ambitious but well scoped "first big goal". At the same time, I think the priotization of granular features will become more be use-case driven.
Interop is clearly important; I'm scared about the details and how much scope creep it means for compiler contributors. I'm optimistic that we can find some sort of 80%+ solution that can leave some of the weirdest bits of C++ as still being kinda ugly, and call that ok.
I wonder if we can successfully make it a separate rustup component, for example, to extend "don't pay for things you don't use" to also not needing to always download another full C++ compiler to use your only-Rust codebase.
Huge for interoperability, both with C and C++.
Agreement on the general axioms, particularly that Rust users should not pay penalties for interoperability features if they're not using them. And encountering a complex language feature in someone else's code is a complexity penalty; it doesn't suffice to say that something doesn't affect people who aren't using it, because people read far more code than they write. Also, a caveat: interoperability features will not be used exclusively for interoperability; any feature we add may be used for any purpose, including purposes we don't expect.
But I think even with that taken into account we can add all the features needed for interoperability with 98% of C++ codebases (which doesn't mean 98% of the C++ spec). We should be prepared to nope a few features if the cost is too high for the benefit, such as (non-normative examples) C++ RTTI. But, for instance, I'd like to see full interoperability with inheritance (inherit from C++ in Rust, inherit from Rust in C++).
I do think we should go further for C than C++. For C I think we should have full native support in rustc for interoperability in both directions. For C++ I think it's acceptable to have external tools/crates/macros required, though I'd like to see "official" tools/crates/macros maintained in the rust-lang organization.
I think rewrites of large codebases (or parts of codebases) are important. And I think one of many huge goals of interoperability should be to allow for incremental, module-by-module rewrites of a codebase into Rust. That's one of many use cases; others include integrating an "engine" written in C/C++, calling a system library, writing a system library, etc.
I think we should measure the success of this initiative by how long it takes, starting from a typical C or C++ codebase, to add some Rust and have it call and be called by the existing code. For C, I want to see that be a 5-minute-or-less endeavor. For C++, I'd propose ~30 minutes or less.
Overall positive about the interop axioms. In particular the lens I'm applying here is from a Wasm Components perspective, where we're also very interested in better Rust integration. I suspect that any efforts towards improved Rust / C++ integration will also help with the integration between Rust and other targets.
Having good C++ interop is really important for adopting Rust at scale. There's very little reason to rewrite 40 year old C codebases. I really like the design axioms, particularly:
Don't make Rust a union of all languages
The libkrust::include!
example looked really slick!
I'm skeptical of approaches that would require building libclang into rustc, but proc macros or similar that do the same thing don't bother me as much.
Niko: There's another axiom I might add: "language interop is an extensibility feature." I'd prefer Rust doesn't need to know about any other language, but that we can still get there. I'm not sure whethere everyone was aligned on this. What's the difference between C and C++ here?
TC: Aligned here. We should always start by seeing if we can just make the language more powerful and see whether the other things we want, like interop, can fall naturally out of that. This also, I think, helps with the concern Josh raised about minimizing complexity in terms of the language surface area.
cramertj: I want to focus in on the example of instantiating std::vector<RustType>
from Rust where RustType
is defined in the current crate. This is really hard, because you have to share type layout information between the Rust and C++ compiler logic with several round trips between them. If we support this, it implies a deeper level of interop between clang and rustc that I'm not sure we want. Swift and Carbon can provide this because they link in Clang.
Connor: The difference between C and C++ is that C++ is a lot more work.
eholk: +1 to the std::vector<RustType>
example. It'd be nice if C++ interop were a special case of general cross-language interop. E.g., if we want Go interop in the future, it'd be nice to not have to include a Go compiler in rustc. Not sure what the integration points in rustc would look like to make that happen though, maybe these requirements are at odds with each other.
scottmcm: What Niko said sounds right to me. The more this can be abstracted out into language features would be good. I don't know how to square that with the vector examples; maybe it's worth special-casing certain things. I also don't know how much people care about other C++ compilers in terms of making this work.
tmandry: Indeed people do use MSVC. The example, cramertj, you brought up in terms of instantiating a vector, is a good one. It points to the kind of differences in architecture we might need. If we had, e.g. a compiler-plugin API, acknowledging that it's a lot of work to do that, then it would decouple that, and it's in line with what Niko is saying.
Josh: The exact use case of vector
is something that I agree should work, along with similar things. I can imagine many different ways we could make that work. If we're not able to go that far, then we're not doing a good job of supporting C++, and that's something we should do.
cramertj: It's harder than it might seem to even just go that far.
NM: That example is what I'd propose as the first milestone, and I don't think that Rust needs to know about clang in order to do that. We can add a callback there. It's probably hard. But we should try as hard as we can to work through it, because I doubt it's impossible. We'll get a lot more out of that.
NM: To Josh's point about C, that all makes sense to me. But I don't think Rust should parse C headers. That's probably the sort of line I'd draw. Do we parse those headers and handle C macros, or do we provide the tools to allow others to do it? It seems to me it should be the latter.
cramertj: It does seem that gets you to a similar place. Those plugins need to be able to do all the things that the compiler does, e.g. evaluate const bodies, and they need to be able to go back and forth multiple times. So they probably need to be in process.
NM: Getting to a version of that we can stabilize will be hard. But it doesn't have to be immediately stable to be usable. So it can probably coevolve. It'd be a useful target for this work that clippy could use these APIs rather than having an unstable connection to rustc
.
tmandry: +1.
Jon: I'm learned we want all the things without any of the complexity.
Jon: As one example, if you can prove certain things about a foreign signature, you can prove its safe. But you can't prove the symbol safe, since you have to assert the signature is correct. It seems like this is the sort of thing that a computer should do.
scottmcm: Agreed humans shouldn't be doing that, but a computer writing the unsafe declation doesn't seem a problem.
NM: +1.
While we are nowhere near this level of capability today from any Rust-based solution, in the long run we expect many of these features to be important for broad adoption. We would therefore like to get agreement on accepting "in principle" any that we can, and to know where the "big red lines" are, if any, that we should not cross.
In particular:
- Can we use interop to justify new language features?
- Can interop boundaries with C++ express semantics not available in Rust?
- Is there a ceiling of how good we want interop to be? Should we leave space for another language between Rust and C++?
Finally, the lang team may want a subteam to iterate on these proposals ahead of their review. This would also be a good place to involve other industry players (other companies, Carbon and Swift if they're interested, etc). If we manage to generate enough interest, is there receptiveness to hosting this work within the Rust Project?
tmandry: I want to
- Can we use interop to justify new language features?
Yes
- Can interop boundaries with C++ express semantics not available in Rust?
Yes
- Is there a ceiling of how good we want interop to be? Should we leave space for another language between Rust and C++?
No but we will evaluate each feature on its merits / cost.
TC: Also we should step back and discuss how to support the feature in general instead of specific features
Niko: And want to define the first milestone better.
scottmcm: Also want to discuss "how far is too far", e.g. with implicit move-constructible types
(The meeting ended here.)
Design axioms
- Pay for what you use: Don't make Rust a worse language for people not using C++.
- Note that for the purpose of this discussion, we don't consider "existence of more features" as a cost unless those features impact everyday Rust users directly.
- Prefer using native Rust mechanisms to express foreign concepts when it's a good fit. Extend the language to remove expressiveness barriers, unless there is a good reason not to.
- Examples: Inherent associated types, Custom reborrow/autoref types
- Don't make Rust a union of all languages: Create well-defined extension points that allow calling interfaces not defineable in Rust.
- Precedent: varargs in
extern "C"
- Support interop in both directions to support evolving an ecosystem one component at a time.
- Automate the boundary and check it. There should be no reason for a human to manually have to do something risky that the machine could do for them.
North star
- Seek to make all C++ APIs callable from unsafe Rust.
- Seek to make all Rust APIs callable from C++.
- Seek to make bindings safe in as many cases as possible.
- Using an API from another language should require effort proportionate to the task.
- Taking on users in another language should require a maintenance burden that is proportionate to the users, and similar to users of the source language.
Note that these are asymptotic: Given the extremely rich, varied and sometimes downright strange surface area of C++ the language, most of these north star goals will never be and should never be met at 100%. That said, if accepted they provide a direction – and an agreement that we would like to move quite a bit farther in that direction than where we are today.
Rust compiler itself interfaces with a number of LLVM C++ APIs through a hand-maintained bindings layer
scottmcm: note that https://github.com/llvm/llvm-project/tree/main/llvm/include/llvm-c is an upstream thing, and covers the majority of how rust calls LLVM, since rustc is hardly the only non-C++ codebase that would like to use LLVM.
tmandry: It exists, but doesn't cover the full surface area of what Rust (and probably most production users) need.
scottmcm: I'd just note that the last 3 times we started using new LLVM features
scottmcm: So I guess my observation here is that I see the direction for full C++ interop as being particularly about "internal" stuff, and less about libraries that want to be consumable from multiple languages, which will want to be exposing a non-C++-phrased ABI anyway.
[[no_unique_address]]
Connor: [[no_unique_address]]
(which is applied to the fields themselves to be clear) can be represented mostly with a ZST on the Rust side* as the effect is to allow empty types (which normally have 1 padding byte in C++) to be stored in zero bytes within a container.
*MSVC does not implement this behaviour however, so using a ZST is not entirely portable. However when targetting Itanium (GNU and non-MSVC clang) compiled code, [[no_unique_address]]
has the semantics that would match a ZST in Rust.
tmandry: It also allows reusing the padding bytes from a type for another field.
Devin: [[no_unique_address]]
is unfortunately far more powerful in ZSTs wrt which types they can optimize. For example, consider this type Bar
: struct Foo {int64_t x; int32_t y;}; struct Bar {[[no_unique_address]] Foo foo; int32_t z;};
. Bar
can have the same layout as struct Bar2 {int64_t x; int32_t y; int32_t z;};
. (It happens not to on e.g. Clang/Linux for some literally trivial reasons – Foo
is "POD for layout" – but you get the idea.) On the flip side, ZSTs can do some things [[no_unique_address]]
cannot (easily) do, like control the exact offset (with repr(C)
), or have multiple ZST fields of the same type at the same address. They only exactly coincide in a few places, and only by coincidence.
Josh: This document, in a couple of places, hypothesizes some general guidelines/policies that are broader than just C++ (e.g. interoperability with arbitrary other languages). I think we should review and consider interoperability with C++ without necessarily setting a precedent we must hold to for other languages. It is absolutely valid for us to observe that C++ interoperability is more important and higher profile than other languages, specifically for Rust because Rust operates in a space that C and C++ and very few other languages are capable of operating in, and our approval of things to support interoperability with C and C++ may go further than the approval we'd give for interoperability with other popular languages that are less closely aligned with the spaces Rust and C and C++ are uniquely positioned to work well in.
In other words: for doing C++ interoperability, and let's just skip setting any broad policy for interoperability with other languages; we can make such decisions on a language by language basis. (Or, in the future, define ways of interoperating with a broad swath of safe languages, I hope.)
nikomatsakis: What concerns do you have about the broader version of the statement? My main concern would be opening the door to wide may cause us to lose focus. I think in general though we ought to be supporting interop as far as possible, and I imagine many of the features under discussion (esp. compiler plugin points) would be useful to other languages. I'd also want to make sure up front the design will scale for that.
Josh: I would like to avoid making normative policy about other languages apart from C and C++, because there's no need to block doing C and C++ interoperability on having a general normative policy for other languages.
Josh: This document talks extensively about interoperability with C++. I think interoperability with C is just as important, if not more so. In particular, I would be prepared to approve going even further for interoperability with C than we do for interoperability with C++.
Concretely, I would like 98% interoperability with real-world C++ code, possibly excluding some things like RTTI that many C++ codebases reject, and possibly requiring the use of external tools/crates/macros/etc. I would like 100% C interoperability using only rustc, and I would not go that far for C++.
cramertj: FWIW, I don't expect 98% interoperability with C++– I think the number is much lower. We can't practically interoperate with complex generic patterns.
Josh: Clarification: 98% of real-world code, not 98% of the specification. ;)
cramertj: I'm referring to real-world code. The STL and other standard libraries (e.g. absl) make extensive use of advanced template metaprogramming that we can't support on the Rust side.
Josh: Something something "sufficiently advanced tools". I can imagine someone building an interop tool / binding tool that could handle generics, built atop clang/LLVM, that can handle arbitrary generics and template metaprogramming. It might require, for instance, that you pre-declare on the Rust side what instantiations you want to use, or that you write some annotations to help with C++ header generation for interop in the other direction.
cramertj: To your original point, I agree C interop is much more achievable than C++ interop, and it would be great to see more built-in support.
Josh:
cramertj: The next question to me is what C compiler and preprocessor we bundle in :). Clang is an obvious choice and would also help with C++ binding generation for folks using clang as their C++ compiler, but support for MSVC is "complicated".
nikomatsakis: +1 to C, but I'm not clear on why it ought to be more built-in to rustc than anything else. Can someone elaborate on that point?
cramertj: I have more reasons to bundle in C++ support natively than I do to bundle in C support– C bindings are much easier to generate ahead-of-time with a tool like bindgen. Josh, what did you have in mind? (one obvious case would be "ergonomics", but we tend to prefer out-of-tree tools where-possible, e.g. more minimal std)
Josh: This is a longer conversation and not necessarily something we should spend time on in this design meeting. Let's have this conversation separately, and make sure we cover all the topics that this design meeting was intended to cover.
That said, to answer those questions:
static inline
functions, ability to "natively" include header files, handle macros in not-awful ways, and not have to teach a second tool about your build system's CFLAGS…#use rust_module
and call Rust functions and instantiate Rust types, effectively extending C with "native" Rust compatibility. And teach it to understand Option and Result and similar types.Location::file_with_nul
et. alConnor: I'd be interested in seeing if, for C++ in particular, std::string_view
and std::u8string_view
could be leveraged as well on the C++ side. That allows use of rusty slice types rather than poluting apis with a bunch of C-ffi support (which isn't necessarily required for interop with C++).
cramertj: The issue is specifically interoperating with existing C and C++ APIs that work with FILE
or std::source_location
, both of which use null-terminated strings.
scottmcm: one part I particularly want to call out here is making sure that we don't weaken things like validity invariants and layout optimizations for everyone.
cramertj: Yes, absolutely– we don't want interop with C++ to pessimize the safety or performance of pure Rust code.
cramertj: At least some of the things we'd like (e.g. unwriteable tail padding) are areas where C++ allows for more efficient behavior than Rust does. e.g. Rust structs and tuples today are much larger than they could be if they were allowed to store data in the tail padding of other values. This is hard to permit with existing unsafe code, however.
Devin: I think aliasing semantics might be a place where "don't pay for what you don't use" might be difficult. For example, tempting to add the Rust equivalent of -fno-strict-aliasing
to allow for mutable aliasing without UB for mixed C++/Rust codebases. (But the reason you would want this is mostly that so much of the Rust ecosystem leans on reference types, which disallow aliasing, so the other way out is to make custom reference types easier to use.)
cramertj: If we did disable noalias/dereferenceable when doing interop, that seems like a reasonable thing to turn off only for projects doing C++ interop, so it would still not pessimize pure-Rust code.
scottmcm: the next thing on the list in the appendix is "custom reference types". What does that mean as distinct from custom self types? Is it things with place integration, things with reborrowing, …?
cramertj: This is types like CppRef
(aka AliasRef
) don't have Rust's dereferenceable
or noalias
requirements. Specifically, we want to bind C++ functions to Rust APIs without adding additional aliasing restrictions on callers.
A naive translation of a mutating C++ method into an &mut self
Rust method makes aliasing assertions that we want to avoid.
scottmcm: Makes sense, I just don't know what CppRef<'a, T>
wants above the custom self stuff to make it a "custom reference type" – you could be thinking Index
support, or…
cramertj: Language-feature wise, we'd like it to autoref (see earlier discussion about autoref for Pin
types), and probably also be #[fundamental]
so that folks can implement traits for CppRef<'_, MyType>
like they can for &T
.
nikomatsakis: At a high-level, I love this doc. I am enthusiastically in favor of adopting interop as a priority for the project (not that we as lang team have say over all of that, but we should do our part). I agree with the design axioms. In general I see this kind of work as joint effort between rust itself (language, compiler, cargo, etc) and crates in the ecosystem. I tend to think of interop crates as just one of many potential "DSL-like" procedural macros and things that could exist, and I think the real focus is on making Rust an extensible language and compiler architecture.
tmandry:
scottmcm: on the topic of "how weird is too weird", would it be worth talking about things like linked lists with move constructors that fixup the pointers? Is the expectation that these types would be usable directly in Rust where a rust move would call such a move constructor, or would it be fine to say those "aren't movable" to Rust and that special functions (or something) need to be used to trigger those behaviours.
cramertj: Today, we bind move constructors to functions that operate on Pin<&mut Self>
references. It would be great to have a more ergonomic path here (e.g. overloaded assignment for these types), but that isn't strictly necessary.
tmandry: Personally I think I'm okay with having to write .move()
or something, just like you have to use std::move()
in C++ for non-rvalues. I think we should start with some kind of emplacement feature that can be used to express this pattern in a more ergonomic way before we really start talking about places where it could become invisible.
yosh: Rust for Linux also has issues with address-stable constructors: The safe pinned initialization problem. This seems related and may point at the need for a broader solution here.
Devin: Crubit has something similar to Rust for Linux here (based on moveit): support/ctor.rs. The ergonomics of it leave much to be desired. For example, syntax to move-construct into a local variable is ctor::emplace!{let mylocal = ctor::mov!(x)}
, which results in mylocal : Pin<&mut X>
, pinning a hidden local variable.
Devin: (By the way, even with Pin
, the existence of Pin::set
actually makes it tricky to use together with non-trivial move semantics. You have to absolutely forbid the existence of these types by value in Rust in any public API.)
jon: at the meeting a few weeks ago in Austria, the committee voted against forwarding Safety Profiles for C++26
. In a prior meeting a study group within the committee voted to pursue Safety Profiles over Sean Baxter's SafeC++ (aka Circle) proposal. My read on the committee at this point is that it acknowledges that Safety Profiles is unlikely to be able to address the memory safety concerns which have been highlighted by industry and government, but the way forward for C++ is unclear. I do think there's potential for the SafeC++ work, but Sean is no longer able to champion it now that he's taken a job at NVIDIA doing unrelated work. That said, I don't think Sean is necessarily the right person to move it forward as the skills to navigate the commitee process are different from the ones to implement the work. I'm engaged in the beginning of a process within the committee to focus on better interop between C++ and all langugages, but with my focus obviously on Rust. My sense from spending time talking to many people during the meeting is that there is a lot of appetite for improving C++ in the ways Rust has demonstrated the benefit of, but there is relatively little understanding of Rust or how it works. I think there's real potential for colloboration between the two communities despite how differently the governance and evolution processes work. The work has begin in terms of building relationships though and I think in future, more participation from willing Rust Project members could be beneficial.
Note that we are using "technical" in a particular way here, to talk about what is possible. It is not about quality or what is more pleasing for the developer. While important, those would fall into the Economic and Social categories of this framework, respectively. ↩︎
bindgen#380, bindgen#607, bindgen#652, bindgen#778, bindgen#1194. List courtesy of https://cxx.rs/context.html#c-vs-c. ↩︎