[clang] [clang][win] Fix __global_delete wrappers for Arm64EC and cross-TU ::delete (PR #209585)
Daniel Paoliello via cfe-commits
cfe-commits at lists.llvm.org
Thu Jul 16 10:51:12 PDT 2026
https://github.com/dpaoliello updated https://github.com/llvm/llvm-project/pull/209585
>From aee831143de94dd8aeffd9e36a7ef114259109df Mon Sep 17 00:00:00 2001
From: Daniel Paoliello <danpao at microsoft.com>
Date: Tue, 14 Jul 2026 11:13:24 -0700
Subject: [PATCH 1/2] [clang][win] Fix __global_delete wrappers for Arm64EC and
cross-TU ::delete
The MSVC-compatible deleting-destructor path routes global deletes through
compiler-generated __global_delete / __global_array_delete wrappers, each
defaulting to a trapping __empty_global_delete fallback that a real forwarding
body overrides when the program actually uses ::delete. Two problems made this
fall over in practice:
1. Arm64EC (miscompile -> LNK2019). The fallback was previously wired up with
an /alternatename directive. /alternatename only names the plain symbol, not
the backend-generated "$$h" hybrid EC symbol that the exit thunk references,
so linking an Arm64EC image left __global_delete$exit_thunk with an
unresolved external. Emit the fallback as a weak GlobalAlias instead. That
lowers to a COFF weak-external-with-default, which is exactly what MSVC does
and which correctly produces the "$$h" symbol, exit thunk, and plain alias
(verified on both x64 and Arm64EC).
2. Cross-TU ::delete (runtime crash). The strong forwarding body was only
emitted in a TU that *both* emitted a matching vector deleting destructor and
contained a ::delete. A TU that only performs `::delete p` -- with the class
(and its deleting destructor) defined in another TU -- emitted no forwarder,
so nothing overrode the weak-alias trap and the program executed a trapping
__empty_global_delete at runtime (STATUS_ILLEGAL_INSTRUCTION). MSVC emits the
forwarder at every ::delete site; do the same by registering the wrapper for
the resolved global operator delete in EmitCXXDeleteExpr, so a strong body is
emitted locally regardless of where the destructor lives.
To share the 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.
---
clang/lib/CodeGen/CGClass.cpp | 113 +-------------
clang/lib/CodeGen/CGExprCXX.cpp | 12 +-
clang/lib/CodeGen/CodeGenModule.cpp | 140 ++++++++++++++++--
clang/lib/CodeGen/CodeGenModule.h | 13 +-
.../microsoft-vector-deleting-dtors.cpp | 29 ++--
.../microsoft-vector-deleting-dtors2.cpp | 10 +-
...lobal-delete-forwarding-at-delete-site.cpp | 34 +++++
.../msvc-global-delete-scalar-array-split.cpp | 21 +--
.../msvc-global-delete-scope-no-dtor.cpp | 23 +--
.../msvc-no-global-delete-forwarding.cpp | 10 +-
10 files changed, 239 insertions(+), 166 deletions(-)
create mode 100644 clang/test/CodeGenCXX/msvc-global-delete-forwarding-at-delete-site.cpp
diff --git a/clang/lib/CodeGen/CGClass.cpp b/clang/lib/CodeGen/CGClass.cpp
index 70666dffdc903..39b8e50f68eaf 100644
--- a/clang/lib/CodeGen/CGClass.cpp
+++ b/clang/lib/CodeGen/CGClass.cpp
@@ -1410,112 +1410,6 @@ static bool CanSkipVTablePointerInitialization(CodeGenFunction &CGF,
return true;
}
-/// Get or create the MSVC-compatible __global_delete wrapper function.
-///
-/// Destructor helpers call __global_delete instead of ::operator delete
-/// directly. If this TU contains a ::delete expression (or a dllexport class
-/// whose deleting destructor takes the global-delete path), a real forwarding
-/// body is emitted at end-of-file. If ::delete is never used anywhere in the
-/// program, then no definition will exist and the `/ALTERNATENAME` linker
-/// directive will cause the linker to use __empty_global_delete as the
-/// definition. __empty_global_delete is never expected to actually be called,
-/// hence it is a trap function (a deliberate deviation from MSVC, whose empty
-/// is a no-op).
-///
-/// Array delete[] uses a parallel __global_array_delete wrapper, matching
-/// MSVC. The scalar and array wrappers of a given signature share a single
-/// __empty_global_delete fallback.
-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();
-
- llvm::Constant *GlobDeleteCallee = CGM.GetAddrOfFunction(GlobOD);
- auto *GlobDeleteFn = cast<llvm::Function>(GlobDeleteCallee);
- llvm::FunctionType *FnTy = GlobDeleteFn->getFunctionType();
-
- // Derive the wrapper and empty-fallback mangled names. MSVC uses distinct
- // wrapper names for scalar vs array global delete, but a single shared empty
- // fallback per signature:
- // Global ::operator delete mangling: ??3@<signature>
- // -> wrapper ?__global_delete@@<signature>
- // Global ::operator delete[] mangling: ??_V@<signature>
- // -> wrapper ?__global_array_delete@@<signature>
- // shared fallback: ?__empty_global_delete@@<signature>
- StringRef GlobDeleteMangledName = GlobDeleteFn->getName();
- StringRef Signature;
- const char *WrapperBase;
- if (GlobDeleteMangledName.starts_with("??3@")) {
- Signature = GlobDeleteMangledName.substr(4);
- WrapperBase = "?__global_delete@@";
- } else if (GlobDeleteMangledName.starts_with("??_V@")) {
- Signature = GlobDeleteMangledName.substr(5);
- WrapperBase = "?__global_array_delete@@";
- } else {
- llvm_unreachable("unexpected global operator delete mangling");
- }
-
- std::string GlobalDeleteName = (WrapperBase + 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 the shared __empty_global_delete fallback if it doesn't already
- // exist. The scalar and array wrappers of a given signature share one empty
- // (matching MSVC, whose weak externals both point at a single
- // __empty_global_delete). The body traps: this path is unreachable at
- // runtime when ::delete is never used (a deliberate deviation from MSVC,
- // whose empty is a no-op; see the doc comment above).
- llvm::Function *EmptyFn = M.getFunction(EmptyGlobalDeleteName);
- if (!EmptyFn) {
- EmptyFn = llvm::Function::Create(
- FnTy, llvm::GlobalValue::LinkOnceODRLinkage, EmptyGlobalDeleteName, &M);
- EmptyFn->setComdat(M.getOrInsertComdat(EmptyGlobalDeleteName));
- EmptyFn->setUnnamedAddr(llvm::GlobalValue::UnnamedAddr::Global);
- CGM.SetLLVMFunctionAttributes(
- GlobalDecl(GlobOD),
- CGM.getTypes().arrangeGlobalDeclaration(GlobalDecl(GlobOD)), EmptyFn,
- /*IsThunk=*/false);
- CGM.SetLLVMFunctionAttributesForDefinition(GlobOD, EmptyFn);
- CGM.getTargetCodeGenInfo().setTargetAttributes(GlobOD, EmptyFn, CGM);
- auto *BB = llvm::BasicBlock::Create(LLVMCtx, "", EmptyFn);
- 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);
-
- // Nothing directly uses the empty other than the /alternatename directive,
- // so explicitly mark it as used.
- appendToUsed(M, {EmptyFn});
- }
-
- // Emit /ALTERNATENAME linker directive: if this wrapper isn't provided,
- // fall back to the trapping __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);
-
- // Return the __global_delete wrapper function to call.
- auto GlobalDeleteCallee = M.getOrInsertFunction(GlobalDeleteName, FnTy);
- auto *GlobalDeleteFn = cast<llvm::Function>(GlobalDeleteCallee.getCallee());
-
- // 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(GlobalDeleteFn, GlobOD);
-
- return GlobalDeleteFn;
-}
-
static void EmitConditionalArrayDtorCall(const CXXDestructorDecl *DD,
CodeGenFunction &CGF,
llvm::Value *ShouldDeleteCondition) {
@@ -1602,8 +1496,9 @@ static void EmitConditionalArrayDtorCall(const CXXDestructorDecl *DD,
// 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());
+ llvm::Constant *GlobalDeleteWrapper =
+ CGF.CGM.getOrCreateMSVCGlobalDeleteWrapper(
+ 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>())
@@ -1870,7 +1765,7 @@ void EmitConditionalDtorDeleteCall(CodeGenFunction &CGF,
// ::operator delete to match MSVC's behavior. See the doc comment on
// getOrCreateMSVCGlobalDeleteWrapper for details.
llvm::Constant *GlobalDeleteWrapper =
- getOrCreateMSVCGlobalDeleteWrapper(CGF.CGM, GlobOD);
+ CGF.CGM.getOrCreateMSVCGlobalDeleteWrapper(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>())
diff --git a/clang/lib/CodeGen/CGExprCXX.cpp b/clang/lib/CodeGen/CGExprCXX.cpp
index 52e0fdbc59a11..4725403b42932 100644
--- a/clang/lib/CodeGen/CGExprCXX.cpp
+++ b/clang/lib/CodeGen/CGExprCXX.cpp
@@ -2108,8 +2108,18 @@ void CodeGenFunction::EmitCXXDeleteExpr(const CXXDeleteExpr *E) {
// operator delete are both irrelevant to the trigger.
if (E->isGlobalDelete() && CGM.getTarget().getCXXABI().isMicrosoft()) {
const CXXRecordDecl *RD = E->getDestroyedType()->getAsCXXRecordDecl();
- if (RD && RD->hasDefinition() && !RD->hasTrivialDestructor())
+ if (RD && RD->hasDefinition() && !RD->hasTrivialDestructor()) {
CGM.noteDirectGlobalDelete();
+ // Ensure a __global_delete wrapper (and thus a strong forwarding body)
+ // is emitted in THIS TU for the resolved global ::operator delete, even
+ // when no vector deleting destructor here references it. Without this, a
+ // TU that only does ::delete (with the deleting destructor defined in
+ // another TU) would emit no forwarder, leaving the wrapper bound to the
+ // trapping empty fallback and crashing at runtime.
+ if (const FunctionDecl *OD = E->getOperatorDelete();
+ OD && !isa<CXXMethodDecl>(OD))
+ CGM.getOrCreateMSVCGlobalDeleteWrapper(OD);
+ }
}
// Null check the pointer.
diff --git a/clang/lib/CodeGen/CodeGenModule.cpp b/clang/lib/CodeGen/CodeGenModule.cpp
index 78627047b19ad..ca71458d85134 100644
--- a/clang/lib/CodeGen/CodeGenModule.cpp
+++ b/clang/lib/CodeGen/CodeGenModule.cpp
@@ -8906,45 +8906,159 @@ void CodeGenModule::requireVectorDestructorDefinition(const CXXRecordDecl *RD) {
}
void CodeGenModule::addPendingGlobalDelete(
- llvm::Function *GlobalDeleteFn, const FunctionDecl *OperatorDeleteFD) {
+ llvm::GlobalAlias *GlobalDeleteAlias,
+ const FunctionDecl *OperatorDeleteFD) {
// insert() is a no-op if this wrapper has already been recorded, keeping the
// first FunctionDecl seen for it.
- PendingMSVCGlobalDeletes.insert({GlobalDeleteFn, OperatorDeleteFD});
+ PendingMSVCGlobalDeletes.insert({GlobalDeleteAlias, OperatorDeleteFD});
}
void CodeGenModule::noteDirectGlobalDelete() { HasDirectGlobalDelete = true; }
+/// Get or create the MSVC-compatible __global_delete wrapper function.
+///
+/// Destructor helpers call __global_delete instead of ::operator delete
+/// directly. If this TU contains a ::delete expression (or a dllexport class
+/// whose deleting destructor takes the global-delete path), a real forwarding
+/// body is emitted at end-of-file. If ::delete is never used anywhere in the
+/// program, then no forwarding body is emitted and the wrapper defaults to a
+/// weak alias to __empty_global_delete. __empty_global_delete is never
+/// expected to actually be called, hence it is a trap function (a deliberate
+/// deviation from MSVC, whose empty is a no-op).
+///
+/// Array delete[] uses a parallel __global_array_delete wrapper, matching
+/// MSVC. The scalar and array wrappers of a given signature share a single
+/// __empty_global_delete fallback.
+llvm::Constant *
+CodeGenModule::getOrCreateMSVCGlobalDeleteWrapper(const FunctionDecl *GlobOD) {
+ assert(getTarget().getCXXABI().isMicrosoft() &&
+ "__global_delete wrapper is only used with the Microsoft ABI");
+ llvm::Module &M = getModule();
+ llvm::LLVMContext &LLVMCtx = M.getContext();
+
+ llvm::Constant *GlobDeleteCallee = GetAddrOfFunction(GlobOD);
+ auto *GlobDeleteFn = cast<llvm::Function>(GlobDeleteCallee);
+ llvm::FunctionType *FnTy = GlobDeleteFn->getFunctionType();
+
+ // Derive the wrapper and empty-fallback mangled names. MSVC uses distinct
+ // wrapper names for scalar vs array global delete, but a single shared empty
+ // fallback per signature:
+ // Global ::operator delete mangling: ??3@<signature>
+ // -> wrapper ?__global_delete@@<signature>
+ // Global ::operator delete[] mangling: ??_V@<signature>
+ // -> wrapper ?__global_array_delete@@<signature>
+ // shared fallback: ?__empty_global_delete@@<signature>
+ StringRef GlobDeleteMangledName = GlobDeleteFn->getName();
+ StringRef Signature;
+ const char *WrapperBase;
+ if (GlobDeleteMangledName.starts_with("??3@")) {
+ Signature = GlobDeleteMangledName.substr(4);
+ WrapperBase = "?__global_delete@@";
+ } else if (GlobDeleteMangledName.starts_with("??_V@")) {
+ Signature = GlobDeleteMangledName.substr(5);
+ WrapperBase = "?__global_array_delete@@";
+ } else {
+ llvm_unreachable("unexpected global operator delete mangling");
+ }
+
+ std::string GlobalDeleteName = (WrapperBase + Signature).str();
+ std::string EmptyGlobalDeleteName =
+ ("?__empty_global_delete@@" + Signature).str();
+
+ // Only set up the wrapper once per module. The wrapper may be a weak alias
+ // (the default fallback) or, once replaced, a real forwarding function.
+ if (llvm::GlobalValue *Existing = M.getNamedValue(GlobalDeleteName))
+ return Existing;
+
+ // Create the shared __empty_global_delete fallback if it doesn't already
+ // exist. The scalar and array wrappers of a given signature share one empty
+ // (matching MSVC, whose weak externals both point at a single
+ // __empty_global_delete). The body traps: this path is unreachable at
+ // runtime when ::delete is never used (a deliberate deviation from MSVC,
+ // whose empty is a no-op; see the doc comment above).
+ llvm::Function *EmptyFn = M.getFunction(EmptyGlobalDeleteName);
+ if (!EmptyFn) {
+ EmptyFn = llvm::Function::Create(
+ FnTy, llvm::GlobalValue::LinkOnceODRLinkage, EmptyGlobalDeleteName, &M);
+ EmptyFn->setComdat(M.getOrInsertComdat(EmptyGlobalDeleteName));
+ EmptyFn->setUnnamedAddr(llvm::GlobalValue::UnnamedAddr::Global);
+ SetLLVMFunctionAttributes(
+ GlobalDecl(GlobOD),
+ getTypes().arrangeGlobalDeclaration(GlobalDecl(GlobOD)), EmptyFn,
+ /*IsThunk=*/false);
+ SetLLVMFunctionAttributesForDefinition(GlobOD, EmptyFn);
+ getTargetCodeGenInfo().setTargetAttributes(GlobOD, EmptyFn, *this);
+ auto *BB = llvm::BasicBlock::Create(LLVMCtx, "", EmptyFn);
+ 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);
+
+ // The empty is referenced only by the wrapper's weak alias. When this TU
+ // uses ::delete that alias is replaced by a real forwarding body, leaving
+ // the empty otherwise unreferenced, so explicitly mark it used to ensure
+ // it is always emitted (matching MSVC).
+ appendToUsed(M, {EmptyFn});
+ }
+
+ // The wrapper defaults to a weak alias to the trapping __empty_global_delete
+ // fallback (see the doc comment above for why this is a weak alias rather
+ // than an /alternatename directive). If this TU directly uses global
+ // ::operator delete, the alias is replaced with a real forwarding body in
+ // emitGlobalDeleteForwardingBodies().
+ auto *GlobalDeleteAlias = llvm::GlobalAlias::create(
+ FnTy, GlobDeleteFn->getAddressSpace(), llvm::GlobalValue::WeakAnyLinkage,
+ GlobalDeleteName, EmptyFn, &M);
+
+ // Register this variant so we can replace the alias with a real forwarding
+ // body at end-of-TU if this TU contains any direct use of global
+ // ::operator delete.
+ addPendingGlobalDelete(GlobalDeleteAlias, GlobOD);
+
+ return GlobalDeleteAlias;
+}
+
void CodeGenModule::emitGlobalDeleteForwardingBodies() {
// MSVC-compatible __global_delete forwarding bodies.
//
// Destructor helpers call __global_delete but they are only needed if there
// is a direct use of ::operator delete. When this TU contains a ::delete
// expression (or a dllexport deleting destructor that takes the global-delete
- // path), we know ::operator delete must exist, so we emit a real
- // __global_delete definition that forwards to it.
+ // path), we know ::operator delete must exist, so we replace the wrapper's
+ // weak alias-to-empty fallback with a real __global_delete definition that
+ // forwards to it.
if (!HasDirectGlobalDelete)
return;
for (const auto &Entry : PendingMSVCGlobalDeletes) {
- llvm::Function *GlobDelFn = Entry.first;
- if (!GlobDelFn->isDeclaration())
- continue;
-
+ llvm::GlobalAlias *Alias = Entry.first;
const FunctionDecl *OperatorDeleteFD = Entry.second;
llvm::Constant *RealDeleteFn = GetAddrOfFunction(OperatorDeleteFD);
- // Create the forwarding body: call ::operator delete with all args.
+ // Create the strong forwarding function. Use LinkOnceODR so multiple TUs
+ // can emit this without conflicts.
+ auto *FnTy = cast<llvm::FunctionType>(Alias->getValueType());
+ auto *GlobDelFn =
+ llvm::Function::Create(FnTy, llvm::GlobalValue::LinkOnceODRLinkage,
+ Alias->getAddressSpace(), "", &getModule());
+
+ // Emit 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::CallInst::Create(FnTy, RealDeleteFn, Args, "", BB);
llvm::ReturnInst::Create(getModule().getContext(), BB);
- // Use LinkOnceODR so multiple TUs can emit this without conflicts.
- GlobDelFn->setLinkage(llvm::GlobalValue::LinkOnceODRLinkage);
+ // Replace the weak alias fallback with the real forwarding body, taking
+ // over its name.
+ Alias->replaceAllUsesWith(GlobDelFn);
+ GlobDelFn->takeName(Alias);
+ Alias->eraseFromParent();
+
GlobDelFn->setComdat(getModule().getOrInsertComdat(GlobDelFn->getName()));
SetLLVMFunctionAttributes(
GlobalDecl(OperatorDeleteFD),
diff --git a/clang/lib/CodeGen/CodeGenModule.h b/clang/lib/CodeGen/CodeGenModule.h
index 0abd75ccb0551..f62c761be0184 100644
--- a/clang/lib/CodeGen/CodeGenModule.h
+++ b/clang/lib/CodeGen/CodeGenModule.h
@@ -562,9 +562,9 @@ class CodeGenModule : public CodeGenTypeCache {
llvm::SmallPtrSet<const CXXRecordDecl *, 16> RequireVectorDeletingDtor;
/// Pending MSVC __global_delete variants that may need forwarding bodies.
- /// Maps each __global_delete wrapper function to the corresponding global
+ /// Maps each __global_delete wrapper alias to the corresponding global
/// ::operator delete FunctionDecl, in insertion order.
- llvm::MapVector<llvm::Function *, const FunctionDecl *>
+ llvm::MapVector<llvm::GlobalAlias *, const FunctionDecl *>
PendingMSVCGlobalDeletes;
/// Whether this TU contains a direct use of global ::operator delete
@@ -1654,9 +1654,16 @@ class CodeGenModule : public CodeGenTypeCache {
void requireVectorDestructorDefinition(const CXXRecordDecl *RD);
/// Record a pending __global_delete variant that may need a forwarding body.
- void addPendingGlobalDelete(llvm::Function *GlobalDeleteFn,
+ void addPendingGlobalDelete(llvm::GlobalAlias *GlobalDeleteAlias,
const FunctionDecl *OperatorDeleteFD);
+ /// Get or create the MSVC-compatible __global_delete wrapper for the given
+ /// global ::operator delete, registering it as a pending variant so a
+ /// forwarding body can be emitted if this TU directly uses global
+ /// ::operator delete.
+ llvm::Constant *
+ getOrCreateMSVCGlobalDeleteWrapper(const FunctionDecl *GlobOD);
+
/// Note that global ::operator delete is directly used in this TU.
void noteDirectGlobalDelete();
diff --git a/clang/test/CodeGenCXX/microsoft-vector-deleting-dtors.cpp b/clang/test/CodeGenCXX/microsoft-vector-deleting-dtors.cpp
index da891a138739c..ba1760b49f2c9 100644
--- a/clang/test/CodeGenCXX/microsoft-vector-deleting-dtors.cpp
+++ b/clang/test/CodeGenCXX/microsoft-vector-deleting-dtors.cpp
@@ -196,6 +196,14 @@ void kernelTest() {
// CHECK: delete.end:
// CHECK-NEXT: ret void
+// The __empty_global_delete fallback is emitted at the first ::delete site,
+// which here lands before the deleting-destructor helpers. Verify it traps
+// (the fallback path is unreachable at runtime once a real forwarding body is
+// linked in).
+// X64: define linkonce_odr void @"?__empty_global_delete@@YAXPEAX_K at Z"(ptr noundef %0, i64 noundef %1)
+// X64-NEXT: call void @llvm.trap()
+// X64-NEXT: unreachable
+
// Vector dtor definition for Parrot.
// X64-LABEL: define weak dso_local noundef ptr @"??_EParrot@@UEAAPEAXI at Z"(
// X64-SAME: ptr {{.*}} %[[THIS:.*]], i32 {{.*}} %[[IMPLICIT_PARAM:.*]]) unnamed_addr
@@ -287,16 +295,6 @@ void kernelTest() {
// destructor calls __global_delete 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 noundef %0, i64 noundef %1)
-// X64-NEXT: call void @llvm.trap()
-// X64-NEXT: unreachable
-
-// Verify that when ::delete is used in the TU, a real __global_array_delete
-// forwarding body is emitted that calls through to the actual ::operator delete[].
-// X64: define linkonce_odr void @"?__global_array_delete@@YAXPEAX_K at Z"(ptr noundef %0, i64 noundef %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_array_delete.
@@ -394,7 +392,14 @@ void foobartest() {
// 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"}
+// The forwarding bodies for the global-delete wrappers are emitted at
+// end-of-module (after all the deleting-destructor helpers). Because ::delete[]
+// is used in the TU, both wrappers get real forwarding bodies: the array
+// __global_array_delete forwards to ::operator delete[] (??_V@) and the scalar
+// __global_delete forwards to ::operator delete (??3@).
+// X64: define linkonce_odr void @"?__global_array_delete@@YAXPEAX_K at Z"(ptr noundef %0, i64 noundef %1)
+// X64-NEXT: call void @"??_V at YAXPEAX_K@Z"(ptr %0, i64 %1)
+// X64: define linkonce_odr void @"?__global_delete@@YAXPEAX_K at Z"(ptr noundef %0, i64 noundef %1)
+// X64-NEXT: call void @"??3 at YAXPEAX_K@Z"(ptr %0, i64 %1)
// 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 0cb596b9a5717..b2f3a1b14d04d 100644
--- a/clang/test/CodeGenCXX/microsoft-vector-deleting-dtors2.cpp
+++ b/clang/test/CodeGenCXX/microsoft-vector-deleting-dtors2.cpp
@@ -94,12 +94,14 @@ 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_array_delete forwarding
-// body emission even without a ::delete expression in the TU.
+// 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)
+
+// The wrapper forwarding bodies are emitted at end-of-module. Verify that the
+// dllexport class triggers __global_array_delete forwarding body emission even
+// without a ::delete expression in the TU.
// X64: define linkonce_odr void @"?__global_array_delete@@YAXPEAX_K at Z"(ptr noundef %0, i64 noundef %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-global-delete-forwarding-at-delete-site.cpp b/clang/test/CodeGenCXX/msvc-global-delete-forwarding-at-delete-site.cpp
new file mode 100644
index 0000000000000..e4a254f6d2ae8
--- /dev/null
+++ b/clang/test/CodeGenCXX/msvc-global-delete-forwarding-at-delete-site.cpp
@@ -0,0 +1,34 @@
+// RUN: %clang_cc1 -emit-llvm -fms-extensions %s -triple=x86_64-pc-windows-msvc -o - | FileCheck %s
+
+// A `::delete` on a class whose deleting destructor is NOT defined in this TU
+// (only declared, e.g. defined in another TU) must still emit a strong
+// __global_delete forwarding body HERE. MSVC emits the forwarder at every
+// `::delete` site (validated against cl.exe). Without this, a TU that only
+// performs `::delete` (with the vector deleting destructor emitted in a
+// different TU) would emit no forwarder, leaving the wrapper bound to the
+// trapping __empty_global_delete fallback and crashing at runtime.
+
+struct W {
+ // Declared, but ~W() is defined in another TU, so no vector deleting
+ // destructor (and hence no __global_delete wrapper reference) is emitted in
+ // this TU. The forwarder must come from the ::delete site below.
+ virtual ~W();
+ void operator delete(void *);
+ int x;
+};
+
+void sink(W *p) { ::delete p; }
+
+// The shared trapping fallback is emitted (kept alive via llvm.used).
+// CHECK: define linkonce_odr void @"?__empty_global_delete@@YAXPEAX_K at Z"(ptr noundef %0, i64 noundef %1)
+// CHECK-NEXT: call void @llvm.trap()
+// CHECK-NEXT: unreachable
+
+// The scalar wrapper gets a real forwarding body that calls the global
+// ::operator delete (??3@), even though no deleting destructor in this TU
+// references the wrapper.
+// CHECK: define linkonce_odr void @"?__global_delete@@YAXPEAX_K at Z"(ptr noundef %0, i64 noundef %1)
+// CHECK-NEXT: call void @"??3 at YAXPEAX_K@Z"(ptr %0, i64 %1)
+
+// No vector deleting destructor for W is emitted in this TU.
+// CHECK-NOT: define {{.*}}@"??_EW@@
diff --git a/clang/test/CodeGenCXX/msvc-global-delete-scalar-array-split.cpp b/clang/test/CodeGenCXX/msvc-global-delete-scalar-array-split.cpp
index 1ff622b0465bb..b558b2e6c78d0 100644
--- a/clang/test/CodeGenCXX/msvc-global-delete-scalar-array-split.cpp
+++ b/clang/test/CodeGenCXX/msvc-global-delete-scalar-array-split.cpp
@@ -43,21 +43,22 @@ void test() {
::delete[] a;
}
+// The shared __empty_global_delete fallback is emitted with a trapping body
+// (kept alive via llvm.used) even though both wrappers have real forwarding
+// bodies here.
+// CHECK: define linkonce_odr void @"?__empty_global_delete@@YAXPEAX_K at Z"(ptr noundef %0, i64 noundef %1)
+// CHECK-NEXT: call void @llvm.trap()
+
// The scalar deleting destructor's global path calls __global_delete.
// CHECK: call void @"?__global_delete@@YAXPEAX_K at Z"(
-// It forwards to the scalar ??3@ (::operator delete).
-// CHECK: define linkonce_odr void @"?__global_delete@@YAXPEAX_K at Z"(ptr noundef %0, i64 noundef %1)
-// CHECK-NEXT: call void @"??3 at YAXPEAX_K@Z"(ptr %0, i64 %1)
-
// The vector deleting destructor's global array path calls __global_array_delete.
// CHECK: call void @"?__global_array_delete@@YAXPEAX_K at Z"(
-// It forwards to the array ??_V@ (::operator delete[]).
+// The forwarding bodies are emitted at end-of-module. Each wrapper forwards to
+// its OWN global operator delete: __global_delete to the scalar ??3@,
+// __global_array_delete to the array ??_V at .
+// CHECK: define linkonce_odr void @"?__global_delete@@YAXPEAX_K at Z"(ptr noundef %0, i64 noundef %1)
+// CHECK-NEXT: call void @"??3 at YAXPEAX_K@Z"(ptr %0, i64 %1)
// CHECK: define linkonce_odr void @"?__global_array_delete@@YAXPEAX_K at Z"(ptr noundef %0, i64 noundef %1)
// CHECK-NEXT: call void @"??_V at YAXPEAX_K@Z"(ptr %0, i64 %1)
-
-// Both wrappers share a single __empty_global_delete fallback, wired via
-// /ALTERNATENAME.
-// CHECK-DAG: !{!"/alternatename:?__global_delete@@YAXPEAX_K at Z=?__empty_global_delete@@YAXPEAX_K at Z"}
-// CHECK-DAG: !{!"/alternatename:?__global_array_delete@@YAXPEAX_K at Z=?__empty_global_delete@@YAXPEAX_K at Z"}
diff --git a/clang/test/CodeGenCXX/msvc-global-delete-scope-no-dtor.cpp b/clang/test/CodeGenCXX/msvc-global-delete-scope-no-dtor.cpp
index df0c2908267f0..09941600defa9 100644
--- a/clang/test/CodeGenCXX/msvc-global-delete-scope-no-dtor.cpp
+++ b/clang/test/CodeGenCXX/msvc-global-delete-scope-no-dtor.cpp
@@ -10,12 +10,12 @@
// __global_delete machinery when a deleting destructor is involved, i.e. for a
// `::delete` on a class type with a non-trivial destructor. A `::delete` on a
// primitive or on a trivially-destructible class is lowered as a plain direct
-// operator delete, so MSVC leaves __global_delete as an unresolved weak
-// external that falls back to the trapping __empty_global_delete via
-// /ALTERNATENAME. (The destructor's virtualness and whether the class has its
-// own operator delete are irrelevant to this trigger.) Emitting a forwarding
-// body here would add a hard reference to the global operator delete and could
-// reintroduce LNK2001 in environments without one.
+// operator delete, so MSVC leaves __global_delete as a weak external that falls
+// back to the trapping __empty_global_delete. Clang models this with a weak
+// alias to __empty_global_delete. (The destructor's virtualness and whether the
+// class has its own operator delete are irrelevant to this trigger.) Emitting a
+// forwarding body here would add a hard reference to the global operator delete
+// and could reintroduce LNK2001 in environments without one.
struct Base {
void *operator new[](__SIZE_TYPE__);
@@ -50,6 +50,12 @@ void scopeTrivial(Trivial *q) {
::delete q;
}
+// The __global_array_delete wrapper defaults to a weak alias to the trapping
+// fallback (no forwarding body of its own is emitted, asserted via the
+// --implicit-check-not on the RUN line). The alias is emitted before the
+// function definitions.
+// CHECK: @"?__global_array_delete@@YAXPEAX_K at Z" = weak alias void (ptr, i64), ptr @"?__empty_global_delete@@YAXPEAX_K at Z"
+
// The vector deleting destructor still routes its global array-delete path
// through the __global_array_delete wrapper.
// CHECK: call void @"?__global_array_delete@@YAXPEAX_K at Z"
@@ -59,8 +65,3 @@ void scopeTrivial(Trivial *q) {
// CHECK: define linkonce_odr void @"?__empty_global_delete@@YAXPEAX_K at Z"(ptr noundef %0, i64 noundef %1)
// CHECK-NEXT: call void @llvm.trap()
// CHECK-NEXT: unreachable
-
-// The /ALTERNATENAME directive wires __global_array_delete to the trapping
-// fallback; no forwarding body of its own is emitted (asserted via the
-// --implicit-check-not on the RUN line).
-// CHECK: !{!"/alternatename:?__global_array_delete@@YAXPEAX_K at Z=?__empty_global_delete@@YAXPEAX_K at Z"}
diff --git a/clang/test/CodeGenCXX/msvc-no-global-delete-forwarding.cpp b/clang/test/CodeGenCXX/msvc-no-global-delete-forwarding.cpp
index a4ac2610926cd..1703b81c7f7f7 100644
--- a/clang/test/CodeGenCXX/msvc-no-global-delete-forwarding.cpp
+++ b/clang/test/CodeGenCXX/msvc-no-global-delete-forwarding.cpp
@@ -24,6 +24,13 @@ void test() {
delete[] p;
}
+// Each wrapper defaults to a weak alias to the trapping __empty_global_delete
+// (matching MSVC's weak-external-with-default; this also works under Arm64EC,
+// unlike an /alternatename directive). These aliases are emitted before the
+// function definitions.
+// CHECK-DAG: @"?__global_delete@@YAXPEAX_K at Z" = weak alias void (ptr, i64), ptr @"?__empty_global_delete@@YAXPEAX_K at Z"
+// CHECK-DAG: @"?__global_array_delete@@YAXPEAX_K at Z" = weak alias void (ptr, i64), ptr @"?__empty_global_delete@@YAXPEAX_K at Z"
+
// The VDD dispatches between class and global delete: the array path uses the
// __global_array_delete wrapper, the scalar path uses __global_delete.
// CHECK-LABEL: define weak dso_local noundef ptr @"??_EDerived@@UEAAPEAXI at Z"
@@ -43,6 +50,3 @@ void test() {
// type in this TU, and no dllexport class).
// CHECK-NOT: define {{.*}}void @"?__global_delete@@YAXPEAX_K at Z"
// CHECK-NOT: define {{.*}}void @"?__global_array_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"}
>From e3ee830d98408efc8d7ab08ceb835ffa275ea599 Mon Sep 17 00:00:00 2001
From: Daniel Paoliello <danpao at microsoft.com>
Date: Thu, 16 Jul 2026 10:50:43 -0700
Subject: [PATCH 2/2] Address PR feedback
---
clang/lib/CodeGen/CGExprCXX.cpp | 8 +++++---
1 file changed, 5 insertions(+), 3 deletions(-)
diff --git a/clang/lib/CodeGen/CGExprCXX.cpp b/clang/lib/CodeGen/CGExprCXX.cpp
index 4725403b42932..1769ab00ed56d 100644
--- a/clang/lib/CodeGen/CGExprCXX.cpp
+++ b/clang/lib/CodeGen/CGExprCXX.cpp
@@ -2116,9 +2116,11 @@ void CodeGenFunction::EmitCXXDeleteExpr(const CXXDeleteExpr *E) {
// TU that only does ::delete (with the deleting destructor defined in
// another TU) would emit no forwarder, leaving the wrapper bound to the
// trapping empty fallback and crashing at runtime.
- if (const FunctionDecl *OD = E->getOperatorDelete();
- OD && !isa<CXXMethodDecl>(OD))
- CGM.getOrCreateMSVCGlobalDeleteWrapper(OD);
+ const FunctionDecl *OD = E->getOperatorDelete();
+ assert(!isa<CXXMethodDecl>(OD) &&
+ "global ::delete should resolve to a namespace-scope "
+ "operator delete");
+ CGM.getOrCreateMSVCGlobalDeleteWrapper(OD);
}
}
More information about the cfe-commits
mailing list