[clang] [clang][win] MSVC-compat: Use `__global_delete` wrapper in deleting destructors instead of directly referencing `::operator delete` (PR #188372)
Daniel Paoliello via cfe-commits
cfe-commits at lists.llvm.org
Mon Apr 13 14:47:14 PDT 2026
https://github.com/dpaoliello updated https://github.com/llvm/llvm-project/pull/188372
>From 2da5b7db358c942305dae9a5daeaaa6f67fb1396 Mon Sep 17 00:00:00 2001
From: "Daniel Paoliello (HE/HIM)" <danpao at microsoft.com>
Date: Mon, 23 Mar 2026 12:26:34 -0700
Subject: [PATCH 1/5] [win] MSVC-compat: Use `__global_delete` wrapper in
deleting destructors instead of directly referencing `::operator delete`
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
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 (e.g., Windows kernel mode).
MSVC handles this by calling `__global_delete` — a weak external that falls back to a no-op `__empty_global_delete` via `/ALTERNATENAME`.
This change aligns Clang's behavior with MSVC when MSVC compatibility mode and non-LLVM 21 ABI is used.
---
clang/docs/ReleaseNotes.rst | 5 ++
clang/lib/CodeGen/CGClass.cpp | 86 ++++++++++++++++++-
clang/lib/CodeGen/CGExprCXX.cpp | 11 ++-
clang/lib/CodeGen/CodeGenFunction.h | 3 +-
.../CodeGenCXX/cxx2a-destroying-delete.cpp | 8 +-
.../CodeGenCXX/microsoft-abi-structors.cpp | 2 +-
.../microsoft-vector-deleting-dtors.cpp | 48 ++++++++++-
.../microsoft-vector-deleting-dtors2.cpp | 4 +-
...svc-vector-deleting-dtors-sized-delete.cpp | 4 +-
.../Modules/glob-delete-with-virtual-dtor.cpp | 4 +-
.../msvc-vector-deleting-destructors.cpp | 8 +-
.../PCH/glob-delete-with-virtual-dtor.cpp | 4 +-
.../PCH/msvc-vector-deleting-destructors.cpp | 8 +-
13 files changed, 163 insertions(+), 32 deletions(-)
diff --git a/clang/docs/ReleaseNotes.rst b/clang/docs/ReleaseNotes.rst
index 51f2c8bf70194..544fd9b2a0505 100644
--- a/clang/docs/ReleaseNotes.rst
+++ b/clang/docs/ReleaseNotes.rst
@@ -516,6 +516,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 c0482fb13ec79..09e7d4a726577 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>
@@ -1409,6 +1410,70 @@ 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) {
@@ -1492,9 +1557,13 @@ 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.
@@ -1721,9 +1790,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
@@ -1747,7 +1819,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..630a0159ff907 100644
--- a/clang/lib/CodeGen/CGExprCXX.cpp
+++ b/clang/lib/CodeGen/CGExprCXX.cpp
@@ -1337,9 +1337,11 @@ 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 +1790,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 +1859,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 f06c216e0c746..ebb2164b84667 100644
--- a/clang/lib/CodeGen/CodeGenFunction.h
+++ b/clang/lib/CodeGen/CodeGenFunction.h
@@ -3299,7 +3299,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 at YAXPEAX_K@Z"(ptr noundef %{{.*}}, i64 noundef 48)
-// CLANG22-MSABI32: call void @"??3 at YAXPAXIW4align_val_t@std@@@Z"(ptr noundef %{{.*}}, i32 noundef 32, i32 noundef 16)
+// CLANG22-MSABI64: call void @"?__global_delete@@YAXPEAX_K at Z"(ptr noundef %{{.*}}, i64 noundef 48)
+// CLANG22-MSABI32: call void @"?__global_delete@@YAXPAXIW4align_val_t at 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 at YAXPEAX_KW4align_val_t@std@@@Z"(ptr noundef %{{.*}}, i64 noundef 96, i64 noundef 32)
-// CLANG22-MSABI32: call void @"??3 at YAXPAXIW4align_val_t@std@@@Z"(ptr noundef %{{.*}}, i32 noundef 64, i32 noundef 32)
+// CLANG22-MSABI64: call void @"?__global_delete@@YAXPEAX_KW4align_val_t at std@@@Z"(ptr noundef %{{.*}}, i64 noundef 96, i64 noundef 32)
+// CLANG22-MSABI32: call void @"?__global_delete@@YAXPAXIW4align_val_t at 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 at YAXPAX@Z"(ptr %[[THIS]])
+// DTORS-NEXT: call void @"?__global_delete@@YAXPAX at 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 cca1c66806af6..4891ce12f8fed 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 at Z" = weak dso_local unnamed_addr alias ptr (ptr, i32), ptr @"??_GBird@@UEAAPEAXI at 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 at YAXPEAX_K@Z"(ptr noundef %2, i64 noundef %[[TOTALSZ]])
-// X86-NEXT: call void @"??_V at YAXPAXI@Z"(ptr noundef %2, i32 noundef %[[TOTALSZ]])
+// X64-NEXT: call void @"?__global_delete@@YAXPEAX_K at Z"(ptr noundef %2, i64 noundef %[[TOTALSZ]])
+// X86-NEXT: call void @"?__global_delete@@YAXPAXI at 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 at Z"(ptr %0, i64 %1)
+// X64-NEXT: ret void
+// X64-LABEL: define weak dso_local noundef ptr @"??_EKernelDerived@@UEAAPEAXI at 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 at 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 at Z"(ptr noundef %{{.*}}, i64 noundef 8)
+// X64: dtor.call_class_delete:
+// X64-NEXT: call void @"??3KernelBase@@SAXPEAX at Z"(ptr noundef %{{.*}})
struct BaseDelete1 {
@@ -346,3 +385,8 @@ void foobartest() {
// X64: define weak dso_local noundef ptr @"??_EAllocatedAsArray@@UEAAPEAXI at Z"
// X86: define weak dso_local x86_thiscallcc noundef ptr @"??_EAllocatedAsArray@@UAEPAXI at Z"
// CLANG21: define linkonce_odr dso_local noundef ptr @"??_GAllocatedAsArray@@UEAAPEAXI at Z"
+
+// Verify the /ALTERNATENAME linker directive.
+// X64: !{!"/alternatename:?__global_delete@@YAXPEAX_K at Z=?__empty_global_delete@@YAXPEAX_K at 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 c6089bb5ecbba..904df6b528bfe 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 at Z"
// X64: call void @"??1DrawingBuffer@@UEAA at XZ"(ptr noundef nonnull align 8 dead_on_return(8) dereferenceable(8) %arraydestroy.element)
// X64: call void @"??_V?$RefCounted at UDrawingBuffer@@@@SAXPEAX at Z"(ptr noundef %2)
-// X64: call void @"??_V at YAXPEAX_K@Z"(ptr noundef %2, i64 noundef %{{.*}})
+// X64: call void @"?__global_delete@@YAXPEAX_K at Z"(ptr noundef %2, i64 noundef %{{.*}})
// X64: call void @"??1DrawingBuffer@@UEAA at XZ"(ptr noundef nonnull align 8 dead_on_return(8) dereferenceable(8) %this1)
// X64: call void @"??3 at 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 at Z"
// X86: call x86_thiscallcc void @"??1DrawingBuffer@@UAE at XZ"(ptr noundef nonnull align 4 dead_on_return(4) dereferenceable(4) %arraydestroy.element)
// X86: call void @"??_V?$RefCounted at UDrawingBuffer@@@@SAXPAX at Z"(ptr noundef %2)
-// X86: call void @"??_V at YAXPAXI@Z"(ptr noundef %2, i32 noundef {{.*}})
+// X86: call void @"?__global_delete@@YAXPAXI at Z"(ptr noundef %2, i32 noundef {{.*}})
// X86 call x86_thiscallcc void @"??1DrawingBuffer@@UAE at XZ"(ptr noundef nonnull align 4 dereferenceable(4) %this1)
// X86: call void @"??3 at 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 at YAXPEAX_K@Z"(ptr noundef %2, i64 noundef %[[TOTALSZ1]])
-// X86-NEXT: call void @"??_V at YAXPAXI@Z"(ptr noundef %2, i32 noundef %[[TOTALSZ1]])
+// X64-NEXT: call void @"?__global_delete@@YAXPEAX_K at Z"(ptr noundef %2, i64 noundef %[[TOTALSZ1]])
+// X86-NEXT: call void @"?__global_delete@@YAXPAXI at 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 at YAXPAXI@Z"
-// CHECK64-NEXT: call void @"??3 at YAXPEAX_K@Z"
+// CHECK32-NEXT: call void @"?__global_delete@@YAXPAXI at Z"
+// CHECK64-NEXT: call void @"?__global_delete@@YAXPEAX_K at 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 at YAXPAXI@Z"(ptr noundef %2, i32 noundef %[[TOTALSZ]])
-// CHECK64-NEXT: call void @"??_V at YAXPEAX_K@Z"(ptr noundef %2, i64 noundef %[[TOTALSZ]])
+// CHECK32-NEXT: call void @"?__global_delete@@YAXPAXI at Z"(ptr noundef %2, i32 noundef %[[TOTALSZ]])
+// CHECK64-NEXT: call void @"?__global_delete@@YAXPEAX_K at Z"(ptr noundef %2, i64 noundef %[[TOTALSZ]])
// CHECK: dtor.call_glob_delete:
-// CHECK32-NEXT: call void @"??3 at YAXPAXI@Z"(ptr noundef %this1, i32 noundef 8)
-// CHECK64-NEXT: call void @"??3 at YAXPEAX_K@Z"(ptr noundef %this1, i64 noundef 16)
+// CHECK32-NEXT: call void @"?__global_delete@@YAXPAXI at Z"(ptr noundef %this1, i32 noundef 8)
+// CHECK64-NEXT: call void @"?__global_delete@@YAXPEAX_K at Z"(ptr noundef %this1, i64 noundef 16)
// CHECK: dtor.call_class_delete:
// CHECK32-NEXT: call void @"??3Base2@@SAXPAX at Z"(ptr noundef %this1)
// CHECK64-NEXT: call void @"??3Base2@@SAXPEAX at Z"(ptr noundef %this1)
diff --git a/clang/test/PCH/glob-delete-with-virtual-dtor.cpp b/clang/test/PCH/glob-delete-with-virtual-dtor.cpp
index 29242b04c4a7f..a17b7570bbd65 100644
--- a/clang/test/PCH/glob-delete-with-virtual-dtor.cpp
+++ b/clang/test/PCH/glob-delete-with-virtual-dtor.cpp
@@ -33,8 +33,8 @@ void out_of_pch_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 at YAXPAXI@Z"
-// CHECK64-NEXT: call void @"??3 at YAXPEAX_K@Z"
+// CHECK32-NEXT: call void @"?__global_delete@@YAXPAXI at Z"
+// CHECK64-NEXT: call void @"?__global_delete@@YAXPEAX_K at Z"
// CHECK-NEXT: br label %[[CONTINUE_LABEL]]
//
// CHECK: [[CALL_CLASS_DELETE]]
diff --git a/clang/test/PCH/msvc-vector-deleting-destructors.cpp b/clang/test/PCH/msvc-vector-deleting-destructors.cpp
index 1409b41d2df82..b17fe35240c89 100644
--- a/clang/test/PCH/msvc-vector-deleting-destructors.cpp
+++ b/clang/test/PCH/msvc-vector-deleting-destructors.cpp
@@ -28,11 +28,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 at YAXPAXI@Z"(ptr noundef %2, i32 noundef %[[TOTALSZ]])
-// CHECK64-NEXT: call void @"??_V at YAXPEAX_K@Z"(ptr noundef %2, i64 noundef %[[TOTALSZ]])
+// CHECK32-NEXT: call void @"?__global_delete@@YAXPAXI at Z"(ptr noundef %2, i32 noundef %[[TOTALSZ]])
+// CHECK64-NEXT: call void @"?__global_delete@@YAXPEAX_K at Z"(ptr noundef %2, i64 noundef %[[TOTALSZ]])
// CHECK: dtor.call_glob_delete:
-// CHECK32-NEXT: call void @"??3 at YAXPAXI@Z"(ptr noundef %this1, i32 noundef 8)
-// CHECK64-NEXT: call void @"??3 at YAXPEAX_K@Z"(ptr noundef %this1, i64 noundef 16)
+// CHECK32-NEXT: call void @"?__global_delete@@YAXPAXI at Z"(ptr noundef %this1, i32 noundef 8)
+// CHECK64-NEXT: call void @"?__global_delete@@YAXPEAX_K at Z"(ptr noundef %this1, i64 noundef 16)
// CHECK: dtor.call_class_delete:
// CHECK32-NEXT: call void @"??3Base2@@SAXPAX at Z"(ptr noundef %this1)
// CHECK64-NEXT: call void @"??3Base2@@SAXPEAX at Z"(ptr noundef %this1)
>From 97e121f6178e876345515eeaf0e9d96db9ddff3d Mon Sep 17 00:00:00 2001
From: "Daniel Paoliello (HE/HIM)" <danpao at microsoft.com>
Date: Wed, 25 Mar 2026 10:24:07 -0700
Subject: [PATCH 2/5] Add assert for MSVC ABI, set attr on
__empty_global_delete
---
clang/lib/CodeGen/CGClass.cpp | 3 +++
1 file changed, 3 insertions(+)
diff --git a/clang/lib/CodeGen/CGClass.cpp b/clang/lib/CodeGen/CGClass.cpp
index 09e7d4a726577..107821116b3f1 100644
--- a/clang/lib/CodeGen/CGClass.cpp
+++ b/clang/lib/CodeGen/CGClass.cpp
@@ -1454,6 +1454,7 @@ getOrCreateMSVCGlobalDeleteWrapper(CodeGenModule &CGM,
FnTy, llvm::GlobalValue::LinkOnceODRLinkage, EmptyGlobalDeleteName, &M);
EmptyFn->setComdat(M.getOrInsertComdat(EmptyGlobalDeleteName));
EmptyFn->setUnnamedAddr(llvm::GlobalValue::UnnamedAddr::Global);
+ CGM.SetLLVMFunctionAttributesForDefinition(GlobOD, EmptyFn);
auto *BB = llvm::BasicBlock::Create(LLVMCtx, "", EmptyFn);
llvm::ReturnInst::Create(LLVMCtx, BB);
@@ -1750,6 +1751,8 @@ struct CallDtorDelete final : EHScopeStack::Cleanup {
void EmitConditionalDtorDeleteCall(CodeGenFunction &CGF,
llvm::Value *ShouldDeleteCondition,
bool ReturnAfterDelete) {
+ assert(CGF.CGM.getTarget().getCXXABI().isMicrosoft() &&
+ "deleting destructor should only be emitted for MSVC ABI");
const CXXDestructorDecl *Dtor = cast<CXXDestructorDecl>(CGF.CurCodeDecl);
const CXXRecordDecl *ClassDecl = Dtor->getParent();
const FunctionDecl *OD = Dtor->getOperatorDelete();
>From b19de90b52e8a1c0d09f9d94b8378fd4795e05f7 Mon Sep 17 00:00:00 2001
From: "Daniel Paoliello (HE/HIM)" <danpao at microsoft.com>
Date: Wed, 25 Mar 2026 10:26:34 -0700
Subject: [PATCH 3/5] Move assert to correct location
---
clang/lib/CodeGen/CGClass.cpp | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/clang/lib/CodeGen/CGClass.cpp b/clang/lib/CodeGen/CGClass.cpp
index 107821116b3f1..ec42be01c5f2b 100644
--- a/clang/lib/CodeGen/CGClass.cpp
+++ b/clang/lib/CodeGen/CGClass.cpp
@@ -1419,6 +1419,8 @@ static bool CanSkipVTablePointerInitialization(CodeGenFunction &CGF,
static llvm::Constant *
getOrCreateMSVCGlobalDeleteWrapper(CodeGenModule &CGM,
const FunctionDecl *GlobOD) {
+ assert(CGM.getTarget().getCXXABI().isMicrosoft() &&
+ "__global_delete wrapper is only used with the Microsoft ABI");
llvm::Module &M = CGM.getModule();
llvm::LLVMContext &LLVMCtx = M.getContext();
@@ -1751,8 +1753,6 @@ struct CallDtorDelete final : EHScopeStack::Cleanup {
void EmitConditionalDtorDeleteCall(CodeGenFunction &CGF,
llvm::Value *ShouldDeleteCondition,
bool ReturnAfterDelete) {
- assert(CGF.CGM.getTarget().getCXXABI().isMicrosoft() &&
- "deleting destructor should only be emitted for MSVC ABI");
const CXXDestructorDecl *Dtor = cast<CXXDestructorDecl>(CGF.CurCodeDecl);
const CXXRecordDecl *ClassDecl = Dtor->getParent();
const FunctionDecl *OD = Dtor->getOperatorDelete();
>From db1aeccf5e9a4dcc14d99aff16053e6b85e1b4d0 Mon Sep 17 00:00:00 2001
From: Daniel Paoliello <danpao at microsoft.com>
Date: Wed, 1 Apr 2026 11:00:46 -0700
Subject: [PATCH 4/5] Generate the __global_delete wrapper when required as
well
---
clang/docs/ReleaseNotes.rst | 9 ++--
clang/lib/CodeGen/CGClass.cpp | 43 ++++++++++------
clang/lib/CodeGen/CGExprCXX.cpp | 6 +++
clang/lib/CodeGen/CodeGenModule.cpp | 49 +++++++++++++++++++
clang/lib/CodeGen/CodeGenModule.h | 21 ++++++++
.../microsoft-vector-deleting-dtors.cpp | 16 ++++--
6 files changed, 123 insertions(+), 21 deletions(-)
diff --git a/clang/docs/ReleaseNotes.rst b/clang/docs/ReleaseNotes.rst
index 544fd9b2a0505..240ee456cdb5b 100644
--- a/clang/docs/ReleaseNotes.rst
+++ b/clang/docs/ReleaseNotes.rst
@@ -517,9 +517,12 @@ Windows Support
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.
+ ``__global_delete`` instead of directly referencing ``::operator delete``.
+ This matches MSVC's behavior and fixes ``LNK2001`` linker errors in
+ environments (such as kernel mode) where no global ``::operator delete``
+ exists. When the translation unit contains a ``::delete`` expression, a
+ ``__global_delete`` forwarding body that calls ``::operator delete`` is
+ emitted automatically.
LoongArch Support
^^^^^^^^^^^^^^^^^
diff --git a/clang/lib/CodeGen/CGClass.cpp b/clang/lib/CodeGen/CGClass.cpp
index ec42be01c5f2b..a7ee41b6b6870 100644
--- a/clang/lib/CodeGen/CGClass.cpp
+++ b/clang/lib/CodeGen/CGClass.cpp
@@ -1412,10 +1412,14 @@ static bool CanSkipVTablePointerInitialization(CodeGenFunction &CGF,
/// 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.
+/// Destructor helpers call __global_delete instead of ::operator delete
+/// directly. __global_delete is a weak symbol that defaults to
+/// __empty_global_delete (a trap) via /ALTERNATENAME. When this TU contains
+/// a ::delete expression, a real forwarding body is emitted at end-of-file
+/// (see CodeGenModule::Release). If ::delete is never used anywhere in the
+/// program, the trap default wins - but that path is unreachable at runtime,
+/// so it only fires if something is seriously wrong (e.g., memory
+/// corruption). Both symbols are compiler-generated.
static llvm::Constant *
getOrCreateMSVCGlobalDeleteWrapper(CodeGenModule &CGM,
const FunctionDecl *GlobOD) {
@@ -1451,17 +1455,23 @@ getOrCreateMSVCGlobalDeleteWrapper(CodeGenModule &CGM,
if (llvm::Function *Existing = M.getFunction(GlobalDeleteName))
return Existing;
- // Create __empty_global_delete fallback.
+ // Create __empty_global_delete fallback (trap - this path is unreachable
+ // at runtime when ::delete is never used; see the doc comment above).
llvm::Function *EmptyFn = llvm::Function::Create(
FnTy, llvm::GlobalValue::LinkOnceODRLinkage, EmptyGlobalDeleteName, &M);
EmptyFn->setComdat(M.getOrInsertComdat(EmptyGlobalDeleteName));
EmptyFn->setUnnamedAddr(llvm::GlobalValue::UnnamedAddr::Global);
CGM.SetLLVMFunctionAttributesForDefinition(GlobOD, EmptyFn);
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.
+ llvm::Function *TrapFn =
+ llvm::Intrinsic::getOrInsertDeclaration(&M, llvm::Intrinsic::trap);
+ auto *TrapCall = llvm::CallInst::Create(TrapFn, {}, "", BB);
+ TrapCall->setDoesNotReturn();
+ TrapCall->setDoesNotThrow();
+ new llvm::UnreachableInst(LLVMCtx, BB);
+
+ // Emit /ALTERNATENAME linker directive: if __global_delete isn't provided,
+ // fall back to the trapping __empty_global_delete.
std::string AltOption =
"/alternatename:" + GlobalDeleteName + "=" + EmptyGlobalDeleteName;
auto *AltMD =
@@ -1474,6 +1484,11 @@ getOrCreateMSVCGlobalDeleteWrapper(CodeGenModule &CGM,
// Return the __global_delete wrapper function to call.
auto GlobalDeleteCallee = M.getOrInsertFunction(GlobalDeleteName, FnTy);
+
+ // Register this variant so we can emit a real forwarding body at end-of-TU
+ // if this TU contains any direct use of global ::operator delete.
+ CGM.addPendingGlobalDelete(GlobalDeleteName, GlobOD);
+
return cast<llvm::Function>(GlobalDeleteCallee.getCallee());
}
@@ -1560,8 +1575,9 @@ 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.
+ // Use __global_delete wrapper instead of directly calling
+ // ::operator delete to match MSVC's behavior. See the doc comment on
+ // getOrCreateMSVCGlobalDeleteWrapper for details.
llvm::Constant *GlobalDeleteWrapper = getOrCreateMSVCGlobalDeleteWrapper(
CGF.CGM, Dtor->getGlobalArrayOperatorDelete());
CGF.EmitDeleteCall(Dtor->getGlobalArrayOperatorDelete(), allocatedPtr,
@@ -1823,9 +1839,8 @@ void EmitConditionalDtorDeleteCall(CodeGenFunction &CGF,
CGF.EmitBlock(GlobDelete);
// 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).
+ // ::operator delete to match MSVC's behavior. See the doc comment on
+ // getOrCreateMSVCGlobalDeleteWrapper for details.
llvm::Constant *GlobalDeleteWrapper =
getOrCreateMSVCGlobalDeleteWrapper(CGF.CGM, GlobOD);
EmitDeleteAndGoToEnd(GlobOD, GlobalDeleteWrapper);
diff --git a/clang/lib/CodeGen/CGExprCXX.cpp b/clang/lib/CodeGen/CGExprCXX.cpp
index 630a0159ff907..5b7f9d9e6415d 100644
--- a/clang/lib/CodeGen/CGExprCXX.cpp
+++ b/clang/lib/CodeGen/CGExprCXX.cpp
@@ -2072,6 +2072,12 @@ void CodeGenFunction::EmitCXXDeleteExpr(const CXXDeleteExpr *E) {
const Expr *Arg = E->getArgument();
Address Ptr = EmitPointerWithAlignment(Arg);
+ // If this delete expression uses global ::operator delete (not a
+ // class-specific one), note it so we emit __global_delete forwarding bodies.
+ if (!isa<CXXMethodDecl>(E->getOperatorDelete()) &&
+ CGM.getTarget().getCXXABI().isMicrosoft())
+ CGM.noteDirectGlobalDelete();
+
// Null check the pointer.
//
// We could avoid this null check if we can determine that the object
diff --git a/clang/lib/CodeGen/CodeGenModule.cpp b/clang/lib/CodeGen/CodeGenModule.cpp
index bb399a71aa047..556bfe29d0f2d 100644
--- a/clang/lib/CodeGen/CodeGenModule.cpp
+++ b/clang/lib/CodeGen/CodeGenModule.cpp
@@ -992,6 +992,7 @@ void CodeGenModule::Release() {
applyReplacements();
emitMultiVersionFunctions();
emitPFPFieldsWithEvaluatedOffset();
+ emitGlobalDeleteForwardingBodies();
if (Context.getLangOpts().IncrementalExtensions &&
GlobalTopLevelStmtBlockInFlight.first) {
@@ -8651,3 +8652,51 @@ void CodeGenModule::requireVectorDestructorDefinition(const CXXRecordDecl *RD) {
// even if destructor is only declared.
addDeferredDeclToEmit(VectorDtorGD);
}
+
+void CodeGenModule::addPendingGlobalDelete(
+ StringRef GlobalDeleteName, const FunctionDecl *OperatorDeleteFD) {
+ // Only add if we haven't seen this name before.
+ for (const auto &Entry : PendingMSVCGlobalDeletes)
+ if (Entry.first == GlobalDeleteName)
+ return;
+ PendingMSVCGlobalDeletes.emplace_back(GlobalDeleteName.str(),
+ OperatorDeleteFD);
+}
+
+void CodeGenModule::noteDirectGlobalDelete() { HasDirectGlobalDelete = true; }
+
+void CodeGenModule::emitGlobalDeleteForwardingBodies() {
+ // MSVC-compatible __global_delete forwarding bodies.
+ //
+ // Destructor helpers call __global_delete (a weak symbol defaulting to the
+ // trapping __empty_global_delete) instead of ::operator delete directly.
+ // When this TU contains a ::delete expression, we know ::operator delete
+ // must exist, so we emit a real __global_delete definition that forwards
+ // to it.
+ if (!HasDirectGlobalDelete)
+ return;
+
+ for (const auto &Entry : PendingMSVCGlobalDeletes) {
+ llvm::Function *GlobDelFn = getModule().getFunction(Entry.first);
+ if (!GlobDelFn || !GlobDelFn->isDeclaration())
+ continue;
+
+ const FunctionDecl *OperatorDeleteFD = Entry.second;
+ llvm::Constant *RealDeleteFn = GetAddrOfFunction(OperatorDeleteFD);
+
+ // Create the forwarding body: call ::operator delete with all args.
+ auto *BB =
+ llvm::BasicBlock::Create(getModule().getContext(), "", GlobDelFn);
+ llvm::SmallVector<llvm::Value *, 4> Args;
+ for (auto &Arg : GlobDelFn->args())
+ Args.push_back(&Arg);
+ llvm::CallInst::Create(GlobDelFn->getFunctionType(), RealDeleteFn, Args, "",
+ BB);
+ llvm::ReturnInst::Create(getModule().getContext(), BB);
+
+ // Use LinkOnceODR so multiple TUs can emit this without conflicts.
+ GlobDelFn->setLinkage(llvm::GlobalValue::LinkOnceODRLinkage);
+ GlobDelFn->setComdat(getModule().getOrInsertComdat(GlobDelFn->getName()));
+ SetLLVMFunctionAttributesForDefinition(OperatorDeleteFD, GlobDelFn);
+ }
+}
diff --git a/clang/lib/CodeGen/CodeGenModule.h b/clang/lib/CodeGen/CodeGenModule.h
index d62707a3355c9..bb20ba479585b 100644
--- a/clang/lib/CodeGen/CodeGenModule.h
+++ b/clang/lib/CodeGen/CodeGenModule.h
@@ -534,6 +534,16 @@ class CodeGenModule : public CodeGenTypeCache {
/// was emitted for the class.
llvm::SmallPtrSet<const CXXRecordDecl *, 16> RequireVectorDeletingDtor;
+ /// Pending MSVC __global_delete variants that may need forwarding bodies.
+ /// Stores the __global_delete mangled name and the corresponding global
+ /// ::operator delete FunctionDecl, in insertion order.
+ llvm::SmallVector<std::pair<std::string, const FunctionDecl *>, 4>
+ PendingMSVCGlobalDeletes;
+
+ /// Whether this TU contains a direct use of global ::operator delete
+ /// (indicating that __global_delete forwarding bodies should be emitted).
+ bool HasDirectGlobalDelete = false;
+
typedef std::pair<OrderGlobalInitsOrStermFinalizers, llvm::Function *>
GlobalInitData;
@@ -1591,6 +1601,17 @@ class CodeGenModule : public CodeGenTypeCache {
/// destructor definition in a form of alias to the actual definition.
void requireVectorDestructorDefinition(const CXXRecordDecl *RD);
+ /// Record a pending __global_delete variant that may need a forwarding body.
+ void addPendingGlobalDelete(StringRef GlobalDeleteName,
+ const FunctionDecl *OperatorDeleteFD);
+
+ /// Note that global ::operator delete is directly used in this TU.
+ void noteDirectGlobalDelete();
+
+ /// Emit __global_delete forwarding bodies for any pending variants,
+ /// if this TU directly uses global ::operator delete.
+ void emitGlobalDeleteForwardingBodies();
+
/// Check that class need vector deleting destructor body.
bool classNeedsVectorDestructor(const CXXRecordDecl *RD);
diff --git a/clang/test/CodeGenCXX/microsoft-vector-deleting-dtors.cpp b/clang/test/CodeGenCXX/microsoft-vector-deleting-dtors.cpp
index 4891ce12f8fed..e517e4e1ccac0 100644
--- a/clang/test/CodeGenCXX/microsoft-vector-deleting-dtors.cpp
+++ b/clang/test/CodeGenCXX/microsoft-vector-deleting-dtors.cpp
@@ -284,12 +284,20 @@ void kernelTest() {
// 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.
+// destructor calls __global_delete (a weak external) instead of directly
+// referencing ::operator delete. This is critical for environments like
+// kernel mode where no global ::operator delete exists.
+// Verify __empty_global_delete traps (the code path is unreachable at runtime).
// X64: define linkonce_odr void @"?__empty_global_delete@@YAXPEAX_K at Z"(ptr %0, i64 %1)
+// X64-NEXT: call void @llvm.trap()
+// X64-NEXT: unreachable
+
+// Verify that when ::delete is used in the TU, a real __global_delete
+// forwarding body is emitted that calls through to the actual ::operator delete.
+// X64: define linkonce_odr void @"?__global_delete@@YAXPEAX_K at Z"(ptr %0, i64 %1)
+// X64-NEXT: call void @"??_V at YAXPEAX_K@Z"(ptr %0, i64 %1)
// X64-NEXT: ret void
+
// X64-LABEL: define weak dso_local noundef ptr @"??_EKernelDerived@@UEAAPEAXI at Z"
// Verify the array delete path in the VDD uses __global_delete.
// X64: dtor.call_glob_delete_after_array_destroy:
>From c2edf9b1401480e0f4d77b37d26ae87ea0260511 Mon Sep 17 00:00:00 2001
From: Daniel Paoliello <danpao at microsoft.com>
Date: Mon, 13 Apr 2026 14:46:51 -0700
Subject: [PATCH 5/5] Handle dllexport, only mark global delete op as being
used if it is an explicit use of the global delete op
---
clang/lib/CodeGen/CGClass.cpp | 8 ++++
clang/lib/CodeGen/CGExprCXX.cpp | 8 ++--
.../microsoft-vector-deleting-dtors2.cpp | 6 +++
.../msvc-no-global-delete-forwarding.cpp | 44 +++++++++++++++++++
4 files changed, 62 insertions(+), 4 deletions(-)
create mode 100644 clang/test/CodeGenCXX/msvc-no-global-delete-forwarding.cpp
diff --git a/clang/lib/CodeGen/CGClass.cpp b/clang/lib/CodeGen/CGClass.cpp
index a7ee41b6b6870..d9c18b18cc2a4 100644
--- a/clang/lib/CodeGen/CGClass.cpp
+++ b/clang/lib/CodeGen/CGClass.cpp
@@ -1580,6 +1580,10 @@ static void EmitConditionalArrayDtorCall(const CXXDestructorDecl *DD,
// getOrCreateMSVCGlobalDeleteWrapper for details.
llvm::Constant *GlobalDeleteWrapper = getOrCreateMSVCGlobalDeleteWrapper(
CGF.CGM, Dtor->getGlobalArrayOperatorDelete());
+ // For dllexport classes, emit forwarding bodies since the dtor is
+ // exported and another TU may not provide the forwarding body.
+ if (Dtor->hasAttr<DLLExportAttr>())
+ CGF.CGM.noteDirectGlobalDelete();
CGF.EmitDeleteCall(Dtor->getGlobalArrayOperatorDelete(), allocatedPtr,
CGF.getContext().getCanonicalTagType(ClassDecl),
numElements, cookieSize, GlobalDeleteWrapper);
@@ -1843,6 +1847,10 @@ void EmitConditionalDtorDeleteCall(CodeGenFunction &CGF,
// getOrCreateMSVCGlobalDeleteWrapper for details.
llvm::Constant *GlobalDeleteWrapper =
getOrCreateMSVCGlobalDeleteWrapper(CGF.CGM, GlobOD);
+ // For dllexport classes, emit forwarding bodies since the dtor is
+ // exported and another TU may not provide the forwarding body.
+ if (Dtor->hasAttr<DLLExportAttr>())
+ CGF.CGM.noteDirectGlobalDelete();
EmitDeleteAndGoToEnd(GlobOD, GlobalDeleteWrapper);
CGF.EmitBlock(ClassDelete);
}
diff --git a/clang/lib/CodeGen/CGExprCXX.cpp b/clang/lib/CodeGen/CGExprCXX.cpp
index 5b7f9d9e6415d..1bc5e576723f2 100644
--- a/clang/lib/CodeGen/CGExprCXX.cpp
+++ b/clang/lib/CodeGen/CGExprCXX.cpp
@@ -2072,10 +2072,10 @@ void CodeGenFunction::EmitCXXDeleteExpr(const CXXDeleteExpr *E) {
const Expr *Arg = E->getArgument();
Address Ptr = EmitPointerWithAlignment(Arg);
- // If this delete expression uses global ::operator delete (not a
- // class-specific one), note it so we emit __global_delete forwarding bodies.
- if (!isa<CXXMethodDecl>(E->getOperatorDelete()) &&
- CGM.getTarget().getCXXABI().isMicrosoft())
+ // If this is a ::delete expression (explicit global scope), note it so we
+ // emit __global_delete forwarding bodies. Only ::delete triggers this, not
+ // regular delete expressions that happen to resolve to a global operator.
+ if (E->isGlobalDelete() && CGM.getTarget().getCXXABI().isMicrosoft())
CGM.noteDirectGlobalDelete();
// Null check the pointer.
diff --git a/clang/test/CodeGenCXX/microsoft-vector-deleting-dtors2.cpp b/clang/test/CodeGenCXX/microsoft-vector-deleting-dtors2.cpp
index 904df6b528bfe..7483731d31d46 100644
--- a/clang/test/CodeGenCXX/microsoft-vector-deleting-dtors2.cpp
+++ b/clang/test/CodeGenCXX/microsoft-vector-deleting-dtors2.cpp
@@ -94,6 +94,12 @@ void TesttheTest() {
// X64: define linkonce_odr dso_local void @"??_V?$RefCounted at UDrawingBuffer@@@@SAXPEAX at Z"(ptr noundef %p)
// X86: define linkonce_odr dso_local void @"??_V?$RefCounted at UDrawingBuffer@@@@SAXPAX at Z"(ptr noundef %p)
+// Verify that the dllexport class triggers __global_delete forwarding body
+// emission even without a ::delete expression in the TU.
+// X64: define linkonce_odr void @"?__global_delete@@YAXPEAX_K at Z"(ptr %0, i64 %1)
+// X64-NEXT: call void @"??_V at YAXPEAX_K@Z"(ptr %0, i64 %1)
+// X64-NEXT: ret void
+
// X86: define linkonce_odr dso_local x86_thiscallcc noundef ptr @"??_GNoExport@@UAEPAXI at Z"(ptr noundef nonnull align 4 dereferenceable(4) %this, i32 noundef %should_call_delete)
// X64: define linkonce_odr dso_local noundef ptr @"??_GNoExport@@UEAAPEAXI at Z"(ptr noundef nonnull align 8 dereferenceable(8) %this, i32 noundef %should_call_delete)
// CHECK-NOT: define {{.*}}_V{{.*}}NoExport
diff --git a/clang/test/CodeGenCXX/msvc-no-global-delete-forwarding.cpp b/clang/test/CodeGenCXX/msvc-no-global-delete-forwarding.cpp
new file mode 100644
index 0000000000000..8c75ca18cb9b7
--- /dev/null
+++ b/clang/test/CodeGenCXX/msvc-no-global-delete-forwarding.cpp
@@ -0,0 +1,44 @@
+// RUN: %clang_cc1 -emit-llvm -fms-extensions %s -triple=x86_64-pc-windows-msvc -o - | FileCheck %s
+
+// Verify that regular delete (not ::delete) does NOT trigger __global_delete
+// forwarding body emission, but the VDD still uses __global_delete wrapper.
+// This matches MSVC behavior where only ::delete triggers forwarding bodies.
+
+struct Base {
+ void* operator new(__SIZE_TYPE__);
+ void operator delete(void*);
+ void operator delete[](void*);
+ virtual ~Base();
+};
+struct Derived : Base {
+ virtual ~Derived();
+};
+Base::~Base() {}
+Derived::~Derived() {}
+
+// new[] forces VDD emission; regular delete[], not ::delete[].
+void test() {
+ Base *p = new Derived[2];
+ delete[] p;
+}
+
+// The VDD dispatches between class and global delete using __global_delete.
+// CHECK-LABEL: define weak dso_local noundef ptr @"??_EDerived@@UEAAPEAXI at Z"
+// CHECK: dtor.call_glob_delete_after_array_destroy:
+// CHECK: call void @"?__global_delete@@YAXPEAX_K at Z"(ptr noundef %{{.*}}, i64 noundef %{{.*}})
+// CHECK: dtor.call_glob_delete:
+// CHECK-NEXT: call void @"?__global_delete@@YAXPEAX_K at Z"(ptr noundef %{{.*}}, i64 noundef 8)
+// CHECK: dtor.call_class_delete:
+// CHECK-NEXT: call void @"??3Base@@SAXPEAX at Z"(ptr noundef %{{.*}})
+
+// __empty_global_delete should be emitted with a trap.
+// CHECK: define linkonce_odr void @"?__empty_global_delete@@YAXPEAX_K at Z"(ptr %0, i64 %1)
+// CHECK-NEXT: call void @llvm.trap()
+// CHECK-NEXT: unreachable
+
+// __global_delete should NOT have a forwarding body (no ::delete in this TU,
+// no dllexport class).
+// CHECK-NOT: define {{.*}}void @"?__global_delete@@YAXPEAX_K at Z"
+
+// Verify the /ALTERNATENAME linker directive.
+// CHECK: !{!"/alternatename:?__global_delete@@YAXPEAX_K at Z=?__empty_global_delete@@YAXPEAX_K at Z"}
More information about the cfe-commits
mailing list