toolchain/docs/check/cpp/thunks.md
When C++ code calls Carbon functions, or when Carbon calls C++ code, we need to be able to interoperate between the Carbon ABI and the C++ ABI. We don't want to hardcode all the minutiae of the various C++ ABIs into the Carbon toolchain, so instead we generate thunks with intentionally simple ABIs.
Example:
inline Cpp '''
class X { ... };
X f(X x);
''';
fn G(x: Cpp.X) -> Cpp.X {
return Cpp.f(x);
}
The calling conventions used to pass and return an X object in C++ are very
varied, and depend on various aspects of both the target and of the definition
of X. In order to call f from Carbon, we generate a thunk on the C++ side:
__attribute__((always_inline))
inline void f__carbon_thunk__(void *result, void *x) {
new (result) X(f(*static_cast<X*>(x)));
}
... and notionally create a second thunk on the Carbon side to call it:
fn F:thunk(var x: Cpp.X) -> Cpp.X {
returned var result: Cpp.X;
Cpp.f__carbon_thunk__(&result, &x);
return var;
}
The simple ABI that we use for cross-language calls supports only the following:
void return types.We generate calls with these types by using the corresponding LLVM function type. We assume that this matches the calling convention for these types on the C++ side; in practice, it does for the ABIs that we care about.
We aggressively inline thunks. This happens in two ways:
always_inline attribute in the AST;
in Carbon it happens by emitting the LLVM alwaysinline attribute directly.call instruction whose target is
a Carbon-defined thunk, we instead inline the body of the thunk directly
into the SemIR. This means that we can usually avoid the Carbon-side thunk
entirely.Carbon has another kind of thunk beyond those used for C++ interoperability.
When a function in an interface has a different signature than the corresponding
function in an impl, or a virtual function has a different signature than an
overrider, a thunk is generated to adapt the signature of the function. This
thunk simply implicitly converts each argument to the parameter type, and
implicitly converts the return value to the return type, with one exception: for
a virtual function, the self parameter is converted from the base class type
to the derived class type.
base class A {
virtual fn F(self, n: i32) -> i32;
}
class B {
extend base: A;
override fn F(self, n: i64) -> i16;
}
// Behaves as if this function is in the vtable:
fn B.OverrideF(self: A, n: i32) -> i32 {
// Implicitly converts n from i32 to i64
// Implicitly converts result from i16 to i32
return (self unsafe as B).F(n);
}
These thunks are not the topic of this document, but understanding them is important for understanding the behavior of Carbon overriders of C++ virtual functions.
If a C++ function already has a simple ABI, we don't generate a thunk, and instead we call it directly. Otherwise, we generate a thunk as follows.
On the C++ side, we have two clang::FunctionDecls:
On the Carbon side, we have two SemIR::Functions:
SpecialFunctionKind::HasCppThunk. Attempts to call this function generate
a call through the thunk instead. This is returned when Carbon invokes C++
overload resolution.call
instructions, and is marked as SpecialFunctionKind::CppThunk. This has the
same symbol name as the C++ thunk.Context::clang_decls can be used to map between the corresponding C++ and
Carbon functions above, and Function::cpp_thunk_decl_id and
Function::cpp_thunk_callee can be used to map between the two
SemIR::Functions.
The HasCppThunk function on the Carbon side is only ever directly invoked. It
can't be placed into a witness table or a vtable. Therefore the thunk is
always inlined, and we never generate a Carbon-side
definition for it.
The full story is a little more involved than this: in order to support C++
default arguments (and some other call quirks), each C++ function can map to
multiple different SemIR::Functions with different Carbon-side signatures,
such as having different numbers of parameters. This leads to there being up to
2N SemIR::Functions per C++ function rather than only 2, where N is the number
of function variants in use.
When C++ code calls into Carbon, we always generate a thunk on each side.
On the Carbon side, we have two SemIR::Functions:
SpecialFunctionKind::CppThunk.On the C++ side, we have two clang::FunctionDecls:
Context::clang_decls can be used to map between the corresponding C++ and
Carbon functions above. Function::cpp_thunk_callee can be used to map from the
CppThunk to the original Carbon callee.
When a Carbon class extends a C++ base class and overrides a C++ virtual function, we generate a set of thunks in order to produce a virtual override with the correct signature. Broadly we use a similar pattern to C++ calling Carbon, but with extra complexity because we need to fine-tune the signature of the C++ function, and we need to import it back into Carbon so it can be referenced from the Carbon vtable representation.
Consider the following example of a Carbon class overriding a C++ virtual function:
import Cpp;
inline Cpp '''
struct Base {
virtual void f(int x) = 0;
};
void CallBase(Base& b) { b.f(42); }
''';
class Derived {
extend base: Cpp.Base;
override fn f(self, x: i64) {
...
}
}
This is implemented by combining three other kinds of thunk, as follows:
CppThunk definition that calls the signature
adaptation thunk, and a C++-side definition.In clang_decls, the signature adaptation thunk corresponds to the C++ virtual
override function.
We generate a signature adaptation thunk in Carbon so that any conversions required in order to call the Carbon derived function from the base function signature are performed using Carbon rules, not C++ rules.
We are concerned with two vtables:
Derived is a Carbon class, but in practice it is only
used for constant evaluation on the Carbon side.The Carbon-side vtable contains the signature adaptation thunk. The C++-side vtable contains the corresponding C++ virtual override function.
In the C++ AST:
namespace Carbon {
struct Derived : Base {
// The C++ vtable entry for Derived::f.
// Placed into `Derived`'s C++ vtable. When called from C++,
// it forwards directly to the Carbon thunk.
virtual void f(int x) override {
// The C++ declaration of the Carbon-side thunk.
extern void _Cf__carbon_thunk_Derived_Main(Base* self, int x);
_Cf__carbon_thunk_Derived_Main(this, x);
}
};
}
In Carbon SemIR:
// The user-written Carbon override function.
fn Derived.f(self: Derived, x: i64) {
...
}
// Signature adaptation thunk.
fn Derived.OverrideF(self: Cpp.Base, x: i32) {
// Explicitly converts `self` from `Base` to `Derived`.
// Implicitly converts x from i32 to i64.
(self unsafe as Derived).F(x);
}
// The synthesized Carbon thunk.
fn _Cf__carbon_thunk.Derived.Main(self_ptr: Base*, x: i32) {
// Notionally: `Derived.OverrideF(*self_ptr, x)`, but call is inlined:
(*self_ptr unsafe as Derived).F(x);
}
We can't synthesize the definition of the C++-side virtual overrider until the enclosing class is complete in the C++ AST. Therefore we split the responsibility for generating the thunks in two: