[llvm-branch-commits] [clang] release/23.x: [clang][win] Fix __global_delete wrappers for Arm64EC and cross-TU ::delete (PR #210144)

Douglas Yung via llvm-branch-commits llvm-branch-commits at lists.llvm.org
Wed Jul 22 06:45:58 PDT 2026


https://github.com/dyung updated https://github.com/llvm/llvm-project/pull/210144

>From 71ee1456b23270f768571db9a4b060fc4f4b8569 Mon Sep 17 00:00:00 2001
From: Daniel Paoliello <danpao at microsoft.com>
Date: Thu, 16 Jul 2026 12:01:50 -0700
Subject: [PATCH] [clang][win] Fix __global_delete wrappers for Arm64EC and
 cross-TU ::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.
---
 clang/lib/CodeGen/CGClass.cpp                 | 113 +-------------
 clang/lib/CodeGen/CGExprCXX.cpp               |  14 +-
 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, 241 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..1769ab00ed56d 100644
--- a/clang/lib/CodeGen/CGExprCXX.cpp
+++ b/clang/lib/CodeGen/CGExprCXX.cpp
@@ -2108,8 +2108,20 @@ 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.
+      const FunctionDecl *OD = E->getOperatorDelete();
+      assert(!isa<CXXMethodDecl>(OD) &&
+             "global ::delete should resolve to a namespace-scope "
+             "operator delete");
+      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"}



More information about the llvm-branch-commits mailing list