This proposal details the toolchain implementation for calling imported C++ functions from Carbon. It covers how C++ overload sets are handled, the process of overload resolution leveraging Clang, and the generation of "thunks" (intermediate functions) when necessary to bridge Application Binary Interface (ABI) differences between Carbon and C++.
Seamless, high-performance interoperability with C++ is a fundamental goal of Carbon. The Carbon language design for C++ interoperability, particularly for function calls, is described in the Carbon calling convention design. To implement that design, the Carbon toolchain must be able to translate Carbon-side calls to C++ functions into instructions the C++ side can understand. Several challenges arise at the toolchain level:
this pointer.A clear, robust implementation strategy is needed to handle these complexities, ensuring both correctness and performance.
Carbon's C++ interoperability philosophy aims to minimize bridge code and provide unsurprising mappings. When Carbon code imports a C++ header, the functions declared within become potentially callable entities. C++ overload resolution rules are complex, and replicating them perfectly within Carbon would be difficult and likely divergent over time. Furthermore, direct calls are only possible when the ABI conventions of the Carbon call site precisely match the expectations of the C++ callee.
Sema determines the best viable function.i32/i64). The thunk internally calls the actual C++ function,
performing necessary argument conversions (for example, loading a value
from a pointer) and handling return value conventions (for example,
managing a return slot).When a C++ header is imported using import Cpp, declarations within that
header are made available. Function declarations, including member functions and
overloaded functions, are represented internally within Carbon's SemIR. An
overload set from C++ is represented as a single callable entity in Carbon,
associated with the set of C++ candidate functions.
To resolve a call like Cpp.MyNamespace.MyFunc(arg1, arg2) where MyFunc might
be an overload set imported from C++:
arg1, arg2) are mapped
to placeholder C++ expressions (conceptually similar to
clang::OpaqueValueExpr).
The types of these expressions are determined by mapping the Carbon argument
types to corresponding C++ types
(Carbon <-> C++ Interop: Primitive Types).clang::OverloadCandidateSet::BestViableFunction())
with the mapped C++ name, the candidate functions from the imported overload
set, and the placeholder argument expressions.public, protected,
private) in the context of the call.A direct call from Carbon to C++ is possible only if the ABI matches exactly. A C++ thunk is required if:
bool (i1) passed to a C++ bool
(often i8), or complex struct types.If a thunk is not required, Carbon emits a direct call instruction targeting the mangled name of the C++ function.
If a thunk is required for a C++ function CppOriginalFunc(), Carbon generates
a new internal function, conceptually CppOriginalFunc__carbon_thunk():
T*).i32, i64, raw pointers) are passed
directly.CppOriginalFunc uses a return slot, the thunk takes a pointer
parameter for the return slot. Its LLVM return type becomes void.CppOriginalFunc returns a simple type directly, the thunk returns
the same simple type directly.i1 to i8 for bool).CppOriginalFunc with the converted arguments, potentially
passing the return slot address.CppOriginalFunc returned directly, the thunk returns that value. If
it used a return slot, the thunk returns void.always_inline to encourage
the optimizer to remove the indirection. It is given a predictable mangled
name based on the original function's mangled name plus a suffix.The Carbon call site then calls the thunk instead of the original C++ function.
void, the Carbon call
expression has type (). If it returns a simple type directly, the Carbon
call has the corresponding mapped Carbon type. If the C++ function uses a
return slot, the Carbon call is modeled as initializing the storage
designated by the return slot argument (often a temporary created at the
call site), and the overall call expression typically results in the
initialized value.object.CppMethod() is called, object becomes
the implicit this argument. Clang's overload resolution handles the
qualification (for example, const). The this pointer is passed as the
first argument, either directly or to the thunk.CppClass::StaticMethod() are treated like
free function calls; no this pointer is involved.Calls to overloaded C++ operators are handled similarly to function calls. Carbon identifies the operator call, looks up potential C++ operator functions (both member and non-member), and uses Clang's overload resolution to select the best candidate. Thunks may be generated if required by the selected operator function's ABI.
Instead of generating thunks automatically, Carbon could require developers to write C++ wrapper functions with simple C-like ABIs for any C++ function whose ABI doesn't directly match Carbon's expectations.
Carbon could define its types and calling conventions to always match a specific C++ ABI (for example, Itanium).
string_view). It conflicts with the
goal of software and language evolution.