Bidirectional interoperability with C and C++
Table of contents
- Philosophy and goals
- Overview
- C++ interoperability model: introduction and principles
- Importing C++ APIs into Carbon
- Calling C++ code from Carbon
- TODO: Accessing C++ classes, structs, and members
- TODO: Accessing global variables
- TODO: Bi-directional type mapping: primitives and core types
- TODO: Advanced type mapping: pointers, references, and
const - TODO: Bi-directional type mapping: standard library types
- TODO: The operator interoperability model
Philosophy and goals
The C++ interoperability layer of Carbon allows a subset of C++ APIs to be accessed from Carbon code, and similarly a subset of Carbon APIs to be accessed from C++ code. This requires expressing one language as a subset of the other. Bridge code may be needed to map some APIs into the relevant subset, but the constraints on expressivity should be loose enough to keep the amount of such bridge code sustainable.
The interoperability philosophy and goals provide more detail.
Overview
Carbon’s bidirectional interoperability with C++ is a cornerstone of its design, enabling a gradual transition from existing C++ codebases. The goal is not just a foreign function interface (FFI), but a seamless, high-fidelity integration that supports advanced C++ features, from templates to class hierarchies.
C++ APIs are imported into Carbon using an import Cpp directive, which makes C++ declarations available within a dedicated Cpp package in Carbon. This prevents name collisions and makes the origin of symbols explicit. Carbon code can then call C++ functions, instantiate C++ classes, and use C++ types, while respecting C++’s semantics, including its complex overload resolution rules and preserving the nominal distinctions between C++ types like long and long long, or T* and T&, which is critical for correct overload resolution and template instantiation.
Similarly, Carbon APIs can be designed to be callable from C++. The interoperability layer is designed to be zero-cost, avoiding unnecessary allocations or copies when calling between the two languages.
C++ interoperability model: introduction and principles
The successor language mandate
The design of Carbon’s C++ interoperability is governed by its foundational goal: to be a successor language, not merely a language with a foreign function interface (FFI). This mandate dictates a design that moves beyond the C-style FFI adopted by most modern languages and instead provides seamless, bidirectional interoperability. The objective is to support deep integration with existing C++ code, encompassing its most complex features, from inheritance to templates.
This goal has profound implications for the Carbon compiler and language semantics. It requires that C++ is not treated as a foreign entity. Instead, Carbon’s semantic model must be co-designed to understand, map, and interact with C++’s semantic constructs—including templates, class hierarchies, and complex overload resolution—with high fidelity. The interoperability layer must, therefore, operate at the semantic analysis level, not just at the linking (ABI) level. This document specifies the design of this semantic contract.
The C++ interop type
A core mechanism in this design is the C++ interop type. This concept defines the “trigger” that activates C++-specific semantic rules within the Carbon compiler. Any operation involving a type that is designated as a C++ interop type could invoke the specialized interoperability logic, such as C++ overload resolution or operator overload resolution that involves both Carbon and C++ operator overloads.
A type is considered a C++ interop type if its definition involves an imported C++ type in any of the following ways:
- A C++ imported type (for example,
Cpp.Widget). - A pointer to a C++ interop type (for example,
Cpp.Widget*). - A Carbon generic type parameterized with a C++ interop type (for example,
MyCarbonVector(Cpp.Widget)).
More generally, a C++ interop type is any type for which Carbon’s orphan rule would allow an impl to be provided by a library in package Cpp.
This “pervasive” model of C++-awareness is a fundamental design choice. The C++ semantics are not confined to a specific unsafe or extern "C++" block; they affect any Carbon type that composes them. For example, when the Carbon compiler instantiates a Carbon generic type like MyCarbonVector(Cpp.Widget), its type system must be aware that the Cpp.Widget parameter carries C++-specific rules. This mandates that Carbon’s own generic system, struct layout logic, overload resolution and operator lookup must query the type system for the presence of a C++ interop type. If present, Carbon must consider C++ rules when operating over C++ interop types. This design prioritizes the goal of a seamless and intuitive user experience.
Importing C++ APIs into Carbon
Importing C++ libraries (header-based)
The primary mechanism for importing existing, user-defined C++ code is through header file inclusion. Carbon must be able to parse and analyze C++ header files to make their declarations available within Carbon.
Syntax: The syntax for this operation is import Cpp library "header_name". This syntax is used for both standard library headers and user-defined headers:
-
Standard Library:
import Cpp library "<cstdio>";This import makes entities like
putcharavailable. -
C++ User-Defined Header:
import Cpp library "circle.h";This import makes user-defined declarations and definitions available.
TODO: Importing C++ code (inline)
Accessing built-in C++ entities (file-less)
Some C++ entities, particularly built-in primitive types, are not defined in any header file. They are “intrinsic” to the C++ language. These entities are available in Carbon without an explicit import declaration.
The Cpp package
A critical design choice for managing C++ imports is the mandatory use of a containing package, Cpp. All imported C++ named entities (functions, types, namespaces) are contained in the Cpp package.
- Functions:
Cpp.putchar(...) - Classes/Types:
Cpp.Circle,Cpp.Point - Constructors:
Cpp.Circle.Circle()
The Cpp. prefix makes the origin of every symbol explicit and unambiguous. It ensures that C++ entities cannot collide with Carbon code.
TODO: Importing C++ macros
Calling C++ code from Carbon
Function call syntax and semantics
Once imported, C++ functions are invoked using standard Carbon function call syntax, prefixed with the Cpp name. The Carbon compiler is responsible for mapping the Carbon arguments to the types expected by the C++ function’s signature.
This often requires explicit casting on the Carbon side, using the as keyword, to satisfy the C++ function’s parameter types.
Example: The following example imports cstdio and calls the C function putchar. The Carbon Core.Char variable n must be cast first to u8 and then to i32 to match the int parameter expected by putchar.
import Cpp library "<cstdio>";
fn Run() {
let hello: array(Core.Char, 6) = ('H', 'e', 'l', 'l', 'o', '!');
for (n: Core.Char in hello) {
// Carbon 'as' casting is used to match the C++ signature
Cpp.putchar((n as u8) as i32);
}
}