Carbon’s Bidirectional C++ Interoperability for Incremental Migration
Carbon’s export/import mechanism lets you call existing C++ from Carbon and expose Carbon APIs to C++ without a full rewrite. See a worked example and trade‑offs.
20 Mar 2026, 17:02 UTC

The problem
Large C++ codebases often need incremental upgrades. Rewriting everything in a new language can break tests, delay features, and cost money. Teams want a way to mix new code with legacy C++ without a big‑bang rewrite.
The thesis
Carbon’s export/import mechanism gives a clean ABI boundary that lets you call existing C++ from Carbon and vice versa. Because the compiler generates a matching C++ header for every exported Carbon symbol, a single build system can compile both languages together.
How the boundary works
Exporting Carbon to C++
Annotate a Carbon function with export. The Carbon compiler emits an LLVM IR module and, in the same build step, a C++ header that contains the mangled name and a thin wrapper that follows the Itanium ABI. The header is placed in the target’s output group so C++ code can include it.
Importing C++ into Carbon
Use import to declare a C++ symbol. The declaration specifies the header path and the function signature. The compiler produces a thin wrapper that calls the C++ function with the proper calling convention.
Worked example
Suppose a legacy C++ function factorial lives in factorial.cpp:
// factorial.cpp
#include
extern \"C\" uint64_t factorial(uint64_t n) {
uint64_t r = 1;
for (uint64_t i = 2; i <= n; ++i) r *= i;
return r;
}
Build it with Bazel:
// BUILD
cc_library(
name = \"factorial_lib\",
srcs = [\"factorial.cpp\"],
hdrs = [\"factorial.h\"],
visibility = [\"//visibility:public\"],
)
In Carbon, import the header and expose a new function:
// series.carbon
package demo;
import \"factorial_lib/factorial.h\" as fact;
export fn series_sum(terms: i64) -> f64 {
var total: f64 = 0.0;
for i in 1..terms {
total += 1.0 / fact::factorial(i as u64);
}
return total;
}
When the Carbon target is built, a header series.carbon.h is generated. C++ code can then call it:
#include \"series.carbon.h\"
#include
int main() {
std::cout << \"Sum: \" << series_sum(10) << std::endl;
}
Build the demo with:
bazel build //examples:interop_demo
Verify the generated header contains the expected mangled names and that the binary runs.
Trade‑offs
- Not all C++ features are wrapped automatically. Multiple inheritance or heavy template metaprogramming may need manual adapters.
- The Carbon toolchain is experimental; IDE support and build caching are less mature than for standard Clang.
- Header generation adds a small build overhead, especially for large APIs.
Actionable next steps
- Select a small, self‑contained module to rewrite in Carbon.
- Wrap its C++ functions with
importdeclarations. - Annotate new Carbon functions with
exportto expose them back to C++. - Add both targets to your existing Bazel workspace and run a smoke test.
- Iterate until the call overhead is negligible; the compiler will inline across the boundary when possible.
With this incremental path you can enjoy Carbon’s safety features while keeping the rest of the build functional.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.