From 273411fe02935f07bf8eaf07516b6f1202cb47fc Mon Sep 17 00:00:00 2001 From: Marc Auberer Date: Mon, 15 Jun 2026 00:24:02 +0200 Subject: [PATCH] Emit vtables/type infos with weak ODR linkage for MachO compatibility VTables, type infos and type info names are ODR entities that may legitimately be emitted in more than one translation unit (e.g. an interface that is forward-declared in one file and fully defined in another, as the bootstrap IAbstractAstVisitor is). They were emitted as strong ExternalLinkage symbols relying on a comdat group to coalesce duplicates. comdat is skipped on MachO, so on macOS the duplicate definitions collided with a linker "duplicate symbol" error. Give these symbols weak ODR linkage, matching how Clang emits C++ vtables/type infos. On ELF this pairs with the existing comdat group (behavior unchanged); on MachO the weak/coalesced linkage lets the linker merge the duplicates. Co-Authored-By: Claude Opus 4.8 --- src/irgenerator/GenVTable.cpp | 6 +++--- src/irgenerator/IRGenerator.cpp | 8 ++++++++ src/irgenerator/IRGenerator.h | 1 + .../success-cross-sourcefile-interfaces/ir-code-O2.ll | 6 +++--- .../success-cross-sourcefile-interfaces/ir-code.ll | 6 +++--- 5 files changed, 18 insertions(+), 9 deletions(-) diff --git a/src/irgenerator/GenVTable.cpp b/src/irgenerator/GenVTable.cpp index 49506203d..a96731215 100644 --- a/src/irgenerator/GenVTable.cpp +++ b/src/irgenerator/GenVTable.cpp @@ -27,7 +27,7 @@ llvm::Constant *IRGenerator::generateTypeInfoName(StructBase *spiceStruct) const // Set global attributes global->setConstant(true); global->setUnnamedAddr(llvm::GlobalValue::UnnamedAddr::None); - global->setLinkage(getSymbolLinkageType(isPublic)); + global->setLinkage(getVTableLinkageType(isPublic)); global->setDSOLocal(isSymbolDSOLocal(isPublic)); attachComdatToSymbol(global, globalName, isPublic); @@ -83,7 +83,7 @@ llvm::Constant *IRGenerator::generateTypeInfo(StructBase *spiceStruct) const { // Set global attributes global->setConstant(true); global->setUnnamedAddr(llvm::GlobalValue::UnnamedAddr::None); - global->setLinkage(getSymbolLinkageType(isPublic)); + global->setLinkage(getVTableLinkageType(isPublic)); global->setDSOLocal(isSymbolDSOLocal(isPublic)); global->setAlignment(llvm::MaybeAlign(8)); attachComdatToSymbol(global, mangledName, isPublic); @@ -113,7 +113,7 @@ llvm::Constant *IRGenerator::generateVTable(StructBase *spiceStruct) const { // Set global attributes global->setConstant(true); global->setUnnamedAddr(llvm::GlobalValue::UnnamedAddr::Global); - global->setLinkage(getSymbolLinkageType(isPublic)); + global->setLinkage(getVTableLinkageType(isPublic)); global->setDSOLocal(isSymbolDSOLocal(isPublic)); global->setAlignment(llvm::MaybeAlign(8)); attachComdatToSymbol(global, mangledName, isPublic); diff --git a/src/irgenerator/IRGenerator.cpp b/src/irgenerator/IRGenerator.cpp index 4767d92cb..9c74af44d 100644 --- a/src/irgenerator/IRGenerator.cpp +++ b/src/irgenerator/IRGenerator.cpp @@ -660,6 +660,14 @@ llvm::GlobalValue::LinkageTypes IRGenerator::getSymbolLinkageType(bool isPublic) return isPublic ? llvm::GlobalValue::ExternalLinkage : llvm::GlobalValue::PrivateLinkage; } +llvm::GlobalValue::LinkageTypes IRGenerator::getVTableLinkageType(bool isPublic) const { + // VTables, type infos and type info names are ODR entities that may legitimately be emitted in more than one + // translation unit (e.g. an interface that is forward-declared in one file and fully defined in another). Giving + // them weak ODR linkage lets the linker coalesce the duplicates. On ELF this pairs with the comdat group below; on + // MachO, which has no comdat support, the weak/coalesced linkage is what prevents a duplicate-symbol error. + return isPublic ? llvm::GlobalValue::WeakODRLinkage : llvm::GlobalValue::PrivateLinkage; +} + void IRGenerator::attachComdatToSymbol(llvm::GlobalVariable *global, const std::string &comdatName, bool isPublic) const { // MachO does not support comdat annotations if (isPublic && cliOptions.targetTriple.getObjectFormat() != llvm::Triple::MachO) diff --git a/src/irgenerator/IRGenerator.h b/src/irgenerator/IRGenerator.h index 713ae58ed..f4f144b9c 100644 --- a/src/irgenerator/IRGenerator.h +++ b/src/irgenerator/IRGenerator.h @@ -179,6 +179,7 @@ class IRGenerator final : CompilerPass, public ParallelizableASTVisitor { llvm::Attribute::AttrKind getExtAttrKindForType(const QualType &type) const; bool isSymbolDSOLocal(bool isPublic) const; llvm::GlobalValue::LinkageTypes getSymbolLinkageType(bool isPublic) const; + llvm::GlobalValue::LinkageTypes getVTableLinkageType(bool isPublic) const; void attachComdatToSymbol(llvm::GlobalVariable *global, const std::string &comdatName, bool isPublic) const; // Generate implicit diff --git a/test/test-files/irgenerator/interfaces/success-cross-sourcefile-interfaces/ir-code-O2.ll b/test/test-files/irgenerator/interfaces/success-cross-sourcefile-interfaces/ir-code-O2.ll index 1de98984a..2fcc0df60 100644 --- a/test/test-files/irgenerator/interfaces/success-cross-sourcefile-interfaces/ir-code-O2.ll +++ b/test/test-files/irgenerator/interfaces/success-cross-sourcefile-interfaces/ir-code-O2.ll @@ -10,11 +10,11 @@ $_ZTI3Car = comdat any $_ZTV3Car = comdat any -@_ZTS3Car = dso_local constant [5 x i8] c"3Car\00", comdat, align 4 +@_ZTS3Car = weak_odr dso_local constant [5 x i8] c"3Car\00", comdat, align 4 @_ZTV8TypeInfo = external global ptr @_ZTI9Driveable = external global ptr -@_ZTI3Car = dso_local constant { ptr, ptr, ptr } { ptr getelementptr inbounds (ptr, ptr @_ZTV8TypeInfo, i64 2), ptr @_ZTS3Car, ptr @_ZTI9Driveable }, comdat, align 8 -@_ZTV3Car = dso_local unnamed_addr constant { [4 x ptr] } { [4 x ptr] [ptr null, ptr @_ZTI3Car, ptr @_ZN3Car5driveEi, ptr @_ZN3Car9isDrivingEv] }, comdat, align 8 +@_ZTI3Car = weak_odr dso_local constant { ptr, ptr, ptr } { ptr getelementptr inbounds (ptr, ptr @_ZTV8TypeInfo, i64 2), ptr @_ZTS3Car, ptr @_ZTI9Driveable }, comdat, align 8 +@_ZTV3Car = weak_odr dso_local unnamed_addr constant { [4 x ptr] } { [4 x ptr] [ptr null, ptr @_ZTI3Car, ptr @_ZN3Car5driveEi, ptr @_ZN3Car9isDrivingEv] }, comdat, align 8 @printf.str.0 = private unnamed_addr constant [15 x i8] c"Is driving: %d\00", align 4 ; Function Attrs: mustprogress nofree norecurse nosync nounwind willreturn memory(argmem: write) diff --git a/test/test-files/irgenerator/interfaces/success-cross-sourcefile-interfaces/ir-code.ll b/test/test-files/irgenerator/interfaces/success-cross-sourcefile-interfaces/ir-code.ll index ab2fba93d..52e43183c 100644 --- a/test/test-files/irgenerator/interfaces/success-cross-sourcefile-interfaces/ir-code.ll +++ b/test/test-files/irgenerator/interfaces/success-cross-sourcefile-interfaces/ir-code.ll @@ -10,11 +10,11 @@ $_ZTI3Car = comdat any $_ZTV3Car = comdat any -@_ZTS3Car = dso_local constant [5 x i8] c"3Car\00", comdat, align 4 +@_ZTS3Car = weak_odr dso_local constant [5 x i8] c"3Car\00", comdat, align 4 @_ZTV8TypeInfo = external global ptr @_ZTI9Driveable = external global ptr -@_ZTI3Car = dso_local constant { ptr, ptr, ptr } { ptr getelementptr inbounds (ptr, ptr @_ZTV8TypeInfo, i64 2), ptr @_ZTS3Car, ptr @_ZTI9Driveable }, comdat, align 8 -@_ZTV3Car = dso_local unnamed_addr constant { [4 x ptr] } { [4 x ptr] [ptr null, ptr @_ZTI3Car, ptr @_ZN3Car5driveEi, ptr @_ZN3Car9isDrivingEv] }, comdat, align 8 +@_ZTI3Car = weak_odr dso_local constant { ptr, ptr, ptr } { ptr getelementptr inbounds (ptr, ptr @_ZTV8TypeInfo, i64 2), ptr @_ZTS3Car, ptr @_ZTI9Driveable }, comdat, align 8 +@_ZTV3Car = weak_odr dso_local unnamed_addr constant { [4 x ptr] } { [4 x ptr] [ptr null, ptr @_ZTI3Car, ptr @_ZN3Car5driveEi, ptr @_ZN3Car9isDrivingEv] }, comdat, align 8 @printf.str.0 = private unnamed_addr constant [15 x i8] c"Is driving: %d\00", align 4 define private void @_ZN3Car4ctorEv(ptr noundef nonnull align 8 dereferenceable(16) %0) {