[clang][win] MSVC-compat: Use __global_delete wrapper in deleting destructors instead of directly referencing ::operator delete - #188372
Conversation
|
@llvm/pr-subscribers-clang @llvm/pr-subscribers-clang-modules Author: Daniel Paoliello (dpaoliello) ChangesWhen Clang emits scalar/vector deleting destructors for classes with a class-level MSVC handles this by calling This change aligns Clang's behavior with MSVC when MSVC compatibility mode and non-LLVM 21 ABI is used. Patch is 22.50 KiB, truncated to 20.00 KiB below, full version: https://github.com/llvm/llvm-project/pull/188372.diff 13 Files Affected:
diff --git a/clang/docs/ReleaseNotes.rst b/clang/docs/ReleaseNotes.rst
index 0dbe667e4f07a..90ae251ad85a2 100644
--- a/clang/docs/ReleaseNotes.rst
+++ b/clang/docs/ReleaseNotes.rst
@@ -452,6 +452,11 @@ Windows Support
- Clang now defines the ``_MSVC_TRADITIONAL`` macro as ``1`` when emulating MSVC
19.15 (Visual Studio 2017 version 15.8) and later. (#GH47114)
+- In MSVC compatibility mode, scalar and vector deleting destructors now call
+ ``__global_delete`` (a weak external) instead of directly referencing
+ ``::operator delete``. This matches MSVC's behavior and fixes ``LNK2001``
+ linker errors in environments where no global ``::operator delete`` exists.
+
LoongArch Support
^^^^^^^^^^^^^^^^^
diff --git a/clang/lib/CodeGen/CGClass.cpp b/clang/lib/CodeGen/CGClass.cpp
index 6572009fc6003..b3a0d22f403a2 100644
--- a/clang/lib/CodeGen/CGClass.cpp
+++ b/clang/lib/CodeGen/CGClass.cpp
@@ -29,6 +29,7 @@
#include "llvm/IR/Intrinsics.h"
#include "llvm/IR/Metadata.h"
#include "llvm/Support/SaveAndRestore.h"
+#include "llvm/Transforms/Utils/ModuleUtils.h"
#include "llvm/Transforms/Utils/SanitizerStats.h"
#include <optional>
@@ -1435,6 +1436,72 @@ static bool CanSkipVTablePointerInitialization(CodeGenFunction &CGF,
return true;
}
+/// Get or create the MSVC-compatible __global_delete wrapper function.
+///
+/// MSVC's scalar/vector deleting destructors call __global_delete (a weak
+/// external) instead of calling ::operator delete directly. This allows
+/// environments without a global ::operator delete (e.g., kernel mode) to
+/// gracefully fall back to a no-op __empty_global_delete.
+static llvm::Constant *
+getOrCreateMSVCGlobalDeleteWrapper(CodeGenModule &CGM,
+ const FunctionDecl *GlobOD) {
+ llvm::Module &M = CGM.getModule();
+ llvm::LLVMContext &LLVMCtx = M.getContext();
+
+ llvm::Constant *GlobDeleteCallee = CGM.GetAddrOfFunction(GlobOD);
+ auto *GlobDeleteFn = cast<llvm::Function>(GlobDeleteCallee);
+ llvm::FunctionType *FnTy = GlobDeleteFn->getFunctionType();
+
+ // Derive __global_delete and __empty_global_delete mangled names.
+ // Global ::operator delete mangling: ??3@<signature>
+ // Global ::operator delete[] mangling: ??_V@<signature>
+ // We construct:
+ // ?__global_delete@@<signature>
+ // ?__empty_global_delete@@<signature>
+ StringRef GlobDeleteMangledName = GlobDeleteFn->getName();
+ StringRef Signature;
+ if (GlobDeleteMangledName.starts_with("??3@"))
+ Signature = GlobDeleteMangledName.substr(4);
+ else if (GlobDeleteMangledName.starts_with("??_V@"))
+ Signature = GlobDeleteMangledName.substr(5);
+ else
+ llvm_unreachable("unexpected global operator delete mangling");
+
+ std::string GlobalDeleteName =
+ ("?__global_delete@@" + Signature).str();
+ std::string EmptyGlobalDeleteName =
+ ("?__empty_global_delete@@" + Signature).str();
+
+ // Only set up the wrapper once per module.
+ if (llvm::Function *Existing = M.getFunction(GlobalDeleteName))
+ return Existing;
+
+ // Create __empty_global_delete fallback.
+ llvm::Function *EmptyFn = llvm::Function::Create(
+ FnTy, llvm::GlobalValue::LinkOnceODRLinkage,
+ EmptyGlobalDeleteName, &M);
+ EmptyFn->setComdat(M.getOrInsertComdat(EmptyGlobalDeleteName));
+ EmptyFn->setUnnamedAddr(llvm::GlobalValue::UnnamedAddr::Global);
+ auto *BB = llvm::BasicBlock::Create(LLVMCtx, "", EmptyFn);
+ llvm::ReturnInst::Create(LLVMCtx, BB);
+
+ // Emit /ALTERNATENAME linker directive: if __global_delete isn't provided
+ // (e.g., by the CRT), fall back to the no-op __empty_global_delete.
+ std::string AltOption = "/alternatename:" + GlobalDeleteName + "=" +
+ EmptyGlobalDeleteName;
+ auto *AltMD = llvm::MDNode::get(
+ LLVMCtx, {llvm::MDString::get(LLVMCtx, AltOption)});
+ M.getOrInsertNamedMetadata("llvm.linker.options")->addOperand(AltMD);
+
+ // Nothing directly uses this function other than the /alternatename
+ // directive, so explicitly mark it as used.
+ appendToUsed(M, {EmptyFn});
+
+ // Return the __global_delete wrapper function to call.
+ auto GlobalDeleteCallee = M.getOrInsertFunction(GlobalDeleteName, FnTy);
+ return cast<llvm::Function>(GlobalDeleteCallee.getCallee());
+}
+
static void EmitConditionalArrayDtorCall(const CXXDestructorDecl *DD,
CodeGenFunction &CGF,
llvm::Value *ShouldDeleteCondition) {
@@ -1518,9 +1585,14 @@ static void EmitConditionalArrayDtorCall(const CXXDestructorDecl *DD,
CGF.EmitBranchThroughCleanup(CGF.ReturnBlock);
CGF.EmitBlock(GlobDelete);
+ // Use __global_delete wrapper for the global array delete path,
+ // matching MSVC's weak external mechanism.
+ llvm::Constant *GlobalDeleteWrapper =
+ getOrCreateMSVCGlobalDeleteWrapper(
+ CGF.CGM, Dtor->getGlobalArrayOperatorDelete());
CGF.EmitDeleteCall(Dtor->getGlobalArrayOperatorDelete(), allocatedPtr,
CGF.getContext().getCanonicalTagType(ClassDecl),
- numElements, cookieSize);
+ numElements, cookieSize, GlobalDeleteWrapper);
}
} else {
// No operators delete[] were found, so emit a trap.
@@ -1747,9 +1819,12 @@ void EmitConditionalDtorDeleteCall(CodeGenFunction &CGF,
CGF.Builder.CreateCondBr(ShouldCallDelete, continueBB, callDeleteBB);
CGF.EmitBlock(callDeleteBB);
- auto EmitDeleteAndGoToEnd = [&](const FunctionDecl *DeleteOp) {
+ auto EmitDeleteAndGoToEnd = [&](const FunctionDecl *DeleteOp,
+ llvm::Constant *CalleeOverride = nullptr) {
CGF.EmitDeleteCall(DeleteOp, LoadThisForDtorDelete(CGF, Dtor),
- Context.getCanonicalTagType(ClassDecl));
+ Context.getCanonicalTagType(ClassDecl),
+ /*NumElements=*/nullptr, /*CookieSize=*/CharUnits(),
+ CalleeOverride);
if (ReturnAfterDelete)
CGF.EmitBranchThroughCleanup(CGF.ReturnBlock);
else
@@ -1773,7 +1848,13 @@ void EmitConditionalDtorDeleteCall(CodeGenFunction &CGF,
CGF.Builder.CreateCondBr(ShouldCallGlobDelete, ClassDelete, GlobDelete);
CGF.EmitBlock(GlobDelete);
- EmitDeleteAndGoToEnd(GlobOD);
+ // Use __global_delete wrapper instead of directly calling
+ // ::operator delete. This matches MSVC's behavior: __global_delete is a
+ // weak external that falls back to __empty_global_delete (a no-op) when
+ // the CRT doesn't provide it (e.g., kernel-mode environments).
+ llvm::Constant *GlobalDeleteWrapper =
+ getOrCreateMSVCGlobalDeleteWrapper(CGF.CGM, GlobOD);
+ EmitDeleteAndGoToEnd(GlobOD, GlobalDeleteWrapper);
CGF.EmitBlock(ClassDelete);
}
EmitDeleteAndGoToEnd(OD);
diff --git a/clang/lib/CodeGen/CGExprCXX.cpp b/clang/lib/CodeGen/CGExprCXX.cpp
index 82300c3ede183..9e61097b40c71 100644
--- a/clang/lib/CodeGen/CGExprCXX.cpp
+++ b/clang/lib/CodeGen/CGExprCXX.cpp
@@ -1337,9 +1337,12 @@ static void EmitNewInitializer(CodeGenFunction &CGF, const CXXNewExpr *E,
static RValue EmitNewDeleteCall(CodeGenFunction &CGF,
const FunctionDecl *CalleeDecl,
const FunctionProtoType *CalleeType,
- const CallArgList &Args) {
+ const CallArgList &Args,
+ llvm::Constant *CalleeOverride = nullptr) {
llvm::CallBase *CallOrInvoke;
- llvm::Constant *CalleePtr = CGF.CGM.GetAddrOfFunction(CalleeDecl);
+ llvm::Constant *CalleePtr = CalleeOverride
+ ? CalleeOverride
+ : CGF.CGM.GetAddrOfFunction(CalleeDecl);
CGCallee Callee = CGCallee::forDirect(CalleePtr, GlobalDecl(CalleeDecl));
RValue RV = CGF.EmitCall(CGF.CGM.getTypes().arrangeFreeFunctionCall(
Args, CalleeType, /*ChainCall=*/false),
@@ -1788,7 +1791,8 @@ llvm::Value *CodeGenFunction::EmitCXXNewExpr(const CXXNewExpr *E) {
void CodeGenFunction::EmitDeleteCall(const FunctionDecl *DeleteFD,
llvm::Value *DeletePtr, QualType DeleteTy,
llvm::Value *NumElements,
- CharUnits CookieSize) {
+ CharUnits CookieSize,
+ llvm::Constant *CalleeOverride) {
assert((!NumElements && CookieSize.isZero()) ||
DeleteFD->getOverloadedOperator() == OO_Array_Delete);
@@ -1856,7 +1860,7 @@ void CodeGenFunction::EmitDeleteCall(const FunctionDecl *DeleteFD,
"unknown parameter to usual delete function");
// Emit the call to delete.
- EmitNewDeleteCall(*this, DeleteFD, DeleteFTy, DeleteArgs);
+ EmitNewDeleteCall(*this, DeleteFD, DeleteFTy, DeleteArgs, CalleeOverride);
// If call argument lowering didn't use a generated tag argument alloca we
// remove them
diff --git a/clang/lib/CodeGen/CodeGenFunction.h b/clang/lib/CodeGen/CodeGenFunction.h
index 0ff93d2ce7363..5ecb64885b40a 100644
--- a/clang/lib/CodeGen/CodeGenFunction.h
+++ b/clang/lib/CodeGen/CodeGenFunction.h
@@ -3291,7 +3291,8 @@ class CodeGenFunction : public CodeGenTypeCache {
void EmitDeleteCall(const FunctionDecl *DeleteFD, llvm::Value *Ptr,
QualType DeleteTy, llvm::Value *NumElements = nullptr,
- CharUnits CookieSize = CharUnits());
+ CharUnits CookieSize = CharUnits(),
+ llvm::Constant *CalleeOverride = nullptr);
RValue EmitBuiltinNewDeleteCall(const FunctionProtoType *Type,
const CallExpr *TheCallExpr, bool IsDelete);
diff --git a/clang/test/CodeGenCXX/cxx2a-destroying-delete.cpp b/clang/test/CodeGenCXX/cxx2a-destroying-delete.cpp
index c83cb32251462..97ed0c57e8f68 100644
--- a/clang/test/CodeGenCXX/cxx2a-destroying-delete.cpp
+++ b/clang/test/CodeGenCXX/cxx2a-destroying-delete.cpp
@@ -229,8 +229,8 @@ H::~H() { call_in_dtor(); }
// CLANG22-MSABI-NEXT: br i1 %[[CHCK2]], label %dtor.call_class_delete, label %dtor.call_glob_delete
//
// CLANG22-MSABI-LABEL: dtor.call_glob_delete:
-// CLANG22-MSABI64: call void @"??3@YAXPEAX_K@Z"(ptr noundef %{{.*}}, i64 noundef 48)
-// CLANG22-MSABI32: call void @"??3@YAXPAXIW4align_val_t@std@@@Z"(ptr noundef %{{.*}}, i32 noundef 32, i32 noundef 16)
+// CLANG22-MSABI64: call void @"?__global_delete@@YAXPEAX_K@Z"(ptr noundef %{{.*}}, i64 noundef 48)
+// CLANG22-MSABI32: call void @"?__global_delete@@YAXPAXIW4align_val_t@std@@@Z"(ptr noundef %{{.*}}, i32 noundef 32, i32 noundef 16)
// CLANG22-MSABI-NEXT: br label %[[RETURN:.*]]
//
// CLANG21-MSABI: dtor.call_delete:
@@ -284,8 +284,8 @@ I::~I() { call_in_dtor(); }
// CLANG22-MSABI-NEXT: br i1 %[[CHCK2]], label %dtor.call_class_delete, label %dtor.call_glob_delete
//
// CLANG22-MSABI: dtor.call_glob_delete:
-// CLANG22-MSABI64: call void @"??3@YAXPEAX_KW4align_val_t@std@@@Z"(ptr noundef %{{.*}}, i64 noundef 96, i64 noundef 32)
-// CLANG22-MSABI32: call void @"??3@YAXPAXIW4align_val_t@std@@@Z"(ptr noundef %{{.*}}, i32 noundef 64, i32 noundef 32)
+// CLANG22-MSABI64: call void @"?__global_delete@@YAXPEAX_KW4align_val_t@std@@@Z"(ptr noundef %{{.*}}, i64 noundef 96, i64 noundef 32)
+// CLANG22-MSABI32: call void @"?__global_delete@@YAXPAXIW4align_val_t@std@@@Z"(ptr noundef %{{.*}}, i32 noundef 64, i32 noundef 32)
// CLANG22-MSABI-NEXT: br label %[[RETURN:.*]]
//
// CLANG21-MSABI: dtor.call_delete:
diff --git a/clang/test/CodeGenCXX/microsoft-abi-structors.cpp b/clang/test/CodeGenCXX/microsoft-abi-structors.cpp
index 670988fc1ada2..1a4a291e28c0a 100644
--- a/clang/test/CodeGenCXX/microsoft-abi-structors.cpp
+++ b/clang/test/CodeGenCXX/microsoft-abi-structors.cpp
@@ -487,7 +487,7 @@ void checkH() {
// DTORS-NEXT: br i1 %[[CONDITION1]], label %[[CALL_CLASS_DELETE:[0-9a-z._]+]], label %[[CALL_GLOB_DELETE:[0-9a-z._]+]]
//
// DTORS: [[CALL_GLOB_DELETE]]
-// DTORS-NEXT: call void @"??3@YAXPAX@Z"(ptr %[[THIS]])
+// DTORS-NEXT: call void @"?__global_delete@@YAXPAX@Z"(ptr %[[THIS]])
// DTORS-NEXT: br label %[[CONTINUE_LABEL]]
//
// DTORS: [[CALL_CLASS_DELETE]]
diff --git a/clang/test/CodeGenCXX/microsoft-vector-deleting-dtors.cpp b/clang/test/CodeGenCXX/microsoft-vector-deleting-dtors.cpp
index 459c1b6593fa1..766d921fde276 100644
--- a/clang/test/CodeGenCXX/microsoft-vector-deleting-dtors.cpp
+++ b/clang/test/CodeGenCXX/microsoft-vector-deleting-dtors.cpp
@@ -42,6 +42,17 @@ struct AllocatedAsArray : public Bird {
};
+struct KernelBase {
+ static void* operator new(__SIZE_TYPE__ n, int tag = 0);
+ static void operator delete(void* p);
+ static void operator delete[](void* p);
+ virtual ~KernelBase();
+};
+
+struct KernelDerived : KernelBase {
+ virtual ~KernelDerived();
+};
+
// Vector deleting dtor for Bird is an alias because no new Bird[] expressions
// in the TU.
// X64: @"??_EBird@@UEAAPEAXI@Z" = weak dso_local unnamed_addr alias ptr (ptr, i32), ptr @"??_GBird@@UEAAPEAXI@Z"
@@ -83,6 +94,14 @@ void bar() {
sp.foo();
}
+KernelBase::~KernelBase() {}
+KernelDerived::~KernelDerived() {}
+
+void kernelTest() {
+ KernelBase *p = new KernelDerived[2];
+ delete[] p;
+}
+
// CHECK-LABEL: define dso_local void @{{.*}}dealloc{{.*}}(
// CHECK-SAME: ptr noundef %[[PTR:.*]])
// CHECK: entry:
@@ -260,10 +279,30 @@ void bar() {
// X86-NEXT: %[[ARRSZ:.*]] = mul i32 4, %[[COOKIE:.*]]
// X64-NEXT: %[[TOTALSZ:.*]] = add i64 %[[ARRSZ]], 8
// X86-NEXT: %[[TOTALSZ:.*]] = add i32 %[[ARRSZ]], 4
-// X64-NEXT: call void @"??_V@YAXPEAX_K@Z"(ptr noundef %2, i64 noundef %[[TOTALSZ]])
-// X86-NEXT: call void @"??_V@YAXPAXI@Z"(ptr noundef %2, i32 noundef %[[TOTALSZ]])
+// X64-NEXT: call void @"?__global_delete@@YAXPEAX_K@Z"(ptr noundef %2, i64 noundef %[[TOTALSZ]])
+// X86-NEXT: call void @"?__global_delete@@YAXPAXI@Z"(ptr noundef %2, i32 noundef %[[TOTALSZ]])
// CHECK-NEXT: br label %dtor.continue
+// Test that when a class provides its own operator delete, the deleting
+// destructor calls __global_delete (a weak external with no-op fallback)
+// instead of directly referencing ::operator delete. This is critical for
+// environments like kernel mode where no global ::operator delete exists.
+// Verify __empty_global_delete is emitted as a no-op fallback.
+// X64: define linkonce_odr void @"?__empty_global_delete@@YAXPEAX_K@Z"(ptr %0, i64 %1)
+// X64-NEXT: ret void
+// X64-LABEL: define weak dso_local noundef ptr @"??_EKernelDerived@@UEAAPEAXI@Z"
+// Verify the array delete path in the VDD uses __global_delete.
+// X64: dtor.call_glob_delete_after_array_destroy:
+// X64: call void @"?__global_delete@@YAXPEAX_K@Z"(ptr noundef %{{.*}}, i64 noundef %{{.*}})
+// Verify the scalar deleting dtor uses __global_delete, not ::operator delete.
+// X64: dtor.call_delete:
+// X64-NEXT: %[[FLAGCHECK:.*]] = and i32 %should_call_delete2, 4
+// X64-NEXT: %[[ISGLOB:.*]] = icmp eq i32 %[[FLAGCHECK]], 0
+// X64-NEXT: br i1 %[[ISGLOB]], label %dtor.call_class_delete, label %dtor.call_glob_delete
+// X64: dtor.call_glob_delete:
+// X64-NEXT: call void @"?__global_delete@@YAXPEAX_K@Z"(ptr noundef %{{.*}}, i64 noundef 8)
+// X64: dtor.call_class_delete:
+// X64-NEXT: call void @"??3KernelBase@@SAXPEAX@Z"(ptr noundef %{{.*}})
struct BaseDelete1 {
@@ -346,3 +385,8 @@ void foobartest() {
// X64: define weak dso_local noundef ptr @"??_EAllocatedAsArray@@UEAAPEAXI@Z"
// X86: define weak dso_local x86_thiscallcc noundef ptr @"??_EAllocatedAsArray@@UAEPAXI@Z"
// CLANG21: define linkonce_odr dso_local noundef ptr @"??_GAllocatedAsArray@@UEAAPEAXI@Z"
+
+// Verify the /ALTERNATENAME linker directive.
+// X64: !{!"/alternatename:?__global_delete@@YAXPEAX_K@Z=?__empty_global_delete@@YAXPEAX_K@Z"}
+
+// CLANG21-NOT: __global_delete
diff --git a/clang/test/CodeGenCXX/microsoft-vector-deleting-dtors2.cpp b/clang/test/CodeGenCXX/microsoft-vector-deleting-dtors2.cpp
index 6412bf5b1dc3b..b1ea0381b85a8 100644
--- a/clang/test/CodeGenCXX/microsoft-vector-deleting-dtors2.cpp
+++ b/clang/test/CodeGenCXX/microsoft-vector-deleting-dtors2.cpp
@@ -59,7 +59,7 @@ void TesttheTest() {
// X64: define weak dso_local noundef ptr @"??_EDrawingBuffer@@UEAAPEAXI@Z"
// X64: call void @"??1DrawingBuffer@@UEAA@XZ"(ptr noundef nonnull align 8 dereferenceable(8) %arraydestroy.element)
// X64: call void @"??_V?$RefCounted@UDrawingBuffer@@@@SAXPEAX@Z"(ptr noundef %2)
-// X64: call void @"??_V@YAXPEAX_K@Z"(ptr noundef %2, i64 noundef %{{.*}})
+// X64: call void @"?__global_delete@@YAXPEAX_K@Z"(ptr noundef %2, i64 noundef %{{.*}})
// X64: call void @"??1DrawingBuffer@@UEAA@XZ"(ptr noundef nonnull align 8 dereferenceable(8) %this1)
// X64: call void @"??3@YAXPEAX_K@Z"(ptr noundef %this1, i64 noundef {{.*}})
@@ -70,7 +70,7 @@ void TesttheTest() {
// X86: define weak dso_local x86_thiscallcc noundef ptr @"??_EDrawingBuffer@@UAEPAXI@Z"
// X86: call x86_thiscallcc void @"??1DrawingBuffer@@UAE@XZ"(ptr noundef nonnull align 4 dereferenceable(4) %arraydestroy.element)
// X86: call void @"??_V?$RefCounted@UDrawingBuffer@@@@SAXPAX@Z"(ptr noundef %2)
-// X86: call void @"??_V@YAXPAXI@Z"(ptr noundef %2, i32 noundef {{.*}})
+// X86: call void @"?__global_delete@@YAXPAXI@Z"(ptr noundef %2, i32 noundef {{.*}})
// X86 call x86_thiscallcc void @"??1DrawingBuffer@@UAE@XZ"(ptr noundef nonnull align 4 dereferenceable(4) %this1)
// X86: call void @"??3@YAXPAXI@Z"(ptr noundef %this1, i32 noundef {{.*}})
diff --git a/clang/test/CodeGenCXX/msvc-vector-deleting-dtors-sized-delete.cpp b/clang/test/CodeGenCXX/msvc-vector-deleting-dtors-sized-delete.cpp
index 6c9faa88e08e9..db8c429956b6f 100644
--- a/clang/test/CodeGenCXX/msvc-vector-deleting-dtors-sized-delete.cpp
+++ b/clang/test/CodeGenCXX/msvc-vector-deleting-dtors-sized-delete.cpp
@@ -50,5 +50,5 @@ void test() {
// X86-NEXT: %[[ARRSZ1:.*]] = mul i32 12, %[[HOWMANY]]
// X64-NEXT: %[[TOTALSZ1:.*]] = add i64 %[[ARRSZ1]], 8
// X86-NEXT: %[[TOTALSZ1:.*]] = add i32 %[[ARRSZ1]], 4
-// X64-NEXT: call void @"??_V@YAXPEAX_K@Z"(ptr noundef %2, i64 noundef %[[TOTALSZ1]])
-// X86-NEXT: call void @"??_V@YAXPAXI@Z"(ptr noundef %2, i32 noundef %[[TOTALSZ1]])
+// X64-NEXT: call void @"?__global_delete@@YAXPEAX_K@Z"(ptr noundef %2, i64 noundef %[[TOTALSZ1]])
+// X86-NEXT: call void @"?__global_delete@@YAXPAXI@Z"(ptr noundef %2, i32 noundef %[[TOTALSZ1]])
diff --git a/clang/test/Modules/glob-delete-with-virtual-dtor.cpp b/clang/test/Modules/glob-delete-with-virtual-dtor.cpp
index fb2e2a4decf60..18e90aaca78f0 100644
--- a/clang/test/Modules/glob-delete-with-virtual-dtor.cpp
+++ b/clang/test/Modules/glob-delete-with-virtual-dtor.cpp
@@ -30,8 +30,8 @@ void out_of_module_tests() {
// CHECK-NEXT: br i1 %[[CONDITION1]], label %[[CALL_CLASS_DELETE:[0-9a-z._]+]], label %[[CALL_GLOB_DELETE:[0-9a-z._]+]]
//
// CHECK: [[CALL_GLOB_DELETE]]
-// CHECK32-NEXT: call void @"??3@YAXPAXI@Z"
-// CHECK64-NEXT: call void @"??3@YAXPEAX_K@Z"
+// CHECK32-NEXT: call void @"?__global_delete@@YAXPAXI@Z"
+// CHECK64-NEXT: call void @"?__global_delete@@YAXPEAX_K@Z"
// CHECK-NEXT: br label %[[CONTINUE_LABEL]]
//
// CHECK: [[CALL_CLASS_DELETE]]
diff --git a/clang/test/Modules/msvc-vector-deleting-destructors.cpp b/clang/test/Modules/msvc-vector-deleting-destructors.cpp
index 68faa687251d7..9e99ae1e191b7 100644
--- a/clang/test/Modules/msvc-vector-deleting-destructors.cpp
+++ b/clang/test/Modules/msvc-vector-deleting-destructors.cpp
@@ -24,11 +24,11 @@ void out_of_module_tests(Derived *p, Derived *p1) {
// CHECK32-NEXT: %[[ARRSZ:.*]] = mul i32 8, %[[COOKIE:.*]]
// CHECK64-NEXT: %[[TOTALSZ:.*]] = add i64 %[[ARRSZ]], 8
// CHECK32-NEXT: %[[TOTALSZ:.*]] = add i32 %[[ARRSZ]], 4
-// CHECK32-NEXT: call void @"??_V@YAXPAXI@Z"(ptr noundef %2, i32 noundef %[[TOTALSZ]])
-// CHECK64-NEXT: call void @"??_V@YAXPEAX_K@Z"(ptr noundef %2, i64 noundef %[[TOTALSZ]])
+// CHECK32-NEXT: call void @"?__global_delete@@YAXPAXI@Z"(ptr noundef %2, i32 noundef %[[TOTALSZ]])
+// CHECK64-NEXT: call void @"?__global_delete@@YAXPEAX_K@Z"(ptr noundef %2, i64 noundef %[[TOTALSZ]])
// CHECK: dtor.call_glob_delete:
-// CHECK32-NEXT: call void @"??3@YAXPAXI@Z"(ptr noundef %this1, i32 noundef 8)
-// CHECK64-NEXT: call void @"??3@YAXPEAX_K@Z"(ptr noundef %this1, i64 noundef 16)
+// CHECK32-NEXT: call void @"?__global_delete@@YAXPAXI@Z"(ptr noundef %this1, i3...
[truncated]
|
|
@llvm/pr-subscribers-clang-codegen Author: Daniel Paoliello (dpaoliello) ChangesWhen Clang emits scalar/vector deleting destructors for classes with a class-level MSVC handles this by calling This change aligns Clang's behavior with MSVC when MSVC compatibility mode and non-LLVM 21 ABI is used. Patch is 22.50 KiB, truncated to 20.00 KiB below, full version: https://github.com/llvm/llvm-project/pull/188372.diff 13 Files Affected:
diff --git a/clang/docs/ReleaseNotes.rst b/clang/docs/ReleaseNotes.rst
index 0dbe667e4f07a..90ae251ad85a2 100644
--- a/clang/docs/ReleaseNotes.rst
+++ b/clang/docs/ReleaseNotes.rst
@@ -452,6 +452,11 @@ Windows Support
- Clang now defines the ``_MSVC_TRADITIONAL`` macro as ``1`` when emulating MSVC
19.15 (Visual Studio 2017 version 15.8) and later. (#GH47114)
+- In MSVC compatibility mode, scalar and vector deleting destructors now call
+ ``__global_delete`` (a weak external) instead of directly referencing
+ ``::operator delete``. This matches MSVC's behavior and fixes ``LNK2001``
+ linker errors in environments where no global ``::operator delete`` exists.
+
LoongArch Support
^^^^^^^^^^^^^^^^^
diff --git a/clang/lib/CodeGen/CGClass.cpp b/clang/lib/CodeGen/CGClass.cpp
index 6572009fc6003..b3a0d22f403a2 100644
--- a/clang/lib/CodeGen/CGClass.cpp
+++ b/clang/lib/CodeGen/CGClass.cpp
@@ -29,6 +29,7 @@
#include "llvm/IR/Intrinsics.h"
#include "llvm/IR/Metadata.h"
#include "llvm/Support/SaveAndRestore.h"
+#include "llvm/Transforms/Utils/ModuleUtils.h"
#include "llvm/Transforms/Utils/SanitizerStats.h"
#include <optional>
@@ -1435,6 +1436,72 @@ static bool CanSkipVTablePointerInitialization(CodeGenFunction &CGF,
return true;
}
+/// Get or create the MSVC-compatible __global_delete wrapper function.
+///
+/// MSVC's scalar/vector deleting destructors call __global_delete (a weak
+/// external) instead of calling ::operator delete directly. This allows
+/// environments without a global ::operator delete (e.g., kernel mode) to
+/// gracefully fall back to a no-op __empty_global_delete.
+static llvm::Constant *
+getOrCreateMSVCGlobalDeleteWrapper(CodeGenModule &CGM,
+ const FunctionDecl *GlobOD) {
+ llvm::Module &M = CGM.getModule();
+ llvm::LLVMContext &LLVMCtx = M.getContext();
+
+ llvm::Constant *GlobDeleteCallee = CGM.GetAddrOfFunction(GlobOD);
+ auto *GlobDeleteFn = cast<llvm::Function>(GlobDeleteCallee);
+ llvm::FunctionType *FnTy = GlobDeleteFn->getFunctionType();
+
+ // Derive __global_delete and __empty_global_delete mangled names.
+ // Global ::operator delete mangling: ??3@<signature>
+ // Global ::operator delete[] mangling: ??_V@<signature>
+ // We construct:
+ // ?__global_delete@@<signature>
+ // ?__empty_global_delete@@<signature>
+ StringRef GlobDeleteMangledName = GlobDeleteFn->getName();
+ StringRef Signature;
+ if (GlobDeleteMangledName.starts_with("??3@"))
+ Signature = GlobDeleteMangledName.substr(4);
+ else if (GlobDeleteMangledName.starts_with("??_V@"))
+ Signature = GlobDeleteMangledName.substr(5);
+ else
+ llvm_unreachable("unexpected global operator delete mangling");
+
+ std::string GlobalDeleteName =
+ ("?__global_delete@@" + Signature).str();
+ std::string EmptyGlobalDeleteName =
+ ("?__empty_global_delete@@" + Signature).str();
+
+ // Only set up the wrapper once per module.
+ if (llvm::Function *Existing = M.getFunction(GlobalDeleteName))
+ return Existing;
+
+ // Create __empty_global_delete fallback.
+ llvm::Function *EmptyFn = llvm::Function::Create(
+ FnTy, llvm::GlobalValue::LinkOnceODRLinkage,
+ EmptyGlobalDeleteName, &M);
+ EmptyFn->setComdat(M.getOrInsertComdat(EmptyGlobalDeleteName));
+ EmptyFn->setUnnamedAddr(llvm::GlobalValue::UnnamedAddr::Global);
+ auto *BB = llvm::BasicBlock::Create(LLVMCtx, "", EmptyFn);
+ llvm::ReturnInst::Create(LLVMCtx, BB);
+
+ // Emit /ALTERNATENAME linker directive: if __global_delete isn't provided
+ // (e.g., by the CRT), fall back to the no-op __empty_global_delete.
+ std::string AltOption = "/alternatename:" + GlobalDeleteName + "=" +
+ EmptyGlobalDeleteName;
+ auto *AltMD = llvm::MDNode::get(
+ LLVMCtx, {llvm::MDString::get(LLVMCtx, AltOption)});
+ M.getOrInsertNamedMetadata("llvm.linker.options")->addOperand(AltMD);
+
+ // Nothing directly uses this function other than the /alternatename
+ // directive, so explicitly mark it as used.
+ appendToUsed(M, {EmptyFn});
+
+ // Return the __global_delete wrapper function to call.
+ auto GlobalDeleteCallee = M.getOrInsertFunction(GlobalDeleteName, FnTy);
+ return cast<llvm::Function>(GlobalDeleteCallee.getCallee());
+}
+
static void EmitConditionalArrayDtorCall(const CXXDestructorDecl *DD,
CodeGenFunction &CGF,
llvm::Value *ShouldDeleteCondition) {
@@ -1518,9 +1585,14 @@ static void EmitConditionalArrayDtorCall(const CXXDestructorDecl *DD,
CGF.EmitBranchThroughCleanup(CGF.ReturnBlock);
CGF.EmitBlock(GlobDelete);
+ // Use __global_delete wrapper for the global array delete path,
+ // matching MSVC's weak external mechanism.
+ llvm::Constant *GlobalDeleteWrapper =
+ getOrCreateMSVCGlobalDeleteWrapper(
+ CGF.CGM, Dtor->getGlobalArrayOperatorDelete());
CGF.EmitDeleteCall(Dtor->getGlobalArrayOperatorDelete(), allocatedPtr,
CGF.getContext().getCanonicalTagType(ClassDecl),
- numElements, cookieSize);
+ numElements, cookieSize, GlobalDeleteWrapper);
}
} else {
// No operators delete[] were found, so emit a trap.
@@ -1747,9 +1819,12 @@ void EmitConditionalDtorDeleteCall(CodeGenFunction &CGF,
CGF.Builder.CreateCondBr(ShouldCallDelete, continueBB, callDeleteBB);
CGF.EmitBlock(callDeleteBB);
- auto EmitDeleteAndGoToEnd = [&](const FunctionDecl *DeleteOp) {
+ auto EmitDeleteAndGoToEnd = [&](const FunctionDecl *DeleteOp,
+ llvm::Constant *CalleeOverride = nullptr) {
CGF.EmitDeleteCall(DeleteOp, LoadThisForDtorDelete(CGF, Dtor),
- Context.getCanonicalTagType(ClassDecl));
+ Context.getCanonicalTagType(ClassDecl),
+ /*NumElements=*/nullptr, /*CookieSize=*/CharUnits(),
+ CalleeOverride);
if (ReturnAfterDelete)
CGF.EmitBranchThroughCleanup(CGF.ReturnBlock);
else
@@ -1773,7 +1848,13 @@ void EmitConditionalDtorDeleteCall(CodeGenFunction &CGF,
CGF.Builder.CreateCondBr(ShouldCallGlobDelete, ClassDelete, GlobDelete);
CGF.EmitBlock(GlobDelete);
- EmitDeleteAndGoToEnd(GlobOD);
+ // Use __global_delete wrapper instead of directly calling
+ // ::operator delete. This matches MSVC's behavior: __global_delete is a
+ // weak external that falls back to __empty_global_delete (a no-op) when
+ // the CRT doesn't provide it (e.g., kernel-mode environments).
+ llvm::Constant *GlobalDeleteWrapper =
+ getOrCreateMSVCGlobalDeleteWrapper(CGF.CGM, GlobOD);
+ EmitDeleteAndGoToEnd(GlobOD, GlobalDeleteWrapper);
CGF.EmitBlock(ClassDelete);
}
EmitDeleteAndGoToEnd(OD);
diff --git a/clang/lib/CodeGen/CGExprCXX.cpp b/clang/lib/CodeGen/CGExprCXX.cpp
index 82300c3ede183..9e61097b40c71 100644
--- a/clang/lib/CodeGen/CGExprCXX.cpp
+++ b/clang/lib/CodeGen/CGExprCXX.cpp
@@ -1337,9 +1337,12 @@ static void EmitNewInitializer(CodeGenFunction &CGF, const CXXNewExpr *E,
static RValue EmitNewDeleteCall(CodeGenFunction &CGF,
const FunctionDecl *CalleeDecl,
const FunctionProtoType *CalleeType,
- const CallArgList &Args) {
+ const CallArgList &Args,
+ llvm::Constant *CalleeOverride = nullptr) {
llvm::CallBase *CallOrInvoke;
- llvm::Constant *CalleePtr = CGF.CGM.GetAddrOfFunction(CalleeDecl);
+ llvm::Constant *CalleePtr = CalleeOverride
+ ? CalleeOverride
+ : CGF.CGM.GetAddrOfFunction(CalleeDecl);
CGCallee Callee = CGCallee::forDirect(CalleePtr, GlobalDecl(CalleeDecl));
RValue RV = CGF.EmitCall(CGF.CGM.getTypes().arrangeFreeFunctionCall(
Args, CalleeType, /*ChainCall=*/false),
@@ -1788,7 +1791,8 @@ llvm::Value *CodeGenFunction::EmitCXXNewExpr(const CXXNewExpr *E) {
void CodeGenFunction::EmitDeleteCall(const FunctionDecl *DeleteFD,
llvm::Value *DeletePtr, QualType DeleteTy,
llvm::Value *NumElements,
- CharUnits CookieSize) {
+ CharUnits CookieSize,
+ llvm::Constant *CalleeOverride) {
assert((!NumElements && CookieSize.isZero()) ||
DeleteFD->getOverloadedOperator() == OO_Array_Delete);
@@ -1856,7 +1860,7 @@ void CodeGenFunction::EmitDeleteCall(const FunctionDecl *DeleteFD,
"unknown parameter to usual delete function");
// Emit the call to delete.
- EmitNewDeleteCall(*this, DeleteFD, DeleteFTy, DeleteArgs);
+ EmitNewDeleteCall(*this, DeleteFD, DeleteFTy, DeleteArgs, CalleeOverride);
// If call argument lowering didn't use a generated tag argument alloca we
// remove them
diff --git a/clang/lib/CodeGen/CodeGenFunction.h b/clang/lib/CodeGen/CodeGenFunction.h
index 0ff93d2ce7363..5ecb64885b40a 100644
--- a/clang/lib/CodeGen/CodeGenFunction.h
+++ b/clang/lib/CodeGen/CodeGenFunction.h
@@ -3291,7 +3291,8 @@ class CodeGenFunction : public CodeGenTypeCache {
void EmitDeleteCall(const FunctionDecl *DeleteFD, llvm::Value *Ptr,
QualType DeleteTy, llvm::Value *NumElements = nullptr,
- CharUnits CookieSize = CharUnits());
+ CharUnits CookieSize = CharUnits(),
+ llvm::Constant *CalleeOverride = nullptr);
RValue EmitBuiltinNewDeleteCall(const FunctionProtoType *Type,
const CallExpr *TheCallExpr, bool IsDelete);
diff --git a/clang/test/CodeGenCXX/cxx2a-destroying-delete.cpp b/clang/test/CodeGenCXX/cxx2a-destroying-delete.cpp
index c83cb32251462..97ed0c57e8f68 100644
--- a/clang/test/CodeGenCXX/cxx2a-destroying-delete.cpp
+++ b/clang/test/CodeGenCXX/cxx2a-destroying-delete.cpp
@@ -229,8 +229,8 @@ H::~H() { call_in_dtor(); }
// CLANG22-MSABI-NEXT: br i1 %[[CHCK2]], label %dtor.call_class_delete, label %dtor.call_glob_delete
//
// CLANG22-MSABI-LABEL: dtor.call_glob_delete:
-// CLANG22-MSABI64: call void @"??3@YAXPEAX_K@Z"(ptr noundef %{{.*}}, i64 noundef 48)
-// CLANG22-MSABI32: call void @"??3@YAXPAXIW4align_val_t@std@@@Z"(ptr noundef %{{.*}}, i32 noundef 32, i32 noundef 16)
+// CLANG22-MSABI64: call void @"?__global_delete@@YAXPEAX_K@Z"(ptr noundef %{{.*}}, i64 noundef 48)
+// CLANG22-MSABI32: call void @"?__global_delete@@YAXPAXIW4align_val_t@std@@@Z"(ptr noundef %{{.*}}, i32 noundef 32, i32 noundef 16)
// CLANG22-MSABI-NEXT: br label %[[RETURN:.*]]
//
// CLANG21-MSABI: dtor.call_delete:
@@ -284,8 +284,8 @@ I::~I() { call_in_dtor(); }
// CLANG22-MSABI-NEXT: br i1 %[[CHCK2]], label %dtor.call_class_delete, label %dtor.call_glob_delete
//
// CLANG22-MSABI: dtor.call_glob_delete:
-// CLANG22-MSABI64: call void @"??3@YAXPEAX_KW4align_val_t@std@@@Z"(ptr noundef %{{.*}}, i64 noundef 96, i64 noundef 32)
-// CLANG22-MSABI32: call void @"??3@YAXPAXIW4align_val_t@std@@@Z"(ptr noundef %{{.*}}, i32 noundef 64, i32 noundef 32)
+// CLANG22-MSABI64: call void @"?__global_delete@@YAXPEAX_KW4align_val_t@std@@@Z"(ptr noundef %{{.*}}, i64 noundef 96, i64 noundef 32)
+// CLANG22-MSABI32: call void @"?__global_delete@@YAXPAXIW4align_val_t@std@@@Z"(ptr noundef %{{.*}}, i32 noundef 64, i32 noundef 32)
// CLANG22-MSABI-NEXT: br label %[[RETURN:.*]]
//
// CLANG21-MSABI: dtor.call_delete:
diff --git a/clang/test/CodeGenCXX/microsoft-abi-structors.cpp b/clang/test/CodeGenCXX/microsoft-abi-structors.cpp
index 670988fc1ada2..1a4a291e28c0a 100644
--- a/clang/test/CodeGenCXX/microsoft-abi-structors.cpp
+++ b/clang/test/CodeGenCXX/microsoft-abi-structors.cpp
@@ -487,7 +487,7 @@ void checkH() {
// DTORS-NEXT: br i1 %[[CONDITION1]], label %[[CALL_CLASS_DELETE:[0-9a-z._]+]], label %[[CALL_GLOB_DELETE:[0-9a-z._]+]]
//
// DTORS: [[CALL_GLOB_DELETE]]
-// DTORS-NEXT: call void @"??3@YAXPAX@Z"(ptr %[[THIS]])
+// DTORS-NEXT: call void @"?__global_delete@@YAXPAX@Z"(ptr %[[THIS]])
// DTORS-NEXT: br label %[[CONTINUE_LABEL]]
//
// DTORS: [[CALL_CLASS_DELETE]]
diff --git a/clang/test/CodeGenCXX/microsoft-vector-deleting-dtors.cpp b/clang/test/CodeGenCXX/microsoft-vector-deleting-dtors.cpp
index 459c1b6593fa1..766d921fde276 100644
--- a/clang/test/CodeGenCXX/microsoft-vector-deleting-dtors.cpp
+++ b/clang/test/CodeGenCXX/microsoft-vector-deleting-dtors.cpp
@@ -42,6 +42,17 @@ struct AllocatedAsArray : public Bird {
};
+struct KernelBase {
+ static void* operator new(__SIZE_TYPE__ n, int tag = 0);
+ static void operator delete(void* p);
+ static void operator delete[](void* p);
+ virtual ~KernelBase();
+};
+
+struct KernelDerived : KernelBase {
+ virtual ~KernelDerived();
+};
+
// Vector deleting dtor for Bird is an alias because no new Bird[] expressions
// in the TU.
// X64: @"??_EBird@@UEAAPEAXI@Z" = weak dso_local unnamed_addr alias ptr (ptr, i32), ptr @"??_GBird@@UEAAPEAXI@Z"
@@ -83,6 +94,14 @@ void bar() {
sp.foo();
}
+KernelBase::~KernelBase() {}
+KernelDerived::~KernelDerived() {}
+
+void kernelTest() {
+ KernelBase *p = new KernelDerived[2];
+ delete[] p;
+}
+
// CHECK-LABEL: define dso_local void @{{.*}}dealloc{{.*}}(
// CHECK-SAME: ptr noundef %[[PTR:.*]])
// CHECK: entry:
@@ -260,10 +279,30 @@ void bar() {
// X86-NEXT: %[[ARRSZ:.*]] = mul i32 4, %[[COOKIE:.*]]
// X64-NEXT: %[[TOTALSZ:.*]] = add i64 %[[ARRSZ]], 8
// X86-NEXT: %[[TOTALSZ:.*]] = add i32 %[[ARRSZ]], 4
-// X64-NEXT: call void @"??_V@YAXPEAX_K@Z"(ptr noundef %2, i64 noundef %[[TOTALSZ]])
-// X86-NEXT: call void @"??_V@YAXPAXI@Z"(ptr noundef %2, i32 noundef %[[TOTALSZ]])
+// X64-NEXT: call void @"?__global_delete@@YAXPEAX_K@Z"(ptr noundef %2, i64 noundef %[[TOTALSZ]])
+// X86-NEXT: call void @"?__global_delete@@YAXPAXI@Z"(ptr noundef %2, i32 noundef %[[TOTALSZ]])
// CHECK-NEXT: br label %dtor.continue
+// Test that when a class provides its own operator delete, the deleting
+// destructor calls __global_delete (a weak external with no-op fallback)
+// instead of directly referencing ::operator delete. This is critical for
+// environments like kernel mode where no global ::operator delete exists.
+// Verify __empty_global_delete is emitted as a no-op fallback.
+// X64: define linkonce_odr void @"?__empty_global_delete@@YAXPEAX_K@Z"(ptr %0, i64 %1)
+// X64-NEXT: ret void
+// X64-LABEL: define weak dso_local noundef ptr @"??_EKernelDerived@@UEAAPEAXI@Z"
+// Verify the array delete path in the VDD uses __global_delete.
+// X64: dtor.call_glob_delete_after_array_destroy:
+// X64: call void @"?__global_delete@@YAXPEAX_K@Z"(ptr noundef %{{.*}}, i64 noundef %{{.*}})
+// Verify the scalar deleting dtor uses __global_delete, not ::operator delete.
+// X64: dtor.call_delete:
+// X64-NEXT: %[[FLAGCHECK:.*]] = and i32 %should_call_delete2, 4
+// X64-NEXT: %[[ISGLOB:.*]] = icmp eq i32 %[[FLAGCHECK]], 0
+// X64-NEXT: br i1 %[[ISGLOB]], label %dtor.call_class_delete, label %dtor.call_glob_delete
+// X64: dtor.call_glob_delete:
+// X64-NEXT: call void @"?__global_delete@@YAXPEAX_K@Z"(ptr noundef %{{.*}}, i64 noundef 8)
+// X64: dtor.call_class_delete:
+// X64-NEXT: call void @"??3KernelBase@@SAXPEAX@Z"(ptr noundef %{{.*}})
struct BaseDelete1 {
@@ -346,3 +385,8 @@ void foobartest() {
// X64: define weak dso_local noundef ptr @"??_EAllocatedAsArray@@UEAAPEAXI@Z"
// X86: define weak dso_local x86_thiscallcc noundef ptr @"??_EAllocatedAsArray@@UAEPAXI@Z"
// CLANG21: define linkonce_odr dso_local noundef ptr @"??_GAllocatedAsArray@@UEAAPEAXI@Z"
+
+// Verify the /ALTERNATENAME linker directive.
+// X64: !{!"/alternatename:?__global_delete@@YAXPEAX_K@Z=?__empty_global_delete@@YAXPEAX_K@Z"}
+
+// CLANG21-NOT: __global_delete
diff --git a/clang/test/CodeGenCXX/microsoft-vector-deleting-dtors2.cpp b/clang/test/CodeGenCXX/microsoft-vector-deleting-dtors2.cpp
index 6412bf5b1dc3b..b1ea0381b85a8 100644
--- a/clang/test/CodeGenCXX/microsoft-vector-deleting-dtors2.cpp
+++ b/clang/test/CodeGenCXX/microsoft-vector-deleting-dtors2.cpp
@@ -59,7 +59,7 @@ void TesttheTest() {
// X64: define weak dso_local noundef ptr @"??_EDrawingBuffer@@UEAAPEAXI@Z"
// X64: call void @"??1DrawingBuffer@@UEAA@XZ"(ptr noundef nonnull align 8 dereferenceable(8) %arraydestroy.element)
// X64: call void @"??_V?$RefCounted@UDrawingBuffer@@@@SAXPEAX@Z"(ptr noundef %2)
-// X64: call void @"??_V@YAXPEAX_K@Z"(ptr noundef %2, i64 noundef %{{.*}})
+// X64: call void @"?__global_delete@@YAXPEAX_K@Z"(ptr noundef %2, i64 noundef %{{.*}})
// X64: call void @"??1DrawingBuffer@@UEAA@XZ"(ptr noundef nonnull align 8 dereferenceable(8) %this1)
// X64: call void @"??3@YAXPEAX_K@Z"(ptr noundef %this1, i64 noundef {{.*}})
@@ -70,7 +70,7 @@ void TesttheTest() {
// X86: define weak dso_local x86_thiscallcc noundef ptr @"??_EDrawingBuffer@@UAEPAXI@Z"
// X86: call x86_thiscallcc void @"??1DrawingBuffer@@UAE@XZ"(ptr noundef nonnull align 4 dereferenceable(4) %arraydestroy.element)
// X86: call void @"??_V?$RefCounted@UDrawingBuffer@@@@SAXPAX@Z"(ptr noundef %2)
-// X86: call void @"??_V@YAXPAXI@Z"(ptr noundef %2, i32 noundef {{.*}})
+// X86: call void @"?__global_delete@@YAXPAXI@Z"(ptr noundef %2, i32 noundef {{.*}})
// X86 call x86_thiscallcc void @"??1DrawingBuffer@@UAE@XZ"(ptr noundef nonnull align 4 dereferenceable(4) %this1)
// X86: call void @"??3@YAXPAXI@Z"(ptr noundef %this1, i32 noundef {{.*}})
diff --git a/clang/test/CodeGenCXX/msvc-vector-deleting-dtors-sized-delete.cpp b/clang/test/CodeGenCXX/msvc-vector-deleting-dtors-sized-delete.cpp
index 6c9faa88e08e9..db8c429956b6f 100644
--- a/clang/test/CodeGenCXX/msvc-vector-deleting-dtors-sized-delete.cpp
+++ b/clang/test/CodeGenCXX/msvc-vector-deleting-dtors-sized-delete.cpp
@@ -50,5 +50,5 @@ void test() {
// X86-NEXT: %[[ARRSZ1:.*]] = mul i32 12, %[[HOWMANY]]
// X64-NEXT: %[[TOTALSZ1:.*]] = add i64 %[[ARRSZ1]], 8
// X86-NEXT: %[[TOTALSZ1:.*]] = add i32 %[[ARRSZ1]], 4
-// X64-NEXT: call void @"??_V@YAXPEAX_K@Z"(ptr noundef %2, i64 noundef %[[TOTALSZ1]])
-// X86-NEXT: call void @"??_V@YAXPAXI@Z"(ptr noundef %2, i32 noundef %[[TOTALSZ1]])
+// X64-NEXT: call void @"?__global_delete@@YAXPEAX_K@Z"(ptr noundef %2, i64 noundef %[[TOTALSZ1]])
+// X86-NEXT: call void @"?__global_delete@@YAXPAXI@Z"(ptr noundef %2, i32 noundef %[[TOTALSZ1]])
diff --git a/clang/test/Modules/glob-delete-with-virtual-dtor.cpp b/clang/test/Modules/glob-delete-with-virtual-dtor.cpp
index fb2e2a4decf60..18e90aaca78f0 100644
--- a/clang/test/Modules/glob-delete-with-virtual-dtor.cpp
+++ b/clang/test/Modules/glob-delete-with-virtual-dtor.cpp
@@ -30,8 +30,8 @@ void out_of_module_tests() {
// CHECK-NEXT: br i1 %[[CONDITION1]], label %[[CALL_CLASS_DELETE:[0-9a-z._]+]], label %[[CALL_GLOB_DELETE:[0-9a-z._]+]]
//
// CHECK: [[CALL_GLOB_DELETE]]
-// CHECK32-NEXT: call void @"??3@YAXPAXI@Z"
-// CHECK64-NEXT: call void @"??3@YAXPEAX_K@Z"
+// CHECK32-NEXT: call void @"?__global_delete@@YAXPAXI@Z"
+// CHECK64-NEXT: call void @"?__global_delete@@YAXPEAX_K@Z"
// CHECK-NEXT: br label %[[CONTINUE_LABEL]]
//
// CHECK: [[CALL_CLASS_DELETE]]
diff --git a/clang/test/Modules/msvc-vector-deleting-destructors.cpp b/clang/test/Modules/msvc-vector-deleting-destructors.cpp
index 68faa687251d7..9e99ae1e191b7 100644
--- a/clang/test/Modules/msvc-vector-deleting-destructors.cpp
+++ b/clang/test/Modules/msvc-vector-deleting-destructors.cpp
@@ -24,11 +24,11 @@ void out_of_module_tests(Derived *p, Derived *p1) {
// CHECK32-NEXT: %[[ARRSZ:.*]] = mul i32 8, %[[COOKIE:.*]]
// CHECK64-NEXT: %[[TOTALSZ:.*]] = add i64 %[[ARRSZ]], 8
// CHECK32-NEXT: %[[TOTALSZ:.*]] = add i32 %[[ARRSZ]], 4
-// CHECK32-NEXT: call void @"??_V@YAXPAXI@Z"(ptr noundef %2, i32 noundef %[[TOTALSZ]])
-// CHECK64-NEXT: call void @"??_V@YAXPEAX_K@Z"(ptr noundef %2, i64 noundef %[[TOTALSZ]])
+// CHECK32-NEXT: call void @"?__global_delete@@YAXPAXI@Z"(ptr noundef %2, i32 noundef %[[TOTALSZ]])
+// CHECK64-NEXT: call void @"?__global_delete@@YAXPEAX_K@Z"(ptr noundef %2, i64 noundef %[[TOTALSZ]])
// CHECK: dtor.call_glob_delete:
-// CHECK32-NEXT: call void @"??3@YAXPAXI@Z"(ptr noundef %this1, i32 noundef 8)
-// CHECK64-NEXT: call void @"??3@YAXPEAX_K@Z"(ptr noundef %this1, i64 noundef 16)
+// CHECK32-NEXT: call void @"?__global_delete@@YAXPAXI@Z"(ptr noundef %this1, i3...
[truncated]
|
|
✅ With the latest revision this PR passed the C/C++ code formatter. |
Fznamznon
left a comment
There was a problem hiding this comment.
Thank you for working on this! This makes sense to me.
However given my inexperience in MSVC ABI and clang CodeGen, it would be great to get an additional approval, perhaps from @efriedma-quic
| @@ -452,6 +452,11 @@ Windows Support | |||
| - Clang now defines the ``_MSVC_TRADITIONAL`` macro as ``1`` when emulating MSVC | |||
| 19.15 (Visual Studio 2017 version 15.8) and later. (#GH47114) | |||
|
|
|||
| - In MSVC compatibility mode, scalar and vector deleting destructors now call | |||
There was a problem hiding this comment.
I wonder if we need to backport this fix to clang 22?
I'm not sure if it is ABI breaking though. If it is - we can't backport it and need to put it under -fclang-abi-compat=23
There was a problem hiding this comment.
It would make sense to me to backport it, and it shouldn't be breaking. But I'll leave that up to the Clang maintainers to decide.
There was a problem hiding this comment.
Mmm, if we end up backporting it, I'm not sure we need a release note. cc @AaronBallman on release note matters
There was a problem hiding this comment.
Backports are usually just noted in the patch release announcement, I think.
This seems a little large to backport... if we expect people who care about this use-case to be using the 22 release, I guess we could.
There was a problem hiding this comment.
I don't think there will be much use of this feature: you need to have a custom operator delete (i.e., using C++) but be in an environment without the CRT (kernel-mode, drivers, etc.). I'm fine with not backporting.
There was a problem hiding this comment.
But this does break ABI, doesn't it? From a comment elsewhere:
So it would be:
Code Provides Old New ::operator delete+__global_delete(common case)Calls __global_delete->::operator newCalls ::operator new__global_deleteonly (kernel-mode)Calls __global_deleteCalls __global_delete::operator newonly (non-MSVC CRT)Calls __empty_global_deleteand leaks> Calls ::operator newNothing Calls __empty_global_deleteand leaksLinker error
| llvm::Function *EmptyFn = llvm::Function::Create( | ||
| FnTy, llvm::GlobalValue::LinkOnceODRLinkage, EmptyGlobalDeleteName, &M); | ||
| EmptyFn->setComdat(M.getOrInsertComdat(EmptyGlobalDeleteName)); | ||
| EmptyFn->setUnnamedAddr(llvm::GlobalValue::UnnamedAddr::Global); |
There was a problem hiding this comment.
I wonder if we just need to call CodeGenModule::SetFunctionAttributes instead of manually adding the attributes?
There was a problem hiding this comment.
SetFunctionAttributes requires a GlobalDecl which we don't have for this function, but I've added SetLLVMFunctionAttributesForDefinition to copy the attributes from the global delete function.
| /// environments without a global ::operator delete (e.g., kernel mode) to | ||
| /// gracefully fall back to a no-op __empty_global_delete. | ||
| static llvm::Constant * | ||
| getOrCreateMSVCGlobalDeleteWrapper(CodeGenModule &CGM, |
There was a problem hiding this comment.
Does it make sense to assert that the target is MSVC ABI at the beginning of the function?
There was a problem hiding this comment.
Sure, makes sense.
| llvm::CallBase *CallOrInvoke; | ||
| llvm::Constant *CalleePtr = CGF.CGM.GetAddrOfFunction(CalleeDecl); | ||
| llvm::Constant *CalleePtr = | ||
| CalleeOverride ? CalleeOverride : CGF.CGM.GetAddrOfFunction(CalleeDecl); |
There was a problem hiding this comment.
Can we somehow end up failing to emit global operator delete if it is never used in a TU from any other place except some deleting destructor?
I thought about type-aware deallocators, although these seem to be force-emitted https://godbolt.org/z/6o6vMc9G5 but maybe there is something else
There was a problem hiding this comment.
I'm a little confused about what you mean here - what function specifically are you worried about missing?
There was a problem hiding this comment.
CodeGen usually tries to be smart about functions that are emitted in the resulting LLVM IR module. If something is never used and there is no way to use it outside of the current translation unit - it won't be emitted, the simplest example of that case is unused static function.
I was thinking since we always replace the calls to the operator delete with a wrapper call, It may happen that we never see the actual (user defined) operator delete (in my compiler explorer link it is function with std::type_identity argument, its mangling name is ??3@YAXU?$type_identity@UKernelBase@@@std@@PEAX_KW4align_val_t@1@@Z in the LLVM IR) in the codegen as something referenced within the TU and therefore it might be missing due to CodeGen being too smart about it.
However in my compiler explorer link I see it emitted even though I never create an object of the class this function intended for and therefore that example is not good.
There was a problem hiding this comment.
Yes, it's possible that there may end up being no calls to the global operator delete, however it's linkage type should prevent it from being optimized out. Once you get to the final binary, if there still are no calls (which there should be, since the CRT provides a __global_delete that forwards to it) then the linker may drop it.
| @@ -452,6 +452,11 @@ Windows Support | |||
| - Clang now defines the ``_MSVC_TRADITIONAL`` macro as ``1`` when emulating MSVC | |||
| 19.15 (Visual Studio 2017 version 15.8) and later. (#GH47114) | |||
|
|
|||
| - In MSVC compatibility mode, scalar and vector deleting destructors now call | |||
There was a problem hiding this comment.
Backports are usually just noted in the patch release announcement, I think.
This seems a little large to backport... if we expect people who care about this use-case to be using the 22 release, I guess we could.
| EmptyFn->setUnnamedAddr(llvm::GlobalValue::UnnamedAddr::Global); | ||
| CGM.SetLLVMFunctionAttributesForDefinition(GlobOD, EmptyFn); | ||
| auto *BB = llvm::BasicBlock::Create(LLVMCtx, "", EmptyFn); | ||
| llvm::ReturnInst::Create(LLVMCtx, BB); |
There was a problem hiding this comment.
The idea behind __empty_global_delete is that it's never called, right? So if execution does reach this implementation, something is wrong.
Should we trap instead of silently leaking/corrupting memory?
There was a problem hiding this comment.
MSVC explicitly leaves this as a no-op, so making it trap instead would be a change in behavior when using a different compiler.
Leaking memory is not a UB: it's well defined (albeit bad) behavior.
Personally, if we don't want to silently leak, then I would rather not emit this symbol at all and have a link-time error instead of runtime crash.
There was a problem hiding this comment.
It should really be impossible to end up calling __empty_global_delete: the only way we should try to call global delete on an object is if it was allocated with global new, and if global new exists, __global_delete should also exist. So it probably indicates memory corruption.
But if you think it's better to just match MSVC, I guess I'm okay with that.
There was a problem hiding this comment.
Your intuition here is correct: MSVC emits the definition for __global_delete if there are any calls to global ::operator delete, therefore __empty_global_delete should never be called. I've switched it to trap instead of being a no-op.
| // Global ::operator delete[] mangling: ??_V@<signature> | ||
| // We construct: | ||
| // ?__global_delete@@<signature> | ||
| // ?__empty_global_delete@@<signature> |
There was a problem hiding this comment.
What happens if someone is using libc++ instead of MSVC stl?
There was a problem hiding this comment.
The difference is the CRTs, not the STLs.
But yes, your point still stands: Microsoft CRT provides the definition of __global_delete that forwards to the global operator delete, but I don't think other CRTs do.
Maybe we should use the "weak" linking mechanism here instead: still call the normal ::operator delete then provide an /ALTERNATENAME from ::operator delete to __global_delete and not do any of the __empty_global_delete stuff at all. This would mean that anyone providing an operator delete gets the normal behavior of calling that directly, but anyone without that would be required to provide __global_delete, which would make it compatible with MSVC for that scenario.
So it would be:
| Code Provides | Old | New |
|---|---|---|
::operator delete + __global_delete (common case) |
Calls __global_delete -> ::operator new |
Calls ::operator new |
__global_delete only (kernel-mode) |
Calls __global_delete |
Calls __global_delete |
::operator new only (non-MSVC CRT) |
Calls __empty_global_delete and leaks |
Calls ::operator new |
| Nothing | Calls __empty_global_delete and leaks |
Linker error |
There was a problem hiding this comment.
Update: the CRT doesn't actually provide the definition of __global_delete, it is compiler generated, therefore this is safe to use with non-MSVC CRTs.
| EmptyFn->setUnnamedAddr(llvm::GlobalValue::UnnamedAddr::Global); | ||
| CGM.SetLLVMFunctionAttributesForDefinition(GlobOD, EmptyFn); | ||
| auto *BB = llvm::BasicBlock::Create(LLVMCtx, "", EmptyFn); | ||
| llvm::ReturnInst::Create(LLVMCtx, BB); |
There was a problem hiding this comment.
It should really be impossible to end up calling __empty_global_delete: the only way we should try to call global delete on an object is if it was allocated with global new, and if global new exists, __global_delete should also exist. So it probably indicates memory corruption.
But if you think it's better to just match MSVC, I guess I'm okay with that.
|
Update: I did some more research into this, and it turns out that |
| // class-specific one), note it so we emit __global_delete forwarding bodies. | ||
| if (!isa<CXXMethodDecl>(E->getOperatorDelete()) && | ||
| CGM.getTarget().getCXXABI().isMicrosoft()) | ||
| CGM.noteDirectGlobalDelete(); |
There was a problem hiding this comment.
I think this is supposed to be specifically looking for a ::delete expression involving a class with a member delete, not just any delete which calls a global operator delete.
We also need to handle the case where the class is dllexport.
There was a problem hiding this comment.
Good call - fixed these and checked MSVC's behavior for other corner cases as well.
There was a problem hiding this comment.
involving a class with a member delete, not just any delete which calls a global operator delete.
Sorry, has been it actually applied/responded to?
There was a problem hiding this comment.
Ah, yep, there's a gap here. Will have an update shortly.
There was a problem hiding this comment.
Fixed: lined-up with MSVC's behavior where it requires an explicit global operator delete (with the leading :: qualification) for a type with non-trivial destructor.
🐧 Linux x64 Test Results
✅ The build succeeded and all tests passed. |
|
@efriedma-quic @Fznamznon Any additional concerns? |
Fznamznon
left a comment
There was a problem hiding this comment.
Sorry for the delay, this is mostly lgtm though I do have a couple questions
|
|
||
| void CodeGenModule::addPendingGlobalDelete( | ||
| StringRef GlobalDeleteName, const FunctionDecl *OperatorDeleteFD) { | ||
| // Only add if we haven't seen this name before. |
There was a problem hiding this comment.
Does it make sense to use a StringMap for PendingMSVCGlobalDeletes collection instead?
There was a problem hiding this comment.
For deterministic builds, we need to keep the order stable. I did switch to a MapVector to simplify this.
| // class-specific one), note it so we emit __global_delete forwarding bodies. | ||
| if (!isa<CXXMethodDecl>(E->getOperatorDelete()) && | ||
| CGM.getTarget().getCXXABI().isMicrosoft()) | ||
| CGM.noteDirectGlobalDelete(); |
There was a problem hiding this comment.
involving a class with a member delete, not just any delete which calls a global operator delete.
Sorry, has been it actually applied/responded to?
|
|
||
| // __global_delete should NOT have a forwarding body (no ::delete in this TU, | ||
| // no dllexport class). | ||
| // CHECK-NOT: define {{.*}}void @"?__global_delete@@YAXPEAX_K@Z" |
There was a problem hiding this comment.
So, the intention to emit __global_delete body on any ::delete call not just involving a class with defined operator delete?
There was a problem hiding this comment.
Correct. The presence of any ::delete in the TU proves ::operator delete exists in the program, which is exactly the condition under which it's safe to emit a real __global_delete forwarding body.
| llvm::ReturnInst::Create(getModule().getContext(), BB); | ||
|
|
||
| // Use LinkOnceODR so multiple TUs can emit this without conflicts. | ||
| GlobDelFn->setLinkage(llvm::GlobalValue::LinkOnceODRLinkage); |
There was a problem hiding this comment.
I'm slightly confused by the fact that multiple comments says that __global_delete is a weak symbol but is emitted with linkonce_odr linkage.
There was a problem hiding this comment.
Yes, sorry, it was weak in an earlier version. Cleaned up the comments to explain the current delayed-emission-or-default mechanism.
482ae8c to
b2575be
Compare
Fznamznon
left a comment
There was a problem hiding this comment.
Thanks! LGTM
@efriedma-quic given that it slightly changed after your approval, are you still ok with it?
| // CHECK-NEXT: br label %dtor.continue | ||
|
|
||
| // Test that when a class provides its own operator delete, the deleting | ||
| // destructor calls __global_delete (a weak external) instead of directly |
There was a problem hiding this comment.
Not weak, right?
| // destructor calls __global_delete (a weak external) instead of directly | |
| // destructor calls __global_delete instead of directly |
efriedma-quic
left a comment
There was a problem hiding this comment.
LGTM with a couple minor comments.
| FnTy, llvm::GlobalValue::LinkOnceODRLinkage, EmptyGlobalDeleteName, &M); | ||
| EmptyFn->setComdat(M.getOrInsertComdat(EmptyGlobalDeleteName)); | ||
| EmptyFn->setUnnamedAddr(llvm::GlobalValue::UnnamedAddr::Global); | ||
| CGM.SetLLVMFunctionAttributesForDefinition(GlobOD, EmptyFn); |
There was a problem hiding this comment.
I'm not sure how much practical effect it has here, but the general pattern is to call CGM.SetLLVMFunctionAttributes(), then CGM.SetLLVMFunctionAttributesForDefinition(), then getTargetCodeGenInfo().setTargetAttributes(). (We should really clean this up at some point...)
| // Use LinkOnceODR so multiple TUs can emit this without conflicts. | ||
| GlobDelFn->setLinkage(llvm::GlobalValue::LinkOnceODRLinkage); | ||
| GlobDelFn->setComdat(getModule().getOrInsertComdat(GlobDelFn->getName())); | ||
| SetLLVMFunctionAttributesForDefinition(OperatorDeleteFD, GlobDelFn); |
There was a problem hiding this comment.
(Same issue with attributes here.)
…tructors instead of directly referencing ::operator delete When Clang emits scalar/vector deleting destructors for classes with a class-level `operator delete`, it generates a conditional dispatch that can call either the class-level or global `::operator delete`. The global path directly referenced `::operator delete`, causing `LNK2001` linker errors in environments where no global `::operator delete` exists. MSVC handles this by calling `__global_delete` - this is a compiler generated function that is ONLY created if there is a direct call to global `::operator delete`. Additionally, it always emits an empty `__empty_global_delete` and uses `/ALTERNATIVENAME` linker arg to default `__global_delete` to `__empty_global_delete` if there is NEVER an actual all to `::operator delete` (thus the empty function should never be called). This change aligns Clang's behavior with MSVC when MSVC compatibility mode and non-LLVM 21 ABI is used, with one difference: the LLVM generated `__empty_global_delete` traps since it should never be called.
…mes, only emit __global_delete body for calls to global op with a class type with a non-trivial dtor
…estructors instead of directly referencing `::operator delete` (llvm#188372) When Clang emits scalar/vector deleting destructors for classes with a class-level `operator delete`, it generates a conditional dispatch that can call either the class-level or global `::operator delete`. The global path directly referenced `::operator delete`, causing `LNK2001` linker errors in environments where no global `::operator delete` exists. MSVC handles this by calling `__global_delete` (and `__global_array_delete` for vector deletes) - this is a compiler generated function that is ONLY defined if there is a direct call to global `::operator delete` for type with non-trivial destructors. Additionally, it always emits an empty `__empty_global_delete` and uses `/ALTERNATIVENAME` linker arg to default `__global_delete` (and `__global_array_delete`) to `__empty_global_delete` if there is NEVER an delete operator call that would triffer the body to be emitted (thus the empty function should never be called). This change aligns Clang's behavior with MSVC when MSVC compatibility mode and non-LLVM 21 ABI is used, with one difference: the LLVM generated `__empty_global_delete` traps since it should never be called.
…delete (#209585) PR #188372 made Clang's MSVC-ABI deleting-destructor path route global deletes through compiler-generated __global_delete / __global_array_delete wrappers instead of referencing ::operator delete directly. This lets a deleting destructor be emitted in environments (e.g. kernel mode) where no global ::operator delete exists: each wrapper defaults to a trapping __empty_global_delete fallback, and a real forwarding body that calls ::operator delete is materialized only when the program actually uses ::delete. This change refines how those wrappers and their fallbacks are emitted, fixing two problems that made the #188372 mechanism fail in practice. New behavior: - The trapping fallback is now emitted as a weak GlobalAlias to __empty_global_delete rather than via an /alternatename directive. This lowers to a COFF weak-external-with-default -- exactly what MSVC emits -- so a real __global_delete defined in any TU overrides the fallback in every other TU at link time. - A ::delete expression now registers the wrapper for its resolved global operator delete directly (in EmitCXXDeleteExpr), so the strong forwarding body is emitted in that TU regardless of where the class's deleting destructor is defined. This matches MSVC, which emits the forwarder at every ::delete site. Bugs fixed: 1. Arm64EC (miscompile -> LNK2019). /alternatename only names the plain symbol, not the backend-generated "$$h" hybrid EC symbol referenced by the exit thunk, so Arm64EC images left __global_delete$exit_thunk unresolved. The weak GlobalAlias correctly produces the "$$h" symbol, exit thunk, and plain alias (verified on both x64 and Arm64EC). 2. Cross-TU ::delete (runtime crash). Previously the forwarding body was only emitted in a TU that *both* emitted a matching vector deleting destructor and contained a ::delete. A TU that only performed `::delete p` -- with the class defined elsewhere -- emitted no forwarder, so nothing overrode the weak-alias trap and the program executed a trapping __empty_global_delete at runtime (STATUS_ILLEGAL_INSTRUCTION). Registering the wrapper at the ::delete site fixes this. To share wrapper-creation logic between the deleting-destructor path and the delete-expression path, getOrCreateMSVCGlobalDeleteWrapper is promoted from a static helper in CGClass.cpp to a CodeGenModule method. Verified end-to-end: a weak alias in one TU and a strong forwarder in another resolve to the forwarder under both lld-link and MSVC link.exe, and the previously-crashing delete-only TU now runs cleanly.
…delete (llvm#209585) PR llvm#188372 made Clang's MSVC-ABI deleting-destructor path route global deletes through compiler-generated __global_delete / __global_array_delete wrappers instead of referencing ::operator delete directly. This lets a deleting destructor be emitted in environments (e.g. kernel mode) where no global ::operator delete exists: each wrapper defaults to a trapping __empty_global_delete fallback, and a real forwarding body that calls ::operator delete is materialized only when the program actually uses ::delete. This change refines how those wrappers and their fallbacks are emitted, fixing two problems that made the llvm#188372 mechanism fail in practice. New behavior: - The trapping fallback is now emitted as a weak GlobalAlias to __empty_global_delete rather than via an /alternatename directive. This lowers to a COFF weak-external-with-default -- exactly what MSVC emits -- so a real __global_delete defined in any TU overrides the fallback in every other TU at link time. - A ::delete expression now registers the wrapper for its resolved global operator delete directly (in EmitCXXDeleteExpr), so the strong forwarding body is emitted in that TU regardless of where the class's deleting destructor is defined. This matches MSVC, which emits the forwarder at every ::delete site. Bugs fixed: 1. Arm64EC (miscompile -> LNK2019). /alternatename only names the plain symbol, not the backend-generated "$$h" hybrid EC symbol referenced by the exit thunk, so Arm64EC images left __global_delete$exit_thunk unresolved. The weak GlobalAlias correctly produces the "$$h" symbol, exit thunk, and plain alias (verified on both x64 and Arm64EC). 2. Cross-TU ::delete (runtime crash). Previously the forwarding body was only emitted in a TU that *both* emitted a matching vector deleting destructor and contained a ::delete. A TU that only performed `::delete p` -- with the class defined elsewhere -- emitted no forwarder, so nothing overrode the weak-alias trap and the program executed a trapping __empty_global_delete at runtime (STATUS_ILLEGAL_INSTRUCTION). Registering the wrapper at the ::delete site fixes this. To share wrapper-creation logic between the deleting-destructor path and the delete-expression path, getOrCreateMSVCGlobalDeleteWrapper is promoted from a static helper in CGClass.cpp to a CodeGenModule method. Verified end-to-end: a weak alias in one TU and a strong forwarder in another resolve to the forwarder under both lld-link and MSVC link.exe, and the previously-crashing delete-only TU now runs cleanly.
When Clang emits scalar/vector deleting destructors for classes with a class-level
operator delete, it generates a conditional dispatch that can call either the class-level or global::operator delete. The global path directly referenced::operator delete, causingLNK2001linker errors in environments where no global::operator deleteexists.MSVC handles this by calling
__global_delete(and__global_array_deletefor vector deletes) - this is a compiler generated function that is ONLY defined if there is a direct call to global::operator deletefor type with non-trivial destructors. Additionally, it always emits an empty__empty_global_deleteand uses/ALTERNATIVENAMElinker arg to default__global_delete(and__global_array_delete) to__empty_global_deleteif there is NEVER an delete operator call that would triffer the body to be emitted (thus the empty function should never be called).This change aligns Clang's behavior with MSVC when MSVC compatibility mode and non-LLVM 21 ABI is used, with one difference: the LLVM generated
__empty_global_deletetraps since it should never be called.