Add return value support to C++ thunks. (#5976)

Based on #5948. A couple of tricky parts:

* When generating the C++ side of the thunk, we are given a pointer to
the location to emplace the return value. The only mechanism C++
provides to perform this emplacement is using placement `operator new`,
which requires a library function in the `<new>` header. We handle this
by declaring that library function ourselves, and rely on Clang not
actually needing a definition for it (which the standard library owns).

* On the Carbon side of the thunk, we want to form an initializing
expression as the result of the call. We don't have a way of expressing
in SemIR that an initializing expression performs its initialization by
storing through a pointer, so this PR adds a new initializing
instruction, `InPlaceInit`, to model an initialization that's performed
opaquely in-place.
This commit is contained in:
Richard Smith
2025-08-26 02:23:38 +00:00
committed by GitHub
parent 1331ade57f
commit d37f1ae6b5
17 changed files with 1039 additions and 96 deletions
+184 -47
View File
@@ -74,34 +74,46 @@ auto IsCppThunkRequired(Context& context, const SemIR::Function& function)
return false;
}
SemIR::TypeId return_type_id =
function.GetDeclaredReturnType(context.sem_ir());
if (return_type_id.has_value()) {
// TODO: Support non-void return values.
if (isa<clang::CXXConstructorDecl>(
context.sem_ir().clang_decls().Get(function.clang_decl_id).decl)) {
// TODO: Support generating thunks for constructors.
return false;
}
bool thunk_required_for_param = false;
// A thunk is required if any parameter or return type requires it. However,
// we don't generate a thunk if any relevant type is erroneous.
bool thunk_required = false;
SemIR::TypeId return_type_id =
function.GetDeclaredReturnType(context.sem_ir());
if (return_type_id.has_value()) {
if (return_type_id == SemIR::ErrorInst::TypeId) {
return false;
}
thunk_required = IsThunkRequiredForType(context, return_type_id);
}
for (auto param_id :
context.inst_blocks().GetOrEmpty(function.call_params_id)) {
if (param_id == SemIR::ErrorInst::InstId) {
return false;
}
if (!thunk_required_for_param &&
if (!thunk_required &&
IsThunkRequiredForType(
context,
context.insts().GetAs<SemIR::AnyParam>(param_id).type_id)) {
thunk_required_for_param = true;
thunk_required = true;
}
}
return thunk_required_for_param;
return thunk_required;
}
// Returns whether the type is a pointer or a signed int of 32 or 64 bits.
// Returns whether the type is void, a pointer, or a signed int of 32 or 64
// bits.
static auto IsSimpleAbiType(clang::ASTContext& ast_context,
clang::QualType type) -> bool {
if (type->isPointerType()) {
if (type->isVoidType() || type->isPointerType()) {
return true;
}
@@ -125,6 +137,8 @@ struct CalleeFunctionInfo {
if (has_object_parameter && method_decl->isImplicitObjectMemberFunction()) {
implicit_this_type = method_decl->getThisType();
}
has_simple_return_type =
IsSimpleAbiType(decl->getASTContext(), decl->getReturnType());
}
// Returns whether this callee has an implicit `this` parameter.
@@ -139,13 +153,23 @@ struct CalleeFunctionInfo {
// Returns the number of parameters the thunk should have.
auto num_thunk_params() const -> unsigned {
return has_implicit_object_parameter() + decl->getNumParams();
return has_implicit_object_parameter() + decl->getNumParams() +
!has_simple_return_type;
}
// Returns the thunk parameter index corresponding to a given callee parameter
// index.
auto GetThunkParamIndex(unsigned callee_param_index) const -> unsigned {
return has_implicit_object_parameter() + callee_param_index;
}
// Returns the thunk parameter index corresponding to the parameter that holds
// the address of the return value.
auto GetThunkReturnParamIndex() const -> unsigned {
CARBON_CHECK(!has_simple_return_type);
return has_implicit_object_parameter() + decl->getNumParams();
}
// The callee function.
clang::FunctionDecl* decl;
@@ -156,9 +180,32 @@ struct CalleeFunctionInfo {
// If the callee has an implicit object parameter, the corresponding `this`
// type. Otherwise a null type.
clang::QualType implicit_this_type;
// Whether the callee has a simple return type, that we can return directly.
// If not, we'll return through an out parameter instead.
bool has_simple_return_type;
};
} // namespace
// Given a pointer type, returns the corresponding _Nonnull-qualified pointer
// type.
static auto GetNonnullType(clang::ASTContext& ast_context,
clang::QualType pointer_type) -> clang::QualType {
return ast_context.getAttributedType(clang::NullabilityKind::NonNull,
pointer_type, pointer_type);
}
// Given the type of a callee parameter, returns the type to use for the
// corresponding thunk parameter.
static auto GetThunkParameterType(clang::ASTContext& ast_context,
clang::QualType callee_type)
-> clang::QualType {
if (IsSimpleAbiType(ast_context, callee_type)) {
return callee_type;
}
return GetNonnullType(ast_context, ast_context.getPointerType(callee_type));
}
// Creates the thunk parameter types given the callee function.
static auto BuildThunkParameterTypes(clang::ASTContext& ast_context,
CalleeFunctionInfo callee_info)
@@ -166,21 +213,22 @@ static auto BuildThunkParameterTypes(clang::ASTContext& ast_context,
llvm::SmallVector<clang::QualType> thunk_param_types;
thunk_param_types.reserve(callee_info.num_thunk_params());
if (callee_info.has_implicit_object_parameter()) {
thunk_param_types.push_back(ast_context.getAttributedType(
clang::NullabilityKind::NonNull, callee_info.implicit_this_type,
callee_info.implicit_this_type));
thunk_param_types.push_back(
GetNonnullType(ast_context, callee_info.implicit_this_type));
}
for (const clang::ParmVarDecl* callee_param :
callee_info.decl->parameters()) {
clang::QualType param_type = callee_param->getType();
bool is_simple_abi_type = IsSimpleAbiType(ast_context, param_type);
if (!is_simple_abi_type) {
clang::QualType pointer_type = ast_context.getPointerType(param_type);
param_type = ast_context.getAttributedType(
clang::NullabilityKind::NonNull, pointer_type, pointer_type);
}
thunk_param_types.push_back(param_type);
// TODO: We should use the type from the function signature, not the type of
// the parameter here.
thunk_param_types.push_back(
GetThunkParameterType(ast_context, callee_param->getType()));
}
if (!callee_info.has_simple_return_type) {
thunk_param_types.push_back(GetNonnullType(
ast_context,
ast_context.getPointerType(callee_info.decl->getReturnType())));
}
CARBON_CHECK(thunk_param_types.size() == callee_info.num_thunk_params());
@@ -220,6 +268,16 @@ static auto BuildThunkParameters(clang::ASTContext& ast_context,
thunk_params.push_back(thunk_param);
}
if (!callee_info.has_simple_return_type) {
clang::ParmVarDecl* thunk_param =
clang::ParmVarDecl::Create(ast_context, thunk_function_decl, clang_loc,
clang_loc, &ast_context.Idents.get("return"),
thunk_function_proto_type->getParamType(
callee_info.GetThunkReturnParamIndex()),
nullptr, clang::SC_None, nullptr);
thunk_params.push_back(thunk_param);
}
CARBON_CHECK(thunk_params.size() == num_thunk_params);
return thunk_params;
}
@@ -237,7 +295,9 @@ static auto CreateThunkFunctionDecl(
auto ext_proto_info = clang::FunctionProtoType::ExtProtoInfo();
clang::QualType thunk_function_type = ast_context.getFunctionType(
callee_info.decl->getReturnType(), thunk_param_types, ext_proto_info);
callee_info.has_simple_return_type ? callee_info.decl->getReturnType()
: ast_context.VoidTy,
thunk_param_types, ext_proto_info);
clang::DeclContext* decl_context = ast_context.getTranslationUnitDecl();
// TODO: Thunks should not have external linkage, consider using `SC_Static`.
@@ -264,11 +324,13 @@ static auto CreateThunkFunctionDecl(
return thunk_function_decl;
}
// Builds a reference to the given parameter thunk, which is carrying a value of
// the given type.
// Builds a reference to the given parameter thunk. If `type` is specified, that
// is the callee parameter type that's being held by the parameter, and
// conversions will be performed as necessary to recover a value of that type.
static auto BuildThunkParamRef(clang::Sema& sema,
clang::FunctionDecl* thunk_function_decl,
unsigned thunk_index, clang::QualType type)
unsigned thunk_index,
clang::QualType type = clang::QualType())
-> clang::Expr* {
clang::ParmVarDecl* thunk_param =
thunk_function_decl->getParamDecl(thunk_index);
@@ -277,7 +339,7 @@ static auto BuildThunkParamRef(clang::Sema& sema,
clang::Expr* call_arg = sema.BuildDeclRefExpr(
thunk_param, thunk_param->getType().getNonReferenceType(),
clang::VK_LValue, clang_loc);
if (thunk_param->getType() != type) {
if (!type.isNull() && thunk_param->getType() != type) {
// TODO: Consider inserting a cast to an rvalue. Note that we currently
// pass pointers to non-temporary objects as the argument when calling a
// thunk, so we'll need to either change that or generate different thunks
@@ -329,6 +391,9 @@ static auto BuildThunkBody(clang::Sema& sema,
clang::FunctionDecl* thunk_function_decl,
CalleeFunctionInfo callee_info)
-> clang::StmtResult {
// TODO: Consider building a CompoundStmt holding our created statement to
// make our result more closely resemble a real C++ function.
clang::SourceLocation clang_loc = callee_info.decl->getLocation();
// If the callee has an object parameter, build a member access expression as
@@ -337,9 +402,9 @@ static auto BuildThunkBody(clang::Sema& sema,
if (callee_info.has_object_parameter) {
auto* object_param_ref =
BuildThunkParamRef(sema, thunk_function_decl, 0,
callee_info.has_implicit_object_parameter()
? thunk_function_decl->getParamDecl(0)->getType()
: callee_info.decl->getParamDecl(0)->getType());
callee_info.has_explicit_object_parameter()
? callee_info.decl->getParamDecl(0)->getType()
: clang::QualType());
bool is_arrow = callee_info.has_implicit_object_parameter();
auto object =
sema.PerformMemberExprBaseConversion(object_param_ref, is_arrow);
@@ -373,9 +438,20 @@ static auto BuildThunkBody(clang::Sema& sema,
return clang::StmtError();
}
// TODO: Consider building a CompoundStmt holding this to make our result more
// closely resemble a real C++ function.
return sema.BuildReturnStmt(clang_loc, call.get());
if (callee_info.has_simple_return_type) {
return sema.BuildReturnStmt(clang_loc, call.get());
}
auto* return_object_addr = BuildThunkParamRef(
sema, thunk_function_decl, callee_info.GetThunkReturnParamIndex());
auto return_type = callee_info.decl->getReturnType();
auto* return_type_info =
sema.Context.getTrivialTypeSourceInfo(return_type, clang_loc);
auto placement_new = sema.BuildCXXNew(
clang_loc, /*UseGlobal=*/true, clang_loc, {return_object_addr}, clang_loc,
/*TypeIdParens=*/clang::SourceRange(), return_type, return_type_info,
/*ArraySize=*/std::nullopt, clang_loc, call.get());
return sema.ActOnExprStmt(placement_new, /*DiscardedValue=*/true);
}
auto BuildCppThunk(Context& context, const SemIR::Function& callee_function)
@@ -413,22 +489,55 @@ auto PerformCppThunkCall(Context& context, SemIR::LocId loc_id,
SemIR::FunctionId callee_function_id,
llvm::ArrayRef<SemIR::InstId> callee_arg_ids,
SemIR::InstId thunk_callee_id) -> SemIR::InstId {
llvm::ArrayRef<SemIR::InstId> callee_function_params =
context.inst_blocks().GetOrEmpty(
context.functions().Get(callee_function_id).call_params_id);
auto& callee_function = context.functions().Get(callee_function_id);
auto callee_function_params =
context.inst_blocks().Get(callee_function.call_params_id);
llvm::ArrayRef<SemIR::InstId> thunk_function_params =
context.inst_blocks().GetOrEmpty(
context.functions()
.Get(GetCalleeFunction(context.sem_ir(), thunk_callee_id)
.function_id)
.call_params_id);
auto thunk_callee = GetCalleeFunction(context.sem_ir(), thunk_callee_id);
auto& thunk_function = context.functions().Get(thunk_callee.function_id);
auto thunk_function_params =
context.inst_blocks().Get(thunk_function.call_params_id);
size_t num_params = callee_function_params.size();
CARBON_CHECK(thunk_function_params.size() == num_params);
CARBON_CHECK(callee_arg_ids.size() == num_params);
// Whether we need to pass a return address to the thunk as a final argument.
bool thunk_takes_return_address =
callee_function.return_slot_pattern_id.has_value() &&
!thunk_function.return_slot_pattern_id.has_value();
// The number of arguments we should be acquiring in order to call the thunk.
// This includes the return address parameter, if any.
unsigned num_thunk_args =
context.inst_blocks().Get(thunk_function.param_patterns_id).size();
// The corresponding number of arguments that would be provided in a syntactic
// call to the callee. This excludes the return slot.
unsigned num_callee_args = num_thunk_args - thunk_takes_return_address;
// Grab the return slot argument, if we were given one.
auto return_slot_id = SemIR::InstId::None;
if (callee_arg_ids.size() == num_callee_args + 1) {
return_slot_id = callee_arg_ids.consume_back();
}
// If there's a return slot pattern, drop the corresponding parameter.
// TODO: The parameter should probably only be created if the return pattern
// actually needs a return address to be passed in.
if (thunk_function.return_slot_pattern_id.has_value()) {
thunk_function_params.consume_back();
}
if (callee_function.return_slot_pattern_id.has_value()) {
callee_function_params.consume_back();
}
// We assume that the call parameters exactly match the parameter patterns for
// both the thunk and the callee. This is currently guaranteed because we only
// create trivial *ParamPatterns when importing a C++ function.
CARBON_CHECK(num_callee_args == callee_function_params.size());
CARBON_CHECK(num_callee_args == callee_arg_ids.size());
CARBON_CHECK(num_thunk_args == thunk_function_params.size());
// Build the thunk arguments by converting the callee arguments as needed.
llvm::SmallVector<SemIR::InstId> thunk_arg_ids;
thunk_arg_ids.reserve(num_params);
thunk_arg_ids.reserve(num_thunk_args);
for (auto [callee_param_inst_id, thunk_param_inst_id, callee_arg_id] :
llvm::zip(callee_function_params, thunk_function_params,
callee_arg_ids)) {
@@ -453,7 +562,35 @@ auto PerformCppThunkCall(Context& context, SemIR::LocId loc_id,
thunk_arg_ids.push_back(arg_id);
}
return PerformCall(context, loc_id, thunk_callee_id, thunk_arg_ids);
// Add an argument to hold the result of the call, if necessary.
auto return_type_id = callee_function.GetDeclaredReturnType(context.sem_ir());
if (thunk_takes_return_address) {
// Create a temporary if the caller didn't provide a return slot.
if (!return_slot_id.has_value()) {
return_slot_id = AddInstWithCleanup<SemIR::TemporaryStorage>(
context, loc_id, {.type_id = return_type_id});
}
auto arg_id = AddInst<SemIR::AddrOf>(
context, loc_id,
{.type_id = GetPointerType(
context, context.types().GetInstId(
context.insts().Get(return_slot_id).type_id())),
.lvalue_id = return_slot_id});
thunk_arg_ids.push_back(arg_id);
}
auto result_id = PerformCall(context, loc_id, thunk_callee_id, thunk_arg_ids);
// Produce the result of the call, taking the value from the return storage.
if (thunk_takes_return_address) {
result_id = AddInst<SemIR::InPlaceInit>(context, loc_id,
{.type_id = return_type_id,
.src_id = result_id,
.dest_id = return_slot_id});
}
return result_id;
}
} // namespace Carbon::Check