diff --git a/toolchain/check/cpp/import.cpp b/toolchain/check/cpp/import.cpp index 7add37683a30..bcc33ca61bd7 100644 --- a/toolchain/check/cpp/import.cpp +++ b/toolchain/check/cpp/import.cpp @@ -13,6 +13,7 @@ #include "clang/AST/ASTContext.h" #include "clang/AST/RecordLayout.h" #include "clang/AST/UnresolvedSet.h" +#include "clang/AST/VTableBuilder.h" #include "clang/Basic/FileManager.h" #include "clang/Frontend/ASTUnit.h" #include "clang/Frontend/CompilerInstance.h" @@ -1623,6 +1624,21 @@ static auto ImportFunction(Context& context, SemIR::LocId loc_id, AddPlaceholderInstInNoBlock(context, Parse::NodeId::None, function_decl); context.imports().push_back(decl_id); + auto virtual_modifier = SemIR::Function::VirtualModifier::None; + int32_t virtual_index = -1; + if (auto* method_decl = dyn_cast(clang_decl)) { + if (method_decl->size_overridden_methods()) { + virtual_modifier = SemIR::Function::VirtualModifier::Override; + } else if (method_decl->isVirtual()) { + virtual_modifier = SemIR::Function::VirtualModifier::Virtual; + } + if (virtual_modifier != SemIR::Function::VirtualModifier::None) { + // TODO: Add support for Microsoft/non-Itanium vtables. + virtual_index = dyn_cast( + context.ast_context().getVTableContext()) + ->getMethodVTableIndex(method_decl); + } + } auto function_info = SemIR::Function{ {.name_id = GetFunctionName(context, clang_decl), .parent_scope_id = GetParentNameScopeId(context, clang_decl), @@ -1640,7 +1656,8 @@ static auto ImportFunction(Context& context, SemIR::LocId loc_id, .definition_id = SemIR::InstId::None}, {.call_params_id = function_params_insts->call_params_id, .return_slot_pattern_id = function_params_insts->return_slot_pattern_id, - .virtual_modifier = SemIR::FunctionFields::VirtualModifier::None, + .virtual_modifier = virtual_modifier, + .virtual_index = virtual_index, .self_param_id = FindSelfPattern( context, function_params_insts->implicit_param_patterns_id), .clang_decl_id = context.sem_ir().clang_decls().Add( @@ -1675,14 +1692,6 @@ auto ImportCppFunctionDecl(Context& context, SemIR::LocId loc_id, return SemIR::ErrorInst::InstId; } - if (auto* method_decl = dyn_cast(clang_decl)) { - if (method_decl->isVirtual()) { - context.TODO(loc_id, "Unsupported: Virtual function"); - MarkFailedDecl(context, clang_decl); - return SemIR::ErrorInst::InstId; - } - } - CARBON_CHECK(clang_decl->getFunctionType()->isFunctionProtoType(), "Not Prototype function (non-C++ code)"); diff --git a/toolchain/check/testdata/interop/cpp/class/struct.carbon b/toolchain/check/testdata/interop/cpp/class/struct.carbon index e1fcaed70581..20d6e0bbab5c 100644 --- a/toolchain/check/testdata/interop/cpp/class/struct.carbon +++ b/toolchain/check/testdata/interop/cpp/class/struct.carbon @@ -169,7 +169,7 @@ import Cpp library "dynamic.h"; fn MyF(bar: Cpp.Bar*); //@dump-sem-ir-end -// --- fail_todo_call_dynamic.carbon +// --- call_dynamic.carbon library "[[@TEST_NAME]]"; @@ -177,13 +177,6 @@ import Cpp library "dynamic.h"; //@dump-sem-ir-begin fn MyF(bar: Cpp.Bar*) { - // CHECK:STDERR: fail_todo_call_dynamic.carbon:[[@LINE+7]]:3: error: semantics TODO: `Unsupported: Virtual function` [SemanticsTodo] - // CHECK:STDERR: bar->f(); - // CHECK:STDERR: ^~~~~~~~ - // CHECK:STDERR: fail_todo_call_dynamic.carbon:[[@LINE+4]]:3: note: in call to Cpp function here [InCallToCppFunction] - // CHECK:STDERR: bar->f(); - // CHECK:STDERR: ^~~~~~~~ - // CHECK:STDERR: bar->f(); } //@dump-sem-ir-end @@ -473,16 +466,19 @@ fn MyF(bar: Cpp.Bar*); // CHECK:STDOUT: // CHECK:STDOUT: fn @MyF(%bar.param: %ptr); // CHECK:STDOUT: -// CHECK:STDOUT: --- fail_todo_call_dynamic.carbon +// CHECK:STDOUT: --- call_dynamic.carbon // CHECK:STDOUT: // CHECK:STDOUT: constants { // CHECK:STDOUT: %Bar: type = class_type @Bar [concrete] // CHECK:STDOUT: %ptr.f68: type = ptr_type %Bar [concrete] -// CHECK:STDOUT: %pattern_type: type = pattern_type %ptr.f68 [concrete] +// CHECK:STDOUT: %pattern_type.146: type = pattern_type %ptr.f68 [concrete] // CHECK:STDOUT: %MyF.type: type = fn_type @MyF [concrete] +// CHECK:STDOUT: %empty_tuple.type: type = tuple_type () [concrete] // CHECK:STDOUT: %MyF: %MyF.type = struct_value () [concrete] -// CHECK:STDOUT: %.2e2: type = cpp_overload_set_type @ [concrete] +// CHECK:STDOUT: %.2e2: type = cpp_overload_set_type @Bar.f [concrete] // CHECK:STDOUT: %empty_struct: %.2e2 = struct_value () [concrete] +// CHECK:STDOUT: %Bar.f.type: type = fn_type @Bar.f [concrete] +// CHECK:STDOUT: %Bar.f: %Bar.f.type = struct_value () [concrete] // CHECK:STDOUT: } // CHECK:STDOUT: // CHECK:STDOUT: imports { @@ -491,13 +487,18 @@ fn MyF(bar: Cpp.Bar*); // CHECK:STDOUT: import Cpp//... // CHECK:STDOUT: } // CHECK:STDOUT: %Bar.decl: type = class_decl @Bar [concrete = constants.%Bar] {} {} -// CHECK:STDOUT: %.6e0: %.2e2 = cpp_overload_set_value @ [concrete = constants.%empty_struct] +// CHECK:STDOUT: %.6e0: %.2e2 = cpp_overload_set_value @Bar.f [concrete = constants.%empty_struct] +// CHECK:STDOUT: %Bar.f.decl: %Bar.f.type = fn_decl @Bar.f [concrete = constants.%Bar.f] { +// CHECK:STDOUT: +// CHECK:STDOUT: } { +// CHECK:STDOUT: +// CHECK:STDOUT: } // CHECK:STDOUT: } // CHECK:STDOUT: // CHECK:STDOUT: file { // CHECK:STDOUT: %MyF.decl: %MyF.type = fn_decl @MyF [concrete = constants.%MyF] { -// CHECK:STDOUT: %bar.patt: %pattern_type = binding_pattern bar [concrete] -// CHECK:STDOUT: %bar.param_patt: %pattern_type = value_param_pattern %bar.patt, call_param0 [concrete] +// CHECK:STDOUT: %bar.patt: %pattern_type.146 = binding_pattern bar [concrete] +// CHECK:STDOUT: %bar.param_patt: %pattern_type.146 = value_param_pattern %bar.patt, call_param0 [concrete] // CHECK:STDOUT: } { // CHECK:STDOUT: %bar.param: %ptr.f68 = value_param call_param0 // CHECK:STDOUT: %.loc7: type = splice_block %ptr [concrete = constants.%ptr.f68] { @@ -512,9 +513,11 @@ fn MyF(bar: Cpp.Bar*); // CHECK:STDOUT: fn @MyF(%bar.param: %ptr.f68) { // CHECK:STDOUT: !entry: // CHECK:STDOUT: %bar.ref: %ptr.f68 = name_ref bar, %bar -// CHECK:STDOUT: %.loc15: ref %Bar = deref %bar.ref +// CHECK:STDOUT: %.loc8: ref %Bar = deref %bar.ref // CHECK:STDOUT: %f.ref: %.2e2 = name_ref f, imports.%.6e0 [concrete = constants.%empty_struct] -// CHECK:STDOUT: %bound_method: = bound_method %.loc15, %f.ref +// CHECK:STDOUT: %bound_method: = bound_method %.loc8, %f.ref +// CHECK:STDOUT: %addr: %ptr.f68 = addr_of %.loc8 +// CHECK:STDOUT: %Bar.f.call: init %empty_tuple.type = call imports.%Bar.f.decl(%addr) // CHECK:STDOUT: return // CHECK:STDOUT: } // CHECK:STDOUT: diff --git a/toolchain/lower/handle_call.cpp b/toolchain/lower/handle_call.cpp index 2419737f4cc9..986e13f16065 100644 --- a/toolchain/lower/handle_call.cpp +++ b/toolchain/lower/handle_call.cpp @@ -476,6 +476,60 @@ static auto HandleBuiltinCall(FunctionContext& context, SemIR::InstId inst_id, CARBON_FATAL("Unsupported builtin call."); } +static auto HandleVirtualCall(FunctionContext& context, + llvm::ArrayRef args, + const SemIR::File* callee_file, + const SemIR::Function& function, + const SemIR::CalleeFunction& callee_function) + -> llvm::CallInst* { + CARBON_CHECK(!args.empty(), + "Virtual functions must have at least one parameter"); + auto* ptr_type = + llvm::PointerType::get(context.llvm_context(), /*AddressSpace=*/0); + // The vtable pointer is always at the start of the object in the Carbon + // ABI, so a pointer to the object is a pointer to the vtable pointer - load + // that to get a pointer to the vtable. + // TODO: Handle the case in C++ interop where the vtable pointer isn't at + // the start of the object. + // TODO: Use `context.LoadObject`. + auto* vtable = context.builder().CreateLoad(ptr_type, args.front(), "vtable"); + auto* i32_type = llvm::IntegerType::getInt32Ty(context.llvm_context()); + auto* pointer_type = + llvm::PointerType::get(context.llvm_context(), /* address space */ 0); + auto function_type_info = + context.GetFileContext(callee_file) + .BuildFunctionTypeInfo(function, + callee_function.resolved_specific_id); + llvm::Value* virtual_fn; + if (function.clang_decl_id.has_value()) { + // Use absolute vtables for clang interop - the itanium vtable contains + // function pointers. + auto* virtual_function_pointer_address = context.builder().CreateGEP( + pointer_type, vtable, + {llvm::ConstantInt::get( + i32_type, static_cast(function.virtual_index))}); + virtual_fn = context.builder().CreateLoad( + pointer_type, virtual_function_pointer_address, "memptr.virtualfn"); + } else { + // For Carbon, use Relative VTables as pioneered by Fuchsia: + // https://llvm.org/devmtg/2021-11/slides/2021-RelativeVTablesinC.pdf + // In this case, the vtable contains an offset from the vtable itself to the + // function in question. This avoids the use of link-time relocations in the + // vtable (making object files smaller, improving link time) - at the cost + // of extra instructions to resolve the offset at the call-site. + // This uses the `llvm.load.relative` intrinsic ( + // https://llvm.org/docs/LangRef.html#llvm-load-relative-intrinsic ) that + // essentially does the arithmetic in one-shot: ptr + *(ptr + offset) + virtual_fn = context.builder().CreateCall( + llvm::Intrinsic::getOrInsertDeclaration( + &context.llvm_module(), llvm::Intrinsic::load_relative, {i32_type}), + {vtable, + llvm::ConstantInt::get( + i32_type, static_cast(function.virtual_index) * 4)}); + } + return context.builder().CreateCall(function_type_info.type, virtual_fn, + args); +} auto HandleInst(FunctionContext& context, SemIR::InstId inst_id, SemIR::Call inst) -> void { llvm::ArrayRef arg_ids = @@ -542,33 +596,7 @@ auto HandleInst(FunctionContext& context, SemIR::InstId inst_id, } llvm::CallInst* call; - if (function.virtual_index != -1) { - CARBON_CHECK(!args.empty(), - "Virtual functions must have at least one parameter"); - auto* ptr_type = - llvm::PointerType::get(context.llvm_context(), /*AddressSpace=*/0); - // The vtable pointer is always at the start of the object in the Carbon - // ABI, so a pointer to the object is a pointer to the vtable pointer - load - // that to get a pointer to the vtable. - // TODO: Use `context.LoadObject`. - auto* vtable = - context.builder().CreateLoad(ptr_type, args.front(), "vtable"); - auto* i32_type = llvm::IntegerType::getInt32Ty(context.llvm_context()); - auto function_type_info = - context.GetFileContext(callee_file) - .BuildFunctionTypeInfo(function, - callee_function.resolved_specific_id); - call = context.builder().CreateCall( - function_type_info.type, - context.builder().CreateCall( - llvm::Intrinsic::getOrInsertDeclaration( - &context.llvm_module(), llvm::Intrinsic::load_relative, - {i32_type}), - {vtable, - llvm::ConstantInt::get( - i32_type, static_cast(function.virtual_index) * 4)}), - args); - } else { + if (function.virtual_modifier == SemIR::Function::VirtualModifier::None) { auto* callee = context.GetFileContext(callee_file) .GetOrCreateFunction(callee_function.function_id, @@ -590,6 +618,9 @@ auto HandleInst(FunctionContext& context, SemIR::InstId inst_id, CARBON_CHECK(callee->arg_size() == args.size(), "Argument count mismatch: {0}", describe_call()); call = context.builder().CreateCall(callee, args); + } else { + call = HandleVirtualCall(context, args, callee_file, function, + callee_function); } context.SetLocal(inst_id, call); diff --git a/toolchain/lower/testdata/interop/cpp/method.carbon b/toolchain/lower/testdata/interop/cpp/method.carbon index 209f9379727e..2dabde9d713a 100644 --- a/toolchain/lower/testdata/interop/cpp/method.carbon +++ b/toolchain/lower/testdata/interop/cpp/method.carbon @@ -13,9 +13,14 @@ // --- methods.h -struct A { +struct Base { + virtual void virt0(); +}; + +struct A: Base { int by_val() const { return n; } int by_ref() { return n; } + virtual void virt1(); int n; }; @@ -39,6 +44,17 @@ fn UseVal(a: Cpp.A*) -> i32 { return a->by_ref(); } +// --- call_virtual.carbon + +library "[[@TEST_NAME]]"; + +import Cpp library "methods.h"; + +fn UseVal(a: Cpp.A*) { + a->virt0(); + a->virt1(); +} + // --- thunk.h struct NeedThunk { @@ -62,7 +78,8 @@ fn Call(n: Cpp.NeedThunk) { // CHECK:STDOUT: target datalayout = "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-f80:128-n8:16:32:64-S128" // CHECK:STDOUT: target triple = "x86_64-unknown-linux-gnu" // CHECK:STDOUT: -// CHECK:STDOUT: %struct.A = type { i32 } +// CHECK:STDOUT: %struct.A = type <{ %struct.Base, i32, [4 x i8] }> +// CHECK:STDOUT: %struct.Base = type { ptr } // CHECK:STDOUT: // CHECK:STDOUT: $_ZNK1A6by_valEv = comdat any // CHECK:STDOUT: @@ -73,13 +90,13 @@ fn Call(n: Cpp.NeedThunk) { // CHECK:STDOUT: } // CHECK:STDOUT: // CHECK:STDOUT: ; Function Attrs: mustprogress noinline nounwind optnone -// CHECK:STDOUT: define linkonce_odr dso_local i32 @_ZNK1A6by_valEv(ptr nonnull align 4 dereferenceable(4) %this) #0 comdat align 2 { +// CHECK:STDOUT: define linkonce_odr dso_local i32 @_ZNK1A6by_valEv(ptr nonnull align 8 dereferenceable(12) %this) #0 comdat align 2 { // CHECK:STDOUT: entry: // CHECK:STDOUT: %this.addr = alloca ptr, align 8 // CHECK:STDOUT: store ptr %this, ptr %this.addr, align 8 // CHECK:STDOUT: %this1 = load ptr, ptr %this.addr, align 8 -// CHECK:STDOUT: %n = getelementptr inbounds nuw %struct.A, ptr %this1, i32 0, i32 0 -// CHECK:STDOUT: %0 = load i32, ptr %n, align 4 +// CHECK:STDOUT: %n = getelementptr inbounds nuw %struct.A, ptr %this1, i32 0, i32 1 +// CHECK:STDOUT: %0 = load i32, ptr %n, align 8 // CHECK:STDOUT: ret i32 %0 // CHECK:STDOUT: } // CHECK:STDOUT: @@ -89,7 +106,7 @@ fn Call(n: Cpp.NeedThunk) { // CHECK:STDOUT: %this.addr = alloca ptr, align 8 // CHECK:STDOUT: store ptr %this, ptr %this.addr, align 8 // CHECK:STDOUT: %0 = load ptr, ptr %this.addr, align 8 -// CHECK:STDOUT: %call = call i32 @_ZNK1A6by_valEv(ptr nonnull align 4 dereferenceable(4) %0) +// CHECK:STDOUT: %call = call i32 @_ZNK1A6by_valEv(ptr nonnull align 8 dereferenceable(12) %0) // CHECK:STDOUT: ret i32 %call // CHECK:STDOUT: } // CHECK:STDOUT: @@ -116,7 +133,8 @@ fn Call(n: Cpp.NeedThunk) { // CHECK:STDOUT: target datalayout = "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-f80:128-n8:16:32:64-S128" // CHECK:STDOUT: target triple = "x86_64-unknown-linux-gnu" // CHECK:STDOUT: -// CHECK:STDOUT: %struct.A = type { i32 } +// CHECK:STDOUT: %struct.A = type <{ %struct.Base, i32, [4 x i8] }> +// CHECK:STDOUT: %struct.Base = type { ptr } // CHECK:STDOUT: // CHECK:STDOUT: $_ZN1A6by_refEv = comdat any // CHECK:STDOUT: @@ -127,13 +145,13 @@ fn Call(n: Cpp.NeedThunk) { // CHECK:STDOUT: } // CHECK:STDOUT: // CHECK:STDOUT: ; Function Attrs: mustprogress noinline nounwind optnone -// CHECK:STDOUT: define linkonce_odr dso_local i32 @_ZN1A6by_refEv(ptr nonnull align 4 dereferenceable(4) %this) #0 comdat align 2 { +// CHECK:STDOUT: define linkonce_odr dso_local i32 @_ZN1A6by_refEv(ptr nonnull align 8 dereferenceable(12) %this) #0 comdat align 2 { // CHECK:STDOUT: entry: // CHECK:STDOUT: %this.addr = alloca ptr, align 8 // CHECK:STDOUT: store ptr %this, ptr %this.addr, align 8 // CHECK:STDOUT: %this1 = load ptr, ptr %this.addr, align 8 -// CHECK:STDOUT: %n = getelementptr inbounds nuw %struct.A, ptr %this1, i32 0, i32 0 -// CHECK:STDOUT: %0 = load i32, ptr %n, align 4 +// CHECK:STDOUT: %n = getelementptr inbounds nuw %struct.A, ptr %this1, i32 0, i32 1 +// CHECK:STDOUT: %0 = load i32, ptr %n, align 8 // CHECK:STDOUT: ret i32 %0 // CHECK:STDOUT: } // CHECK:STDOUT: @@ -154,6 +172,45 @@ fn Call(n: Cpp.NeedThunk) { // CHECK:STDOUT: !9 = !{} // CHECK:STDOUT: !10 = !DILocation(line: 7, column: 10, scope: !7) // CHECK:STDOUT: !11 = !DILocation(line: 7, column: 3, scope: !7) +// CHECK:STDOUT: ; ModuleID = 'call_virtual.carbon' +// CHECK:STDOUT: source_filename = "call_virtual.carbon" +// CHECK:STDOUT: target datalayout = "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-f80:128-n8:16:32:64-S128" +// CHECK:STDOUT: target triple = "x86_64-unknown-linux-gnu" +// CHECK:STDOUT: +// CHECK:STDOUT: define void @_CUseVal.Main(ptr %a) !dbg !7 { +// CHECK:STDOUT: entry: +// CHECK:STDOUT: %.loc7_4.3.base = getelementptr inbounds nuw [16 x i8], ptr %a, i32 0, i32 0, !dbg !10 +// CHECK:STDOUT: %Base.virt0.call.vtable = load ptr, ptr %.loc7_4.3.base, align 8, !dbg !10 +// CHECK:STDOUT: %Base.virt0.call = getelementptr ptr, ptr %Base.virt0.call.vtable, i32 0, !dbg !10 +// CHECK:STDOUT: %Base.virt0.call.memptr.virtualfn = load ptr, ptr %Base.virt0.call, align 8, !dbg !10 +// CHECK:STDOUT: call void %Base.virt0.call.memptr.virtualfn(ptr %.loc7_4.3.base), !dbg !10 +// CHECK:STDOUT: %A.virt1.call.vtable = load ptr, ptr %a, align 8, !dbg !11 +// CHECK:STDOUT: %A.virt1.call = getelementptr ptr, ptr %A.virt1.call.vtable, i32 1, !dbg !11 +// CHECK:STDOUT: %A.virt1.call.memptr.virtualfn = load ptr, ptr %A.virt1.call, align 8, !dbg !11 +// CHECK:STDOUT: call void %A.virt1.call.memptr.virtualfn(ptr %a), !dbg !11 +// CHECK:STDOUT: ret void, !dbg !12 +// CHECK:STDOUT: } +// CHECK:STDOUT: +// CHECK:STDOUT: declare void @_ZN4Base5virt0Ev(ptr) +// CHECK:STDOUT: +// CHECK:STDOUT: declare void @_ZN1A5virt1Ev(ptr) +// CHECK:STDOUT: +// CHECK:STDOUT: !llvm.module.flags = !{!0, !1, !2, !3, !4} +// CHECK:STDOUT: !llvm.dbg.cu = !{!5} +// CHECK:STDOUT: +// CHECK:STDOUT: !0 = !{i32 7, !"Dwarf Version", i32 5} +// CHECK:STDOUT: !1 = !{i32 2, !"Debug Info Version", i32 3} +// CHECK:STDOUT: !2 = !{i32 1, !"wchar_size", i32 4} +// CHECK:STDOUT: !3 = !{i32 8, !"PIC Level", i32 0} +// CHECK:STDOUT: !4 = !{i32 7, !"PIE Level", i32 2} +// CHECK:STDOUT: !5 = distinct !DICompileUnit(language: DW_LANG_C, file: !6, producer: "carbon", isOptimized: false, runtimeVersion: 0, emissionKind: FullDebug) +// CHECK:STDOUT: !6 = !DIFile(filename: "call_virtual.carbon", directory: "") +// CHECK:STDOUT: !7 = distinct !DISubprogram(name: "UseVal", linkageName: "_CUseVal.Main", scope: null, file: !6, line: 6, type: !8, spFlags: DISPFlagDefinition, unit: !5) +// CHECK:STDOUT: !8 = !DISubroutineType(types: !9) +// CHECK:STDOUT: !9 = !{} +// CHECK:STDOUT: !10 = !DILocation(line: 7, column: 3, scope: !7) +// CHECK:STDOUT: !11 = !DILocation(line: 8, column: 3, scope: !7) +// CHECK:STDOUT: !12 = !DILocation(line: 6, column: 1, scope: !7) // CHECK:STDOUT: ; ModuleID = 'call_thunk.carbon' // CHECK:STDOUT: source_filename = "call_thunk.carbon" // CHECK:STDOUT: target datalayout = "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-f80:128-n8:16:32:64-S128"