Skip to content

[clang][win] MSVC-compat: Use __global_delete wrapper in deleting destructors instead of directly referencing ::operator delete - #188372

Merged
dpaoliello merged 6 commits into
llvm:mainfrom
dpaoliello:globalopdel
Jul 10, 2026
Merged

[clang][win] MSVC-compat: Use __global_delete wrapper in deleting destructors instead of directly referencing ::operator delete#188372
dpaoliello merged 6 commits into
llvm:mainfrom
dpaoliello:globalopdel

Conversation

@dpaoliello

@dpaoliello dpaoliello commented Mar 24, 2026

Copy link
Copy Markdown
Contributor

When Clang emits scalar/vector deleting destructors for classes with a class-level operator delete, it generates a conditional dispatch that can call either the class-level or global ::operator delete. The global path directly referenced ::operator delete, causing LNK2001 linker errors in environments where no global ::operator delete exists.

MSVC handles this by calling __global_delete (and __global_array_delete for vector deletes) - this is a compiler generated function that is ONLY defined if there is a direct call to global ::operator delete for type with non-trivial destructors. Additionally, it always emits an empty __empty_global_delete and uses /ALTERNATIVENAME linker arg to default __global_delete (and __global_array_delete) to __empty_global_delete if there is NEVER an delete operator call that would triffer the body to be emitted (thus the empty function should never be called).

This change aligns Clang's behavior with MSVC when MSVC compatibility mode and non-LLVM 21 ABI is used, with one difference: the LLVM generated __empty_global_delete traps since it should never be called.

@llvmbot llvmbot added clang Clang issues not falling into any other category clang:modules C++20 modules and Clang Header Modules clang:codegen IR generation bugs: mangling, exceptions, etc. labels Mar 24, 2026
@llvmbot

llvmbot commented Mar 24, 2026

Copy link
Copy Markdown
Member

@llvm/pr-subscribers-clang

@llvm/pr-subscribers-clang-modules

Author: Daniel Paoliello (dpaoliello)

Changes

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.


Patch is 22.50 KiB, truncated to 20.00 KiB below, full version: https://github.com/llvm/llvm-project/pull/188372.diff

13 Files Affected:

  • (modified) clang/docs/ReleaseNotes.rst (+5)
  • (modified) clang/lib/CodeGen/CGClass.cpp (+85-4)
  • (modified) clang/lib/CodeGen/CGExprCXX.cpp (+8-4)
  • (modified) clang/lib/CodeGen/CodeGenFunction.h (+2-1)
  • (modified) clang/test/CodeGenCXX/cxx2a-destroying-delete.cpp (+4-4)
  • (modified) clang/test/CodeGenCXX/microsoft-abi-structors.cpp (+1-1)
  • (modified) clang/test/CodeGenCXX/microsoft-vector-deleting-dtors.cpp (+46-2)
  • (modified) clang/test/CodeGenCXX/microsoft-vector-deleting-dtors2.cpp (+2-2)
  • (modified) clang/test/CodeGenCXX/msvc-vector-deleting-dtors-sized-delete.cpp (+2-2)
  • (modified) clang/test/Modules/glob-delete-with-virtual-dtor.cpp (+2-2)
  • (modified) clang/test/Modules/msvc-vector-deleting-destructors.cpp (+4-4)
  • (modified) clang/test/PCH/glob-delete-with-virtual-dtor.cpp (+2-2)
  • (modified) clang/test/PCH/msvc-vector-deleting-destructors.cpp (+4-4)
diff --git a/clang/docs/ReleaseNotes.rst b/clang/docs/ReleaseNotes.rst
index 0dbe667e4f07a..90ae251ad85a2 100644
--- a/clang/docs/ReleaseNotes.rst
+++ b/clang/docs/ReleaseNotes.rst
@@ -452,6 +452,11 @@ Windows Support
 - Clang now defines the ``_MSVC_TRADITIONAL`` macro as ``1`` when emulating MSVC
   19.15 (Visual Studio 2017 version 15.8) and later. (#GH47114)
 
+- In MSVC compatibility mode, scalar and vector deleting destructors now call
+  ``__global_delete`` (a weak external) instead of directly referencing
+  ``::operator delete``. This matches MSVC's behavior and fixes ``LNK2001``
+  linker errors in environments where no global ``::operator delete`` exists.
+
 LoongArch Support
 ^^^^^^^^^^^^^^^^^
 
diff --git a/clang/lib/CodeGen/CGClass.cpp b/clang/lib/CodeGen/CGClass.cpp
index 6572009fc6003..b3a0d22f403a2 100644
--- a/clang/lib/CodeGen/CGClass.cpp
+++ b/clang/lib/CodeGen/CGClass.cpp
@@ -29,6 +29,7 @@
 #include "llvm/IR/Intrinsics.h"
 #include "llvm/IR/Metadata.h"
 #include "llvm/Support/SaveAndRestore.h"
+#include "llvm/Transforms/Utils/ModuleUtils.h"
 #include "llvm/Transforms/Utils/SanitizerStats.h"
 #include <optional>
 
@@ -1435,6 +1436,72 @@ static bool CanSkipVTablePointerInitialization(CodeGenFunction &CGF,
   return true;
 }
 
+/// Get or create the MSVC-compatible __global_delete wrapper function.
+///
+/// MSVC's scalar/vector deleting destructors call __global_delete (a weak
+/// external) instead of calling ::operator delete directly. This allows
+/// environments without a global ::operator delete (e.g., kernel mode) to
+/// gracefully fall back to a no-op __empty_global_delete.
+static llvm::Constant *
+getOrCreateMSVCGlobalDeleteWrapper(CodeGenModule &CGM,
+                                   const FunctionDecl *GlobOD) {
+  llvm::Module &M = CGM.getModule();
+  llvm::LLVMContext &LLVMCtx = M.getContext();
+
+  llvm::Constant *GlobDeleteCallee = CGM.GetAddrOfFunction(GlobOD);
+  auto *GlobDeleteFn = cast<llvm::Function>(GlobDeleteCallee);
+  llvm::FunctionType *FnTy = GlobDeleteFn->getFunctionType();
+
+  // Derive __global_delete and __empty_global_delete mangled names.
+  // Global ::operator delete mangling:   ??3@<signature>
+  // Global ::operator delete[] mangling: ??_V@<signature>
+  // We construct:
+  //   ?__global_delete@@<signature>
+  //   ?__empty_global_delete@@<signature>
+  StringRef GlobDeleteMangledName = GlobDeleteFn->getName();
+  StringRef Signature;
+  if (GlobDeleteMangledName.starts_with("??3@"))
+    Signature = GlobDeleteMangledName.substr(4);
+  else if (GlobDeleteMangledName.starts_with("??_V@"))
+    Signature = GlobDeleteMangledName.substr(5);
+  else
+    llvm_unreachable("unexpected global operator delete mangling");
+
+  std::string GlobalDeleteName =
+      ("?__global_delete@@" + Signature).str();
+  std::string EmptyGlobalDeleteName =
+      ("?__empty_global_delete@@" + Signature).str();
+
+  // Only set up the wrapper once per module.
+  if (llvm::Function *Existing = M.getFunction(GlobalDeleteName))
+    return Existing;
+
+  // Create __empty_global_delete fallback.
+  llvm::Function *EmptyFn = llvm::Function::Create(
+      FnTy, llvm::GlobalValue::LinkOnceODRLinkage,
+      EmptyGlobalDeleteName, &M);
+  EmptyFn->setComdat(M.getOrInsertComdat(EmptyGlobalDeleteName));
+  EmptyFn->setUnnamedAddr(llvm::GlobalValue::UnnamedAddr::Global);
+  auto *BB = llvm::BasicBlock::Create(LLVMCtx, "", EmptyFn);
+  llvm::ReturnInst::Create(LLVMCtx, BB);
+
+  // Emit /ALTERNATENAME linker directive: if __global_delete isn't provided
+  // (e.g., by the CRT), fall back to the no-op __empty_global_delete.
+  std::string AltOption = "/alternatename:" + GlobalDeleteName + "=" +
+                          EmptyGlobalDeleteName;
+  auto *AltMD = llvm::MDNode::get(
+      LLVMCtx, {llvm::MDString::get(LLVMCtx, AltOption)});
+  M.getOrInsertNamedMetadata("llvm.linker.options")->addOperand(AltMD);
+
+  // Nothing directly uses this function other than the /alternatename
+  // directive, so explicitly mark it as used.
+  appendToUsed(M, {EmptyFn});
+
+  // Return the __global_delete wrapper function to call.
+  auto GlobalDeleteCallee = M.getOrInsertFunction(GlobalDeleteName, FnTy);
+  return cast<llvm::Function>(GlobalDeleteCallee.getCallee());
+}
+
 static void EmitConditionalArrayDtorCall(const CXXDestructorDecl *DD,
                                          CodeGenFunction &CGF,
                                          llvm::Value *ShouldDeleteCondition) {
@@ -1518,9 +1585,14 @@ static void EmitConditionalArrayDtorCall(const CXXDestructorDecl *DD,
       CGF.EmitBranchThroughCleanup(CGF.ReturnBlock);
 
       CGF.EmitBlock(GlobDelete);
+      // Use __global_delete wrapper for the global array delete path,
+      // matching MSVC's weak external mechanism.
+      llvm::Constant *GlobalDeleteWrapper =
+          getOrCreateMSVCGlobalDeleteWrapper(
+              CGF.CGM, Dtor->getGlobalArrayOperatorDelete());
       CGF.EmitDeleteCall(Dtor->getGlobalArrayOperatorDelete(), allocatedPtr,
                          CGF.getContext().getCanonicalTagType(ClassDecl),
-                         numElements, cookieSize);
+                         numElements, cookieSize, GlobalDeleteWrapper);
     }
   } else {
     // No operators delete[] were found, so emit a trap.
@@ -1747,9 +1819,12 @@ void EmitConditionalDtorDeleteCall(CodeGenFunction &CGF,
   CGF.Builder.CreateCondBr(ShouldCallDelete, continueBB, callDeleteBB);
 
   CGF.EmitBlock(callDeleteBB);
-  auto EmitDeleteAndGoToEnd = [&](const FunctionDecl *DeleteOp) {
+  auto EmitDeleteAndGoToEnd = [&](const FunctionDecl *DeleteOp,
+                                  llvm::Constant *CalleeOverride = nullptr) {
     CGF.EmitDeleteCall(DeleteOp, LoadThisForDtorDelete(CGF, Dtor),
-                       Context.getCanonicalTagType(ClassDecl));
+                       Context.getCanonicalTagType(ClassDecl),
+                       /*NumElements=*/nullptr, /*CookieSize=*/CharUnits(),
+                       CalleeOverride);
     if (ReturnAfterDelete)
       CGF.EmitBranchThroughCleanup(CGF.ReturnBlock);
     else
@@ -1773,7 +1848,13 @@ void EmitConditionalDtorDeleteCall(CodeGenFunction &CGF,
     CGF.Builder.CreateCondBr(ShouldCallGlobDelete, ClassDelete, GlobDelete);
     CGF.EmitBlock(GlobDelete);
 
-    EmitDeleteAndGoToEnd(GlobOD);
+    // Use __global_delete wrapper instead of directly calling
+    // ::operator delete. This matches MSVC's behavior: __global_delete is a
+    // weak external that falls back to __empty_global_delete (a no-op) when
+    // the CRT doesn't provide it (e.g., kernel-mode environments).
+    llvm::Constant *GlobalDeleteWrapper =
+        getOrCreateMSVCGlobalDeleteWrapper(CGF.CGM, GlobOD);
+    EmitDeleteAndGoToEnd(GlobOD, GlobalDeleteWrapper);
     CGF.EmitBlock(ClassDelete);
   }
   EmitDeleteAndGoToEnd(OD);
diff --git a/clang/lib/CodeGen/CGExprCXX.cpp b/clang/lib/CodeGen/CGExprCXX.cpp
index 82300c3ede183..9e61097b40c71 100644
--- a/clang/lib/CodeGen/CGExprCXX.cpp
+++ b/clang/lib/CodeGen/CGExprCXX.cpp
@@ -1337,9 +1337,12 @@ static void EmitNewInitializer(CodeGenFunction &CGF, const CXXNewExpr *E,
 static RValue EmitNewDeleteCall(CodeGenFunction &CGF,
                                 const FunctionDecl *CalleeDecl,
                                 const FunctionProtoType *CalleeType,
-                                const CallArgList &Args) {
+                                const CallArgList &Args,
+                                llvm::Constant *CalleeOverride = nullptr) {
   llvm::CallBase *CallOrInvoke;
-  llvm::Constant *CalleePtr = CGF.CGM.GetAddrOfFunction(CalleeDecl);
+  llvm::Constant *CalleePtr = CalleeOverride
+      ? CalleeOverride
+      : CGF.CGM.GetAddrOfFunction(CalleeDecl);
   CGCallee Callee = CGCallee::forDirect(CalleePtr, GlobalDecl(CalleeDecl));
   RValue RV = CGF.EmitCall(CGF.CGM.getTypes().arrangeFreeFunctionCall(
                                Args, CalleeType, /*ChainCall=*/false),
@@ -1788,7 +1791,8 @@ llvm::Value *CodeGenFunction::EmitCXXNewExpr(const CXXNewExpr *E) {
 void CodeGenFunction::EmitDeleteCall(const FunctionDecl *DeleteFD,
                                      llvm::Value *DeletePtr, QualType DeleteTy,
                                      llvm::Value *NumElements,
-                                     CharUnits CookieSize) {
+                                     CharUnits CookieSize,
+                                     llvm::Constant *CalleeOverride) {
   assert((!NumElements && CookieSize.isZero()) ||
          DeleteFD->getOverloadedOperator() == OO_Array_Delete);
 
@@ -1856,7 +1860,7 @@ void CodeGenFunction::EmitDeleteCall(const FunctionDecl *DeleteFD,
          "unknown parameter to usual delete function");
 
   // Emit the call to delete.
-  EmitNewDeleteCall(*this, DeleteFD, DeleteFTy, DeleteArgs);
+  EmitNewDeleteCall(*this, DeleteFD, DeleteFTy, DeleteArgs, CalleeOverride);
 
   // If call argument lowering didn't use a generated tag argument alloca we
   // remove them
diff --git a/clang/lib/CodeGen/CodeGenFunction.h b/clang/lib/CodeGen/CodeGenFunction.h
index 0ff93d2ce7363..5ecb64885b40a 100644
--- a/clang/lib/CodeGen/CodeGenFunction.h
+++ b/clang/lib/CodeGen/CodeGenFunction.h
@@ -3291,7 +3291,8 @@ class CodeGenFunction : public CodeGenTypeCache {
 
   void EmitDeleteCall(const FunctionDecl *DeleteFD, llvm::Value *Ptr,
                       QualType DeleteTy, llvm::Value *NumElements = nullptr,
-                      CharUnits CookieSize = CharUnits());
+                      CharUnits CookieSize = CharUnits(),
+                      llvm::Constant *CalleeOverride = nullptr);
 
   RValue EmitBuiltinNewDeleteCall(const FunctionProtoType *Type,
                                   const CallExpr *TheCallExpr, bool IsDelete);
diff --git a/clang/test/CodeGenCXX/cxx2a-destroying-delete.cpp b/clang/test/CodeGenCXX/cxx2a-destroying-delete.cpp
index c83cb32251462..97ed0c57e8f68 100644
--- a/clang/test/CodeGenCXX/cxx2a-destroying-delete.cpp
+++ b/clang/test/CodeGenCXX/cxx2a-destroying-delete.cpp
@@ -229,8 +229,8 @@ H::~H() { call_in_dtor(); }
 // CLANG22-MSABI-NEXT: br i1 %[[CHCK2]], label %dtor.call_class_delete, label %dtor.call_glob_delete
 //
 // CLANG22-MSABI-LABEL: dtor.call_glob_delete:
-// CLANG22-MSABI64: call void @"??3@YAXPEAX_K@Z"(ptr noundef %{{.*}}, i64 noundef 48)
-// CLANG22-MSABI32: call void @"??3@YAXPAXIW4align_val_t@std@@@Z"(ptr noundef %{{.*}}, i32 noundef 32, i32 noundef 16)
+// CLANG22-MSABI64: call void @"?__global_delete@@YAXPEAX_K@Z"(ptr noundef %{{.*}}, i64 noundef 48)
+// CLANG22-MSABI32: call void @"?__global_delete@@YAXPAXIW4align_val_t@std@@@Z"(ptr noundef %{{.*}}, i32 noundef 32, i32 noundef 16)
 // CLANG22-MSABI-NEXT: br label %[[RETURN:.*]]
 //
 // CLANG21-MSABI: dtor.call_delete:
@@ -284,8 +284,8 @@ I::~I() { call_in_dtor(); }
 // CLANG22-MSABI-NEXT: br i1 %[[CHCK2]], label %dtor.call_class_delete, label %dtor.call_glob_delete
 //
 // CLANG22-MSABI: dtor.call_glob_delete:
-// CLANG22-MSABI64: call void @"??3@YAXPEAX_KW4align_val_t@std@@@Z"(ptr noundef %{{.*}}, i64 noundef 96, i64 noundef 32)
-// CLANG22-MSABI32: call void @"??3@YAXPAXIW4align_val_t@std@@@Z"(ptr noundef %{{.*}}, i32 noundef 64, i32 noundef 32)
+// CLANG22-MSABI64: call void @"?__global_delete@@YAXPEAX_KW4align_val_t@std@@@Z"(ptr noundef %{{.*}}, i64 noundef 96, i64 noundef 32)
+// CLANG22-MSABI32: call void @"?__global_delete@@YAXPAXIW4align_val_t@std@@@Z"(ptr noundef %{{.*}}, i32 noundef 64, i32 noundef 32)
 // CLANG22-MSABI-NEXT: br label %[[RETURN:.*]]
 //
 // CLANG21-MSABI: dtor.call_delete:
diff --git a/clang/test/CodeGenCXX/microsoft-abi-structors.cpp b/clang/test/CodeGenCXX/microsoft-abi-structors.cpp
index 670988fc1ada2..1a4a291e28c0a 100644
--- a/clang/test/CodeGenCXX/microsoft-abi-structors.cpp
+++ b/clang/test/CodeGenCXX/microsoft-abi-structors.cpp
@@ -487,7 +487,7 @@ void checkH() {
 // DTORS-NEXT:   br i1 %[[CONDITION1]], label %[[CALL_CLASS_DELETE:[0-9a-z._]+]], label %[[CALL_GLOB_DELETE:[0-9a-z._]+]]
 //
 // DTORS:      [[CALL_GLOB_DELETE]]
-// DTORS-NEXT:   call void @"??3@YAXPAX@Z"(ptr %[[THIS]])
+// DTORS-NEXT:   call void @"?__global_delete@@YAXPAX@Z"(ptr %[[THIS]])
 // DTORS-NEXT:   br label %[[CONTINUE_LABEL]]
 //
 // DTORS:      [[CALL_CLASS_DELETE]]
diff --git a/clang/test/CodeGenCXX/microsoft-vector-deleting-dtors.cpp b/clang/test/CodeGenCXX/microsoft-vector-deleting-dtors.cpp
index 459c1b6593fa1..766d921fde276 100644
--- a/clang/test/CodeGenCXX/microsoft-vector-deleting-dtors.cpp
+++ b/clang/test/CodeGenCXX/microsoft-vector-deleting-dtors.cpp
@@ -42,6 +42,17 @@ struct AllocatedAsArray : public Bird {
 
 };
 
+struct KernelBase {
+  static void* operator new(__SIZE_TYPE__ n, int tag = 0);
+  static void operator delete(void* p);
+  static void operator delete[](void* p);
+  virtual ~KernelBase();
+};
+
+struct KernelDerived : KernelBase {
+  virtual ~KernelDerived();
+};
+
 // Vector deleting dtor for Bird is an alias because no new Bird[] expressions
 // in the TU.
 // X64: @"??_EBird@@UEAAPEAXI@Z" = weak dso_local unnamed_addr alias ptr (ptr, i32), ptr @"??_GBird@@UEAAPEAXI@Z"
@@ -83,6 +94,14 @@ void bar() {
   sp.foo();
 }
 
+KernelBase::~KernelBase() {}
+KernelDerived::~KernelDerived() {}
+
+void kernelTest() {
+  KernelBase *p = new KernelDerived[2];
+  delete[] p;
+}
+
 // CHECK-LABEL: define dso_local void @{{.*}}dealloc{{.*}}(
 // CHECK-SAME: ptr noundef %[[PTR:.*]])
 // CHECK: entry:
@@ -260,10 +279,30 @@ void bar() {
 // X86-NEXT: %[[ARRSZ:.*]] = mul i32 4, %[[COOKIE:.*]]
 // X64-NEXT: %[[TOTALSZ:.*]] = add i64 %[[ARRSZ]], 8
 // X86-NEXT: %[[TOTALSZ:.*]] = add i32 %[[ARRSZ]], 4
-// X64-NEXT: call void @"??_V@YAXPEAX_K@Z"(ptr noundef %2, i64 noundef %[[TOTALSZ]])
-// X86-NEXT: call void @"??_V@YAXPAXI@Z"(ptr noundef %2, i32 noundef %[[TOTALSZ]])
+// X64-NEXT: call void @"?__global_delete@@YAXPEAX_K@Z"(ptr noundef %2, i64 noundef %[[TOTALSZ]])
+// X86-NEXT: call void @"?__global_delete@@YAXPAXI@Z"(ptr noundef %2, i32 noundef %[[TOTALSZ]])
 // CHECK-NEXT:   br label %dtor.continue
 
+// Test that when a class provides its own operator delete, the deleting
+// destructor calls __global_delete (a weak external with no-op fallback)
+// instead of directly referencing ::operator delete. This is critical for
+// environments like kernel mode where no global ::operator delete exists.
+// Verify __empty_global_delete is emitted as a no-op fallback.
+// X64: define linkonce_odr void @"?__empty_global_delete@@YAXPEAX_K@Z"(ptr %0, i64 %1)
+// X64-NEXT: ret void
+// X64-LABEL: define weak dso_local noundef ptr @"??_EKernelDerived@@UEAAPEAXI@Z"
+// Verify the array delete path in the VDD uses __global_delete.
+// X64: dtor.call_glob_delete_after_array_destroy:
+// X64: call void @"?__global_delete@@YAXPEAX_K@Z"(ptr noundef %{{.*}}, i64 noundef %{{.*}})
+// Verify the scalar deleting dtor uses __global_delete, not ::operator delete.
+// X64: dtor.call_delete:
+// X64-NEXT:  %[[FLAGCHECK:.*]] = and i32 %should_call_delete2, 4
+// X64-NEXT:  %[[ISGLOB:.*]] = icmp eq i32 %[[FLAGCHECK]], 0
+// X64-NEXT:  br i1 %[[ISGLOB]], label %dtor.call_class_delete, label %dtor.call_glob_delete
+// X64: dtor.call_glob_delete:
+// X64-NEXT:  call void @"?__global_delete@@YAXPEAX_K@Z"(ptr noundef %{{.*}}, i64 noundef 8)
+// X64: dtor.call_class_delete:
+// X64-NEXT:  call void @"??3KernelBase@@SAXPEAX@Z"(ptr noundef %{{.*}})
 
 
 struct BaseDelete1 {
@@ -346,3 +385,8 @@ void foobartest() {
 // X64: define weak dso_local noundef ptr @"??_EAllocatedAsArray@@UEAAPEAXI@Z"
 // X86: define weak dso_local x86_thiscallcc noundef ptr @"??_EAllocatedAsArray@@UAEPAXI@Z"
 // CLANG21: define linkonce_odr dso_local noundef ptr @"??_GAllocatedAsArray@@UEAAPEAXI@Z"
+
+// Verify the /ALTERNATENAME linker directive.
+// X64: !{!"/alternatename:?__global_delete@@YAXPEAX_K@Z=?__empty_global_delete@@YAXPEAX_K@Z"}
+
+// CLANG21-NOT: __global_delete
diff --git a/clang/test/CodeGenCXX/microsoft-vector-deleting-dtors2.cpp b/clang/test/CodeGenCXX/microsoft-vector-deleting-dtors2.cpp
index 6412bf5b1dc3b..b1ea0381b85a8 100644
--- a/clang/test/CodeGenCXX/microsoft-vector-deleting-dtors2.cpp
+++ b/clang/test/CodeGenCXX/microsoft-vector-deleting-dtors2.cpp
@@ -59,7 +59,7 @@ void TesttheTest() {
 // X64: define weak dso_local noundef ptr @"??_EDrawingBuffer@@UEAAPEAXI@Z"
 // X64: call void @"??1DrawingBuffer@@UEAA@XZ"(ptr noundef nonnull align 8 dereferenceable(8) %arraydestroy.element)
 // X64: call void @"??_V?$RefCounted@UDrawingBuffer@@@@SAXPEAX@Z"(ptr noundef %2)
-// X64: call void @"??_V@YAXPEAX_K@Z"(ptr noundef %2, i64 noundef %{{.*}})
+// X64: call void @"?__global_delete@@YAXPEAX_K@Z"(ptr noundef %2, i64 noundef %{{.*}})
 // X64: call void @"??1DrawingBuffer@@UEAA@XZ"(ptr noundef nonnull align 8 dereferenceable(8) %this1)
 // X64: call void @"??3@YAXPEAX_K@Z"(ptr noundef %this1, i64 noundef {{.*}})
 
@@ -70,7 +70,7 @@ void TesttheTest() {
 // X86: define weak dso_local x86_thiscallcc noundef ptr @"??_EDrawingBuffer@@UAEPAXI@Z"
 // X86: call x86_thiscallcc void @"??1DrawingBuffer@@UAE@XZ"(ptr noundef nonnull align 4 dereferenceable(4) %arraydestroy.element)
 // X86: call void @"??_V?$RefCounted@UDrawingBuffer@@@@SAXPAX@Z"(ptr noundef %2)
-// X86: call void @"??_V@YAXPAXI@Z"(ptr noundef %2, i32 noundef {{.*}})
+// X86: call void @"?__global_delete@@YAXPAXI@Z"(ptr noundef %2, i32 noundef {{.*}})
 // X86  call x86_thiscallcc void @"??1DrawingBuffer@@UAE@XZ"(ptr noundef nonnull align 4 dereferenceable(4) %this1)
 // X86: call void @"??3@YAXPAXI@Z"(ptr noundef %this1, i32 noundef {{.*}})
 
diff --git a/clang/test/CodeGenCXX/msvc-vector-deleting-dtors-sized-delete.cpp b/clang/test/CodeGenCXX/msvc-vector-deleting-dtors-sized-delete.cpp
index 6c9faa88e08e9..db8c429956b6f 100644
--- a/clang/test/CodeGenCXX/msvc-vector-deleting-dtors-sized-delete.cpp
+++ b/clang/test/CodeGenCXX/msvc-vector-deleting-dtors-sized-delete.cpp
@@ -50,5 +50,5 @@ void test() {
 // X86-NEXT:  %[[ARRSZ1:.*]] = mul i32 12, %[[HOWMANY]]
 // X64-NEXT:  %[[TOTALSZ1:.*]] = add i64 %[[ARRSZ1]], 8
 // X86-NEXT:  %[[TOTALSZ1:.*]] = add i32 %[[ARRSZ1]], 4
-// X64-NEXT:   call void @"??_V@YAXPEAX_K@Z"(ptr noundef %2, i64 noundef %[[TOTALSZ1]])
-// X86-NEXT:   call void @"??_V@YAXPAXI@Z"(ptr noundef %2, i32 noundef %[[TOTALSZ1]])
+// X64-NEXT:   call void @"?__global_delete@@YAXPEAX_K@Z"(ptr noundef %2, i64 noundef %[[TOTALSZ1]])
+// X86-NEXT:   call void @"?__global_delete@@YAXPAXI@Z"(ptr noundef %2, i32 noundef %[[TOTALSZ1]])
diff --git a/clang/test/Modules/glob-delete-with-virtual-dtor.cpp b/clang/test/Modules/glob-delete-with-virtual-dtor.cpp
index fb2e2a4decf60..18e90aaca78f0 100644
--- a/clang/test/Modules/glob-delete-with-virtual-dtor.cpp
+++ b/clang/test/Modules/glob-delete-with-virtual-dtor.cpp
@@ -30,8 +30,8 @@ void out_of_module_tests() {
 // CHECK-NEXT:   br i1 %[[CONDITION1]], label %[[CALL_CLASS_DELETE:[0-9a-z._]+]], label %[[CALL_GLOB_DELETE:[0-9a-z._]+]]
 //
 // CHECK:      [[CALL_GLOB_DELETE]]
-// CHECK32-NEXT:   call void @"??3@YAXPAXI@Z"
-// CHECK64-NEXT:   call void @"??3@YAXPEAX_K@Z"
+// CHECK32-NEXT:   call void @"?__global_delete@@YAXPAXI@Z"
+// CHECK64-NEXT:   call void @"?__global_delete@@YAXPEAX_K@Z"
 // CHECK-NEXT:   br label %[[CONTINUE_LABEL]]
 //
 // CHECK:      [[CALL_CLASS_DELETE]]
diff --git a/clang/test/Modules/msvc-vector-deleting-destructors.cpp b/clang/test/Modules/msvc-vector-deleting-destructors.cpp
index 68faa687251d7..9e99ae1e191b7 100644
--- a/clang/test/Modules/msvc-vector-deleting-destructors.cpp
+++ b/clang/test/Modules/msvc-vector-deleting-destructors.cpp
@@ -24,11 +24,11 @@ void out_of_module_tests(Derived *p, Derived *p1) {
 // CHECK32-NEXT: %[[ARRSZ:.*]] = mul i32 8, %[[COOKIE:.*]]
 // CHECK64-NEXT: %[[TOTALSZ:.*]] = add i64 %[[ARRSZ]], 8
 // CHECK32-NEXT: %[[TOTALSZ:.*]] = add i32 %[[ARRSZ]], 4
-// CHECK32-NEXT:   call void @"??_V@YAXPAXI@Z"(ptr noundef %2, i32 noundef %[[TOTALSZ]])
-// CHECK64-NEXT:   call void @"??_V@YAXPEAX_K@Z"(ptr noundef %2, i64 noundef %[[TOTALSZ]])
+// CHECK32-NEXT:   call void @"?__global_delete@@YAXPAXI@Z"(ptr noundef %2, i32 noundef %[[TOTALSZ]])
+// CHECK64-NEXT:   call void @"?__global_delete@@YAXPEAX_K@Z"(ptr noundef %2, i64 noundef %[[TOTALSZ]])
 // CHECK: dtor.call_glob_delete:
-// CHECK32-NEXT:   call void @"??3@YAXPAXI@Z"(ptr noundef %this1, i32 noundef 8)
-// CHECK64-NEXT:   call void @"??3@YAXPEAX_K@Z"(ptr noundef %this1, i64 noundef 16)
+// CHECK32-NEXT:   call void @"?__global_delete@@YAXPAXI@Z"(ptr noundef %this1, i3...
[truncated]

@llvmbot

llvmbot commented Mar 24, 2026

Copy link
Copy Markdown
Member

@llvm/pr-subscribers-clang-codegen

Author: Daniel Paoliello (dpaoliello)

Changes

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.


Patch is 22.50 KiB, truncated to 20.00 KiB below, full version: https://github.com/llvm/llvm-project/pull/188372.diff

13 Files Affected:

  • (modified) clang/docs/ReleaseNotes.rst (+5)
  • (modified) clang/lib/CodeGen/CGClass.cpp (+85-4)
  • (modified) clang/lib/CodeGen/CGExprCXX.cpp (+8-4)
  • (modified) clang/lib/CodeGen/CodeGenFunction.h (+2-1)
  • (modified) clang/test/CodeGenCXX/cxx2a-destroying-delete.cpp (+4-4)
  • (modified) clang/test/CodeGenCXX/microsoft-abi-structors.cpp (+1-1)
  • (modified) clang/test/CodeGenCXX/microsoft-vector-deleting-dtors.cpp (+46-2)
  • (modified) clang/test/CodeGenCXX/microsoft-vector-deleting-dtors2.cpp (+2-2)
  • (modified) clang/test/CodeGenCXX/msvc-vector-deleting-dtors-sized-delete.cpp (+2-2)
  • (modified) clang/test/Modules/glob-delete-with-virtual-dtor.cpp (+2-2)
  • (modified) clang/test/Modules/msvc-vector-deleting-destructors.cpp (+4-4)
  • (modified) clang/test/PCH/glob-delete-with-virtual-dtor.cpp (+2-2)
  • (modified) clang/test/PCH/msvc-vector-deleting-destructors.cpp (+4-4)
diff --git a/clang/docs/ReleaseNotes.rst b/clang/docs/ReleaseNotes.rst
index 0dbe667e4f07a..90ae251ad85a2 100644
--- a/clang/docs/ReleaseNotes.rst
+++ b/clang/docs/ReleaseNotes.rst
@@ -452,6 +452,11 @@ Windows Support
 - Clang now defines the ``_MSVC_TRADITIONAL`` macro as ``1`` when emulating MSVC
   19.15 (Visual Studio 2017 version 15.8) and later. (#GH47114)
 
+- In MSVC compatibility mode, scalar and vector deleting destructors now call
+  ``__global_delete`` (a weak external) instead of directly referencing
+  ``::operator delete``. This matches MSVC's behavior and fixes ``LNK2001``
+  linker errors in environments where no global ``::operator delete`` exists.
+
 LoongArch Support
 ^^^^^^^^^^^^^^^^^
 
diff --git a/clang/lib/CodeGen/CGClass.cpp b/clang/lib/CodeGen/CGClass.cpp
index 6572009fc6003..b3a0d22f403a2 100644
--- a/clang/lib/CodeGen/CGClass.cpp
+++ b/clang/lib/CodeGen/CGClass.cpp
@@ -29,6 +29,7 @@
 #include "llvm/IR/Intrinsics.h"
 #include "llvm/IR/Metadata.h"
 #include "llvm/Support/SaveAndRestore.h"
+#include "llvm/Transforms/Utils/ModuleUtils.h"
 #include "llvm/Transforms/Utils/SanitizerStats.h"
 #include <optional>
 
@@ -1435,6 +1436,72 @@ static bool CanSkipVTablePointerInitialization(CodeGenFunction &CGF,
   return true;
 }
 
+/// Get or create the MSVC-compatible __global_delete wrapper function.
+///
+/// MSVC's scalar/vector deleting destructors call __global_delete (a weak
+/// external) instead of calling ::operator delete directly. This allows
+/// environments without a global ::operator delete (e.g., kernel mode) to
+/// gracefully fall back to a no-op __empty_global_delete.
+static llvm::Constant *
+getOrCreateMSVCGlobalDeleteWrapper(CodeGenModule &CGM,
+                                   const FunctionDecl *GlobOD) {
+  llvm::Module &M = CGM.getModule();
+  llvm::LLVMContext &LLVMCtx = M.getContext();
+
+  llvm::Constant *GlobDeleteCallee = CGM.GetAddrOfFunction(GlobOD);
+  auto *GlobDeleteFn = cast<llvm::Function>(GlobDeleteCallee);
+  llvm::FunctionType *FnTy = GlobDeleteFn->getFunctionType();
+
+  // Derive __global_delete and __empty_global_delete mangled names.
+  // Global ::operator delete mangling:   ??3@<signature>
+  // Global ::operator delete[] mangling: ??_V@<signature>
+  // We construct:
+  //   ?__global_delete@@<signature>
+  //   ?__empty_global_delete@@<signature>
+  StringRef GlobDeleteMangledName = GlobDeleteFn->getName();
+  StringRef Signature;
+  if (GlobDeleteMangledName.starts_with("??3@"))
+    Signature = GlobDeleteMangledName.substr(4);
+  else if (GlobDeleteMangledName.starts_with("??_V@"))
+    Signature = GlobDeleteMangledName.substr(5);
+  else
+    llvm_unreachable("unexpected global operator delete mangling");
+
+  std::string GlobalDeleteName =
+      ("?__global_delete@@" + Signature).str();
+  std::string EmptyGlobalDeleteName =
+      ("?__empty_global_delete@@" + Signature).str();
+
+  // Only set up the wrapper once per module.
+  if (llvm::Function *Existing = M.getFunction(GlobalDeleteName))
+    return Existing;
+
+  // Create __empty_global_delete fallback.
+  llvm::Function *EmptyFn = llvm::Function::Create(
+      FnTy, llvm::GlobalValue::LinkOnceODRLinkage,
+      EmptyGlobalDeleteName, &M);
+  EmptyFn->setComdat(M.getOrInsertComdat(EmptyGlobalDeleteName));
+  EmptyFn->setUnnamedAddr(llvm::GlobalValue::UnnamedAddr::Global);
+  auto *BB = llvm::BasicBlock::Create(LLVMCtx, "", EmptyFn);
+  llvm::ReturnInst::Create(LLVMCtx, BB);
+
+  // Emit /ALTERNATENAME linker directive: if __global_delete isn't provided
+  // (e.g., by the CRT), fall back to the no-op __empty_global_delete.
+  std::string AltOption = "/alternatename:" + GlobalDeleteName + "=" +
+                          EmptyGlobalDeleteName;
+  auto *AltMD = llvm::MDNode::get(
+      LLVMCtx, {llvm::MDString::get(LLVMCtx, AltOption)});
+  M.getOrInsertNamedMetadata("llvm.linker.options")->addOperand(AltMD);
+
+  // Nothing directly uses this function other than the /alternatename
+  // directive, so explicitly mark it as used.
+  appendToUsed(M, {EmptyFn});
+
+  // Return the __global_delete wrapper function to call.
+  auto GlobalDeleteCallee = M.getOrInsertFunction(GlobalDeleteName, FnTy);
+  return cast<llvm::Function>(GlobalDeleteCallee.getCallee());
+}
+
 static void EmitConditionalArrayDtorCall(const CXXDestructorDecl *DD,
                                          CodeGenFunction &CGF,
                                          llvm::Value *ShouldDeleteCondition) {
@@ -1518,9 +1585,14 @@ static void EmitConditionalArrayDtorCall(const CXXDestructorDecl *DD,
       CGF.EmitBranchThroughCleanup(CGF.ReturnBlock);
 
       CGF.EmitBlock(GlobDelete);
+      // Use __global_delete wrapper for the global array delete path,
+      // matching MSVC's weak external mechanism.
+      llvm::Constant *GlobalDeleteWrapper =
+          getOrCreateMSVCGlobalDeleteWrapper(
+              CGF.CGM, Dtor->getGlobalArrayOperatorDelete());
       CGF.EmitDeleteCall(Dtor->getGlobalArrayOperatorDelete(), allocatedPtr,
                          CGF.getContext().getCanonicalTagType(ClassDecl),
-                         numElements, cookieSize);
+                         numElements, cookieSize, GlobalDeleteWrapper);
     }
   } else {
     // No operators delete[] were found, so emit a trap.
@@ -1747,9 +1819,12 @@ void EmitConditionalDtorDeleteCall(CodeGenFunction &CGF,
   CGF.Builder.CreateCondBr(ShouldCallDelete, continueBB, callDeleteBB);
 
   CGF.EmitBlock(callDeleteBB);
-  auto EmitDeleteAndGoToEnd = [&](const FunctionDecl *DeleteOp) {
+  auto EmitDeleteAndGoToEnd = [&](const FunctionDecl *DeleteOp,
+                                  llvm::Constant *CalleeOverride = nullptr) {
     CGF.EmitDeleteCall(DeleteOp, LoadThisForDtorDelete(CGF, Dtor),
-                       Context.getCanonicalTagType(ClassDecl));
+                       Context.getCanonicalTagType(ClassDecl),
+                       /*NumElements=*/nullptr, /*CookieSize=*/CharUnits(),
+                       CalleeOverride);
     if (ReturnAfterDelete)
       CGF.EmitBranchThroughCleanup(CGF.ReturnBlock);
     else
@@ -1773,7 +1848,13 @@ void EmitConditionalDtorDeleteCall(CodeGenFunction &CGF,
     CGF.Builder.CreateCondBr(ShouldCallGlobDelete, ClassDelete, GlobDelete);
     CGF.EmitBlock(GlobDelete);
 
-    EmitDeleteAndGoToEnd(GlobOD);
+    // Use __global_delete wrapper instead of directly calling
+    // ::operator delete. This matches MSVC's behavior: __global_delete is a
+    // weak external that falls back to __empty_global_delete (a no-op) when
+    // the CRT doesn't provide it (e.g., kernel-mode environments).
+    llvm::Constant *GlobalDeleteWrapper =
+        getOrCreateMSVCGlobalDeleteWrapper(CGF.CGM, GlobOD);
+    EmitDeleteAndGoToEnd(GlobOD, GlobalDeleteWrapper);
     CGF.EmitBlock(ClassDelete);
   }
   EmitDeleteAndGoToEnd(OD);
diff --git a/clang/lib/CodeGen/CGExprCXX.cpp b/clang/lib/CodeGen/CGExprCXX.cpp
index 82300c3ede183..9e61097b40c71 100644
--- a/clang/lib/CodeGen/CGExprCXX.cpp
+++ b/clang/lib/CodeGen/CGExprCXX.cpp
@@ -1337,9 +1337,12 @@ static void EmitNewInitializer(CodeGenFunction &CGF, const CXXNewExpr *E,
 static RValue EmitNewDeleteCall(CodeGenFunction &CGF,
                                 const FunctionDecl *CalleeDecl,
                                 const FunctionProtoType *CalleeType,
-                                const CallArgList &Args) {
+                                const CallArgList &Args,
+                                llvm::Constant *CalleeOverride = nullptr) {
   llvm::CallBase *CallOrInvoke;
-  llvm::Constant *CalleePtr = CGF.CGM.GetAddrOfFunction(CalleeDecl);
+  llvm::Constant *CalleePtr = CalleeOverride
+      ? CalleeOverride
+      : CGF.CGM.GetAddrOfFunction(CalleeDecl);
   CGCallee Callee = CGCallee::forDirect(CalleePtr, GlobalDecl(CalleeDecl));
   RValue RV = CGF.EmitCall(CGF.CGM.getTypes().arrangeFreeFunctionCall(
                                Args, CalleeType, /*ChainCall=*/false),
@@ -1788,7 +1791,8 @@ llvm::Value *CodeGenFunction::EmitCXXNewExpr(const CXXNewExpr *E) {
 void CodeGenFunction::EmitDeleteCall(const FunctionDecl *DeleteFD,
                                      llvm::Value *DeletePtr, QualType DeleteTy,
                                      llvm::Value *NumElements,
-                                     CharUnits CookieSize) {
+                                     CharUnits CookieSize,
+                                     llvm::Constant *CalleeOverride) {
   assert((!NumElements && CookieSize.isZero()) ||
          DeleteFD->getOverloadedOperator() == OO_Array_Delete);
 
@@ -1856,7 +1860,7 @@ void CodeGenFunction::EmitDeleteCall(const FunctionDecl *DeleteFD,
          "unknown parameter to usual delete function");
 
   // Emit the call to delete.
-  EmitNewDeleteCall(*this, DeleteFD, DeleteFTy, DeleteArgs);
+  EmitNewDeleteCall(*this, DeleteFD, DeleteFTy, DeleteArgs, CalleeOverride);
 
   // If call argument lowering didn't use a generated tag argument alloca we
   // remove them
diff --git a/clang/lib/CodeGen/CodeGenFunction.h b/clang/lib/CodeGen/CodeGenFunction.h
index 0ff93d2ce7363..5ecb64885b40a 100644
--- a/clang/lib/CodeGen/CodeGenFunction.h
+++ b/clang/lib/CodeGen/CodeGenFunction.h
@@ -3291,7 +3291,8 @@ class CodeGenFunction : public CodeGenTypeCache {
 
   void EmitDeleteCall(const FunctionDecl *DeleteFD, llvm::Value *Ptr,
                       QualType DeleteTy, llvm::Value *NumElements = nullptr,
-                      CharUnits CookieSize = CharUnits());
+                      CharUnits CookieSize = CharUnits(),
+                      llvm::Constant *CalleeOverride = nullptr);
 
   RValue EmitBuiltinNewDeleteCall(const FunctionProtoType *Type,
                                   const CallExpr *TheCallExpr, bool IsDelete);
diff --git a/clang/test/CodeGenCXX/cxx2a-destroying-delete.cpp b/clang/test/CodeGenCXX/cxx2a-destroying-delete.cpp
index c83cb32251462..97ed0c57e8f68 100644
--- a/clang/test/CodeGenCXX/cxx2a-destroying-delete.cpp
+++ b/clang/test/CodeGenCXX/cxx2a-destroying-delete.cpp
@@ -229,8 +229,8 @@ H::~H() { call_in_dtor(); }
 // CLANG22-MSABI-NEXT: br i1 %[[CHCK2]], label %dtor.call_class_delete, label %dtor.call_glob_delete
 //
 // CLANG22-MSABI-LABEL: dtor.call_glob_delete:
-// CLANG22-MSABI64: call void @"??3@YAXPEAX_K@Z"(ptr noundef %{{.*}}, i64 noundef 48)
-// CLANG22-MSABI32: call void @"??3@YAXPAXIW4align_val_t@std@@@Z"(ptr noundef %{{.*}}, i32 noundef 32, i32 noundef 16)
+// CLANG22-MSABI64: call void @"?__global_delete@@YAXPEAX_K@Z"(ptr noundef %{{.*}}, i64 noundef 48)
+// CLANG22-MSABI32: call void @"?__global_delete@@YAXPAXIW4align_val_t@std@@@Z"(ptr noundef %{{.*}}, i32 noundef 32, i32 noundef 16)
 // CLANG22-MSABI-NEXT: br label %[[RETURN:.*]]
 //
 // CLANG21-MSABI: dtor.call_delete:
@@ -284,8 +284,8 @@ I::~I() { call_in_dtor(); }
 // CLANG22-MSABI-NEXT: br i1 %[[CHCK2]], label %dtor.call_class_delete, label %dtor.call_glob_delete
 //
 // CLANG22-MSABI: dtor.call_glob_delete:
-// CLANG22-MSABI64: call void @"??3@YAXPEAX_KW4align_val_t@std@@@Z"(ptr noundef %{{.*}}, i64 noundef 96, i64 noundef 32)
-// CLANG22-MSABI32: call void @"??3@YAXPAXIW4align_val_t@std@@@Z"(ptr noundef %{{.*}}, i32 noundef 64, i32 noundef 32)
+// CLANG22-MSABI64: call void @"?__global_delete@@YAXPEAX_KW4align_val_t@std@@@Z"(ptr noundef %{{.*}}, i64 noundef 96, i64 noundef 32)
+// CLANG22-MSABI32: call void @"?__global_delete@@YAXPAXIW4align_val_t@std@@@Z"(ptr noundef %{{.*}}, i32 noundef 64, i32 noundef 32)
 // CLANG22-MSABI-NEXT: br label %[[RETURN:.*]]
 //
 // CLANG21-MSABI: dtor.call_delete:
diff --git a/clang/test/CodeGenCXX/microsoft-abi-structors.cpp b/clang/test/CodeGenCXX/microsoft-abi-structors.cpp
index 670988fc1ada2..1a4a291e28c0a 100644
--- a/clang/test/CodeGenCXX/microsoft-abi-structors.cpp
+++ b/clang/test/CodeGenCXX/microsoft-abi-structors.cpp
@@ -487,7 +487,7 @@ void checkH() {
 // DTORS-NEXT:   br i1 %[[CONDITION1]], label %[[CALL_CLASS_DELETE:[0-9a-z._]+]], label %[[CALL_GLOB_DELETE:[0-9a-z._]+]]
 //
 // DTORS:      [[CALL_GLOB_DELETE]]
-// DTORS-NEXT:   call void @"??3@YAXPAX@Z"(ptr %[[THIS]])
+// DTORS-NEXT:   call void @"?__global_delete@@YAXPAX@Z"(ptr %[[THIS]])
 // DTORS-NEXT:   br label %[[CONTINUE_LABEL]]
 //
 // DTORS:      [[CALL_CLASS_DELETE]]
diff --git a/clang/test/CodeGenCXX/microsoft-vector-deleting-dtors.cpp b/clang/test/CodeGenCXX/microsoft-vector-deleting-dtors.cpp
index 459c1b6593fa1..766d921fde276 100644
--- a/clang/test/CodeGenCXX/microsoft-vector-deleting-dtors.cpp
+++ b/clang/test/CodeGenCXX/microsoft-vector-deleting-dtors.cpp
@@ -42,6 +42,17 @@ struct AllocatedAsArray : public Bird {
 
 };
 
+struct KernelBase {
+  static void* operator new(__SIZE_TYPE__ n, int tag = 0);
+  static void operator delete(void* p);
+  static void operator delete[](void* p);
+  virtual ~KernelBase();
+};
+
+struct KernelDerived : KernelBase {
+  virtual ~KernelDerived();
+};
+
 // Vector deleting dtor for Bird is an alias because no new Bird[] expressions
 // in the TU.
 // X64: @"??_EBird@@UEAAPEAXI@Z" = weak dso_local unnamed_addr alias ptr (ptr, i32), ptr @"??_GBird@@UEAAPEAXI@Z"
@@ -83,6 +94,14 @@ void bar() {
   sp.foo();
 }
 
+KernelBase::~KernelBase() {}
+KernelDerived::~KernelDerived() {}
+
+void kernelTest() {
+  KernelBase *p = new KernelDerived[2];
+  delete[] p;
+}
+
 // CHECK-LABEL: define dso_local void @{{.*}}dealloc{{.*}}(
 // CHECK-SAME: ptr noundef %[[PTR:.*]])
 // CHECK: entry:
@@ -260,10 +279,30 @@ void bar() {
 // X86-NEXT: %[[ARRSZ:.*]] = mul i32 4, %[[COOKIE:.*]]
 // X64-NEXT: %[[TOTALSZ:.*]] = add i64 %[[ARRSZ]], 8
 // X86-NEXT: %[[TOTALSZ:.*]] = add i32 %[[ARRSZ]], 4
-// X64-NEXT: call void @"??_V@YAXPEAX_K@Z"(ptr noundef %2, i64 noundef %[[TOTALSZ]])
-// X86-NEXT: call void @"??_V@YAXPAXI@Z"(ptr noundef %2, i32 noundef %[[TOTALSZ]])
+// X64-NEXT: call void @"?__global_delete@@YAXPEAX_K@Z"(ptr noundef %2, i64 noundef %[[TOTALSZ]])
+// X86-NEXT: call void @"?__global_delete@@YAXPAXI@Z"(ptr noundef %2, i32 noundef %[[TOTALSZ]])
 // CHECK-NEXT:   br label %dtor.continue
 
+// Test that when a class provides its own operator delete, the deleting
+// destructor calls __global_delete (a weak external with no-op fallback)
+// instead of directly referencing ::operator delete. This is critical for
+// environments like kernel mode where no global ::operator delete exists.
+// Verify __empty_global_delete is emitted as a no-op fallback.
+// X64: define linkonce_odr void @"?__empty_global_delete@@YAXPEAX_K@Z"(ptr %0, i64 %1)
+// X64-NEXT: ret void
+// X64-LABEL: define weak dso_local noundef ptr @"??_EKernelDerived@@UEAAPEAXI@Z"
+// Verify the array delete path in the VDD uses __global_delete.
+// X64: dtor.call_glob_delete_after_array_destroy:
+// X64: call void @"?__global_delete@@YAXPEAX_K@Z"(ptr noundef %{{.*}}, i64 noundef %{{.*}})
+// Verify the scalar deleting dtor uses __global_delete, not ::operator delete.
+// X64: dtor.call_delete:
+// X64-NEXT:  %[[FLAGCHECK:.*]] = and i32 %should_call_delete2, 4
+// X64-NEXT:  %[[ISGLOB:.*]] = icmp eq i32 %[[FLAGCHECK]], 0
+// X64-NEXT:  br i1 %[[ISGLOB]], label %dtor.call_class_delete, label %dtor.call_glob_delete
+// X64: dtor.call_glob_delete:
+// X64-NEXT:  call void @"?__global_delete@@YAXPEAX_K@Z"(ptr noundef %{{.*}}, i64 noundef 8)
+// X64: dtor.call_class_delete:
+// X64-NEXT:  call void @"??3KernelBase@@SAXPEAX@Z"(ptr noundef %{{.*}})
 
 
 struct BaseDelete1 {
@@ -346,3 +385,8 @@ void foobartest() {
 // X64: define weak dso_local noundef ptr @"??_EAllocatedAsArray@@UEAAPEAXI@Z"
 // X86: define weak dso_local x86_thiscallcc noundef ptr @"??_EAllocatedAsArray@@UAEPAXI@Z"
 // CLANG21: define linkonce_odr dso_local noundef ptr @"??_GAllocatedAsArray@@UEAAPEAXI@Z"
+
+// Verify the /ALTERNATENAME linker directive.
+// X64: !{!"/alternatename:?__global_delete@@YAXPEAX_K@Z=?__empty_global_delete@@YAXPEAX_K@Z"}
+
+// CLANG21-NOT: __global_delete
diff --git a/clang/test/CodeGenCXX/microsoft-vector-deleting-dtors2.cpp b/clang/test/CodeGenCXX/microsoft-vector-deleting-dtors2.cpp
index 6412bf5b1dc3b..b1ea0381b85a8 100644
--- a/clang/test/CodeGenCXX/microsoft-vector-deleting-dtors2.cpp
+++ b/clang/test/CodeGenCXX/microsoft-vector-deleting-dtors2.cpp
@@ -59,7 +59,7 @@ void TesttheTest() {
 // X64: define weak dso_local noundef ptr @"??_EDrawingBuffer@@UEAAPEAXI@Z"
 // X64: call void @"??1DrawingBuffer@@UEAA@XZ"(ptr noundef nonnull align 8 dereferenceable(8) %arraydestroy.element)
 // X64: call void @"??_V?$RefCounted@UDrawingBuffer@@@@SAXPEAX@Z"(ptr noundef %2)
-// X64: call void @"??_V@YAXPEAX_K@Z"(ptr noundef %2, i64 noundef %{{.*}})
+// X64: call void @"?__global_delete@@YAXPEAX_K@Z"(ptr noundef %2, i64 noundef %{{.*}})
 // X64: call void @"??1DrawingBuffer@@UEAA@XZ"(ptr noundef nonnull align 8 dereferenceable(8) %this1)
 // X64: call void @"??3@YAXPEAX_K@Z"(ptr noundef %this1, i64 noundef {{.*}})
 
@@ -70,7 +70,7 @@ void TesttheTest() {
 // X86: define weak dso_local x86_thiscallcc noundef ptr @"??_EDrawingBuffer@@UAEPAXI@Z"
 // X86: call x86_thiscallcc void @"??1DrawingBuffer@@UAE@XZ"(ptr noundef nonnull align 4 dereferenceable(4) %arraydestroy.element)
 // X86: call void @"??_V?$RefCounted@UDrawingBuffer@@@@SAXPAX@Z"(ptr noundef %2)
-// X86: call void @"??_V@YAXPAXI@Z"(ptr noundef %2, i32 noundef {{.*}})
+// X86: call void @"?__global_delete@@YAXPAXI@Z"(ptr noundef %2, i32 noundef {{.*}})
 // X86  call x86_thiscallcc void @"??1DrawingBuffer@@UAE@XZ"(ptr noundef nonnull align 4 dereferenceable(4) %this1)
 // X86: call void @"??3@YAXPAXI@Z"(ptr noundef %this1, i32 noundef {{.*}})
 
diff --git a/clang/test/CodeGenCXX/msvc-vector-deleting-dtors-sized-delete.cpp b/clang/test/CodeGenCXX/msvc-vector-deleting-dtors-sized-delete.cpp
index 6c9faa88e08e9..db8c429956b6f 100644
--- a/clang/test/CodeGenCXX/msvc-vector-deleting-dtors-sized-delete.cpp
+++ b/clang/test/CodeGenCXX/msvc-vector-deleting-dtors-sized-delete.cpp
@@ -50,5 +50,5 @@ void test() {
 // X86-NEXT:  %[[ARRSZ1:.*]] = mul i32 12, %[[HOWMANY]]
 // X64-NEXT:  %[[TOTALSZ1:.*]] = add i64 %[[ARRSZ1]], 8
 // X86-NEXT:  %[[TOTALSZ1:.*]] = add i32 %[[ARRSZ1]], 4
-// X64-NEXT:   call void @"??_V@YAXPEAX_K@Z"(ptr noundef %2, i64 noundef %[[TOTALSZ1]])
-// X86-NEXT:   call void @"??_V@YAXPAXI@Z"(ptr noundef %2, i32 noundef %[[TOTALSZ1]])
+// X64-NEXT:   call void @"?__global_delete@@YAXPEAX_K@Z"(ptr noundef %2, i64 noundef %[[TOTALSZ1]])
+// X86-NEXT:   call void @"?__global_delete@@YAXPAXI@Z"(ptr noundef %2, i32 noundef %[[TOTALSZ1]])
diff --git a/clang/test/Modules/glob-delete-with-virtual-dtor.cpp b/clang/test/Modules/glob-delete-with-virtual-dtor.cpp
index fb2e2a4decf60..18e90aaca78f0 100644
--- a/clang/test/Modules/glob-delete-with-virtual-dtor.cpp
+++ b/clang/test/Modules/glob-delete-with-virtual-dtor.cpp
@@ -30,8 +30,8 @@ void out_of_module_tests() {
 // CHECK-NEXT:   br i1 %[[CONDITION1]], label %[[CALL_CLASS_DELETE:[0-9a-z._]+]], label %[[CALL_GLOB_DELETE:[0-9a-z._]+]]
 //
 // CHECK:      [[CALL_GLOB_DELETE]]
-// CHECK32-NEXT:   call void @"??3@YAXPAXI@Z"
-// CHECK64-NEXT:   call void @"??3@YAXPEAX_K@Z"
+// CHECK32-NEXT:   call void @"?__global_delete@@YAXPAXI@Z"
+// CHECK64-NEXT:   call void @"?__global_delete@@YAXPEAX_K@Z"
 // CHECK-NEXT:   br label %[[CONTINUE_LABEL]]
 //
 // CHECK:      [[CALL_CLASS_DELETE]]
diff --git a/clang/test/Modules/msvc-vector-deleting-destructors.cpp b/clang/test/Modules/msvc-vector-deleting-destructors.cpp
index 68faa687251d7..9e99ae1e191b7 100644
--- a/clang/test/Modules/msvc-vector-deleting-destructors.cpp
+++ b/clang/test/Modules/msvc-vector-deleting-destructors.cpp
@@ -24,11 +24,11 @@ void out_of_module_tests(Derived *p, Derived *p1) {
 // CHECK32-NEXT: %[[ARRSZ:.*]] = mul i32 8, %[[COOKIE:.*]]
 // CHECK64-NEXT: %[[TOTALSZ:.*]] = add i64 %[[ARRSZ]], 8
 // CHECK32-NEXT: %[[TOTALSZ:.*]] = add i32 %[[ARRSZ]], 4
-// CHECK32-NEXT:   call void @"??_V@YAXPAXI@Z"(ptr noundef %2, i32 noundef %[[TOTALSZ]])
-// CHECK64-NEXT:   call void @"??_V@YAXPEAX_K@Z"(ptr noundef %2, i64 noundef %[[TOTALSZ]])
+// CHECK32-NEXT:   call void @"?__global_delete@@YAXPAXI@Z"(ptr noundef %2, i32 noundef %[[TOTALSZ]])
+// CHECK64-NEXT:   call void @"?__global_delete@@YAXPEAX_K@Z"(ptr noundef %2, i64 noundef %[[TOTALSZ]])
 // CHECK: dtor.call_glob_delete:
-// CHECK32-NEXT:   call void @"??3@YAXPAXI@Z"(ptr noundef %this1, i32 noundef 8)
-// CHECK64-NEXT:   call void @"??3@YAXPEAX_K@Z"(ptr noundef %this1, i64 noundef 16)
+// CHECK32-NEXT:   call void @"?__global_delete@@YAXPAXI@Z"(ptr noundef %this1, i3...
[truncated]

@github-actions

github-actions Bot commented Mar 24, 2026

Copy link
Copy Markdown

✅ With the latest revision this PR passed the C/C++ code formatter.

@Fznamznon Fznamznon left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for working on this! This makes sense to me.
However given my inexperience in MSVC ABI and clang CodeGen, it would be great to get an additional approval, perhaps from @efriedma-quic

Comment thread clang/docs/ReleaseNotes.rst Outdated
@@ -452,6 +452,11 @@ Windows Support
- Clang now defines the ``_MSVC_TRADITIONAL`` macro as ``1`` when emulating MSVC
19.15 (Visual Studio 2017 version 15.8) and later. (#GH47114)

- In MSVC compatibility mode, scalar and vector deleting destructors now call

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if we need to backport this fix to clang 22?
I'm not sure if it is ABI breaking though. If it is - we can't backport it and need to put it under -fclang-abi-compat=23

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would make sense to me to backport it, and it shouldn't be breaking. But I'll leave that up to the Clang maintainers to decide.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mmm, if we end up backporting it, I'm not sure we need a release note. cc @AaronBallman on release note matters

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Backports are usually just noted in the patch release announcement, I think.

This seems a little large to backport... if we expect people who care about this use-case to be using the 22 release, I guess we could.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think there will be much use of this feature: you need to have a custom operator delete (i.e., using C++) but be in an environment without the CRT (kernel-mode, drivers, etc.). I'm fine with not backporting.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But this does break ABI, doesn't it? From a comment elsewhere:

So it would be:

Code Provides Old New
::operator delete + __global_delete (common case) Calls __global_delete -> ::operator new Calls ::operator new
__global_delete only (kernel-mode) Calls __global_delete Calls __global_delete
::operator new only (non-MSVC CRT) Calls __empty_global_delete and leaks > Calls ::operator new
Nothing Calls __empty_global_delete and leaks Linker error

Comment thread clang/lib/CodeGen/CGClass.cpp Outdated
llvm::Function *EmptyFn = llvm::Function::Create(
FnTy, llvm::GlobalValue::LinkOnceODRLinkage, EmptyGlobalDeleteName, &M);
EmptyFn->setComdat(M.getOrInsertComdat(EmptyGlobalDeleteName));
EmptyFn->setUnnamedAddr(llvm::GlobalValue::UnnamedAddr::Global);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if we just need to call CodeGenModule::SetFunctionAttributes instead of manually adding the attributes?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SetFunctionAttributes requires a GlobalDecl which we don't have for this function, but I've added SetLLVMFunctionAttributesForDefinition to copy the attributes from the global delete function.

Comment thread clang/lib/CodeGen/CGClass.cpp Outdated
/// 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,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does it make sense to assert that the target is MSVC ABI at the beginning of the function?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, makes sense.

Comment thread clang/lib/CodeGen/CGExprCXX.cpp Outdated
llvm::CallBase *CallOrInvoke;
llvm::Constant *CalleePtr = CGF.CGM.GetAddrOfFunction(CalleeDecl);
llvm::Constant *CalleePtr =
CalleeOverride ? CalleeOverride : CGF.CGM.GetAddrOfFunction(CalleeDecl);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we somehow end up failing to emit global operator delete if it is never used in a TU from any other place except some deleting destructor?
I thought about type-aware deallocators, although these seem to be force-emitted https://godbolt.org/z/6o6vMc9G5 but maybe there is something else

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm a little confused about what you mean here - what function specifically are you worried about missing?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CodeGen usually tries to be smart about functions that are emitted in the resulting LLVM IR module. If something is never used and there is no way to use it outside of the current translation unit - it won't be emitted, the simplest example of that case is unused static function.

I was thinking since we always replace the calls to the operator delete with a wrapper call, It may happen that we never see the actual (user defined) operator delete (in my compiler explorer link it is function with std::type_identity argument, its mangling name is ??3@YAXU?$type_identity@UKernelBase@@@std@@PEAX_KW4align_val_t@1@@Z in the LLVM IR) in the codegen as something referenced within the TU and therefore it might be missing due to CodeGen being too smart about it.
However in my compiler explorer link I see it emitted even though I never create an object of the class this function intended for and therefore that example is not good.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it's possible that there may end up being no calls to the global operator delete, however it's linkage type should prevent it from being optimized out. Once you get to the final binary, if there still are no calls (which there should be, since the CRT provides a __global_delete that forwards to it) then the linker may drop it.

Comment thread clang/docs/ReleaseNotes.rst Outdated
@@ -452,6 +452,11 @@ Windows Support
- Clang now defines the ``_MSVC_TRADITIONAL`` macro as ``1`` when emulating MSVC
19.15 (Visual Studio 2017 version 15.8) and later. (#GH47114)

- In MSVC compatibility mode, scalar and vector deleting destructors now call

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Backports are usually just noted in the patch release announcement, I think.

This seems a little large to backport... if we expect people who care about this use-case to be using the 22 release, I guess we could.

Comment thread clang/lib/CodeGen/CGClass.cpp Outdated
EmptyFn->setUnnamedAddr(llvm::GlobalValue::UnnamedAddr::Global);
CGM.SetLLVMFunctionAttributesForDefinition(GlobOD, EmptyFn);
auto *BB = llvm::BasicBlock::Create(LLVMCtx, "", EmptyFn);
llvm::ReturnInst::Create(LLVMCtx, BB);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The idea behind __empty_global_delete is that it's never called, right? So if execution does reach this implementation, something is wrong.

Should we trap instead of silently leaking/corrupting memory?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MSVC explicitly leaves this as a no-op, so making it trap instead would be a change in behavior when using a different compiler.

Leaking memory is not a UB: it's well defined (albeit bad) behavior.

Personally, if we don't want to silently leak, then I would rather not emit this symbol at all and have a link-time error instead of runtime crash.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It should really be impossible to end up calling __empty_global_delete: the only way we should try to call global delete on an object is if it was allocated with global new, and if global new exists, __global_delete should also exist. So it probably indicates memory corruption.

But if you think it's better to just match MSVC, I guess I'm okay with that.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your intuition here is correct: MSVC emits the definition for __global_delete if there are any calls to global ::operator delete, therefore __empty_global_delete should never be called. I've switched it to trap instead of being a no-op.

Comment thread clang/lib/CodeGen/CGClass.cpp Outdated
// Global ::operator delete[] mangling: ??_V@<signature>
// We construct:
// ?__global_delete@@<signature>
// ?__empty_global_delete@@<signature>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What happens if someone is using libc++ instead of MSVC stl?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The difference is the CRTs, not the STLs.

But yes, your point still stands: Microsoft CRT provides the definition of __global_delete that forwards to the global operator delete, but I don't think other CRTs do.

Maybe we should use the "weak" linking mechanism here instead: still call the normal ::operator delete then provide an /ALTERNATENAME from ::operator delete to __global_delete and not do any of the __empty_global_delete stuff at all. This would mean that anyone providing an operator delete gets the normal behavior of calling that directly, but anyone without that would be required to provide __global_delete, which would make it compatible with MSVC for that scenario.

So it would be:

Code Provides Old New
::operator delete + __global_delete (common case) Calls __global_delete -> ::operator new Calls ::operator new
__global_delete only (kernel-mode) Calls __global_delete Calls __global_delete
::operator new only (non-MSVC CRT) Calls __empty_global_delete and leaks Calls ::operator new
Nothing Calls __empty_global_delete and leaks Linker error

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Update: the CRT doesn't actually provide the definition of __global_delete, it is compiler generated, therefore this is safe to use with non-MSVC CRTs.

@efriedma-quic efriedma-quic left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

Comment thread clang/lib/CodeGen/CGClass.cpp Outdated
EmptyFn->setUnnamedAddr(llvm::GlobalValue::UnnamedAddr::Global);
CGM.SetLLVMFunctionAttributesForDefinition(GlobOD, EmptyFn);
auto *BB = llvm::BasicBlock::Create(LLVMCtx, "", EmptyFn);
llvm::ReturnInst::Create(LLVMCtx, BB);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It should really be impossible to end up calling __empty_global_delete: the only way we should try to call global delete on an object is if it was allocated with global new, and if global new exists, __global_delete should also exist. So it probably indicates memory corruption.

But if you think it's better to just match MSVC, I guess I'm okay with that.

@dpaoliello

Copy link
Copy Markdown
Contributor Author

Update: I did some more research into this, and it turns out that __global_delete is actually a compiler-generated function, but it is ONLY emitted if there is actually a call to ::operator delete.

Comment thread clang/lib/CodeGen/CGExprCXX.cpp Outdated
// class-specific one), note it so we emit __global_delete forwarding bodies.
if (!isa<CXXMethodDecl>(E->getOperatorDelete()) &&
CGM.getTarget().getCXXABI().isMicrosoft())
CGM.noteDirectGlobalDelete();

@efriedma-quic efriedma-quic Apr 3, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is supposed to be specifically looking for a ::delete expression involving a class with a member delete, not just any delete which calls a global operator delete.

We also need to handle the case where the class is dllexport.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good call - fixed these and checked MSVC's behavior for other corner cases as well.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

involving a class with a member delete, not just any delete which calls a global operator delete.

Sorry, has been it actually applied/responded to?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, yep, there's a gap here. Will have an update shortly.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed: lined-up with MSVC's behavior where it requires an explicit global operator delete (with the leading :: qualification) for a type with non-trivial destructor.

@github-actions

github-actions Bot commented Apr 13, 2026

Copy link
Copy Markdown

🐧 Linux x64 Test Results

  • 120535 tests passed
  • 4941 tests skipped

✅ The build succeeded and all tests passed.

@dpaoliello

Copy link
Copy Markdown
Contributor Author

@efriedma-quic @Fznamznon Any additional concerns?

@Fznamznon Fznamznon left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry for the delay, this is mostly lgtm though I do have a couple questions

Comment thread clang/lib/CodeGen/CodeGenModule.cpp Outdated

void CodeGenModule::addPendingGlobalDelete(
StringRef GlobalDeleteName, const FunctionDecl *OperatorDeleteFD) {
// Only add if we haven't seen this name before.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does it make sense to use a StringMap for PendingMSVCGlobalDeletes collection instead?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For deterministic builds, we need to keep the order stable. I did switch to a MapVector to simplify this.

Comment thread clang/lib/CodeGen/CGExprCXX.cpp Outdated
// class-specific one), note it so we emit __global_delete forwarding bodies.
if (!isa<CXXMethodDecl>(E->getOperatorDelete()) &&
CGM.getTarget().getCXXABI().isMicrosoft())
CGM.noteDirectGlobalDelete();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

involving a class with a member delete, not just any delete which calls a global operator delete.

Sorry, has been it actually applied/responded to?


// __global_delete should NOT have a forwarding body (no ::delete in this TU,
// no dllexport class).
// CHECK-NOT: define {{.*}}void @"?__global_delete@@YAXPEAX_K@Z"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, the intention to emit __global_delete body on any ::delete call not just involving a class with defined operator delete?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct. The presence of any ::delete in the TU proves ::operator delete exists in the program, which is exactly the condition under which it's safe to emit a real __global_delete forwarding body.

llvm::ReturnInst::Create(getModule().getContext(), BB);

// Use LinkOnceODR so multiple TUs can emit this without conflicts.
GlobDelFn->setLinkage(llvm::GlobalValue::LinkOnceODRLinkage);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm slightly confused by the fact that multiple comments says that __global_delete is a weak symbol but is emitted with linkonce_odr linkage.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, sorry, it was weak in an earlier version. Cleaned up the comments to explain the current delayed-emission-or-default mechanism.

@dpaoliello
dpaoliello force-pushed the globalopdel branch 2 times, most recently from 482ae8c to b2575be Compare July 7, 2026 22:06

@Fznamznon Fznamznon left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! LGTM

@efriedma-quic given that it slightly changed after your approval, are you still ok with it?

// CHECK-NEXT: br label %dtor.continue

// Test that when a class provides its own operator delete, the deleting
// destructor calls __global_delete (a weak external) instead of directly

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not weak, right?

Suggested change
// destructor calls __global_delete (a weak external) instead of directly
// destructor calls __global_delete instead of directly

@efriedma-quic efriedma-quic left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM with a couple minor comments.

FnTy, llvm::GlobalValue::LinkOnceODRLinkage, EmptyGlobalDeleteName, &M);
EmptyFn->setComdat(M.getOrInsertComdat(EmptyGlobalDeleteName));
EmptyFn->setUnnamedAddr(llvm::GlobalValue::UnnamedAddr::Global);
CGM.SetLLVMFunctionAttributesForDefinition(GlobOD, EmptyFn);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure how much practical effect it has here, but the general pattern is to call CGM.SetLLVMFunctionAttributes(), then CGM.SetLLVMFunctionAttributesForDefinition(), then getTargetCodeGenInfo().setTargetAttributes(). (We should really clean this up at some point...)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated.

// Use LinkOnceODR so multiple TUs can emit this without conflicts.
GlobDelFn->setLinkage(llvm::GlobalValue::LinkOnceODRLinkage);
GlobDelFn->setComdat(getModule().getOrInsertComdat(GlobDelFn->getName()));
SetLLVMFunctionAttributesForDefinition(OperatorDeleteFD, GlobDelFn);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Same issue with attributes here.)

…tructors instead of directly referencing ::operator delete

When Clang emits scalar/vector deleting destructors for classes with a class-level `operator delete`, it generates a conditional dispatch that can call either the class-level or global `::operator delete`. The global path directly referenced `::operator delete`, causing `LNK2001` linker errors in environments where no global `::operator delete` exists.

MSVC handles this by calling `__global_delete` - this is a compiler generated function that is ONLY created if there is a direct call to global `::operator delete`. Additionally, it always emits an empty `__empty_global_delete` and uses `/ALTERNATIVENAME` linker arg to default `__global_delete` to `__empty_global_delete` if there is NEVER an actual all to `::operator delete` (thus the empty function should never be called).

This change aligns Clang's behavior with MSVC when MSVC compatibility mode and non-LLVM 21 ABI is used, with one difference: the LLVM generated `__empty_global_delete` traps since it should never be called.
…mes, only emit __global_delete body for calls to global op with a class type with a non-trivial dtor
@dpaoliello
dpaoliello merged commit 1dd5ad4 into llvm:main Jul 10, 2026
12 checks passed
@dpaoliello
dpaoliello deleted the globalopdel branch July 10, 2026 23:13
pedroMVicente pushed a commit to pedroMVicente/llvm-project that referenced this pull request Jul 15, 2026
…estructors instead of directly referencing `::operator delete` (llvm#188372)

When Clang emits scalar/vector deleting destructors for classes with a
class-level `operator delete`, it generates a conditional dispatch that
can call either the class-level or global `::operator delete`. The
global path directly referenced `::operator delete`, causing `LNK2001`
linker errors in environments where no global `::operator delete`
exists.

MSVC handles this by calling `__global_delete` (and
`__global_array_delete` for vector deletes) - this is a compiler
generated function that is ONLY defined if there is a direct call to
global `::operator delete` for type with non-trivial destructors.
Additionally, it always emits an empty `__empty_global_delete` and uses
`/ALTERNATIVENAME` linker arg to default `__global_delete` (and
`__global_array_delete`) to `__empty_global_delete` if there is NEVER an
delete operator call that would triffer the body to be emitted (thus the
empty function should never be called).

This change aligns Clang's behavior with MSVC when MSVC compatibility
mode and non-LLVM 21 ABI is used, with one difference: the LLVM
generated `__empty_global_delete` traps since it should never be called.
dpaoliello added a commit that referenced this pull request Jul 16, 2026
…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.
dyung pushed a commit to dpaoliello/llvm-project that referenced this pull request Jul 22, 2026
…delete (llvm#209585)

PR llvm#188372 made Clang's MSVC-ABI deleting-destructor path route global
deletes
through compiler-generated __global_delete / __global_array_delete
wrappers
instead of referencing ::operator delete directly. This lets a deleting
destructor be emitted in environments (e.g. kernel mode) where no global
::operator delete exists: each wrapper defaults to a trapping
__empty_global_delete fallback, and a real forwarding body that calls
::operator delete is materialized only when the program actually uses
::delete.

This change refines how those wrappers and their fallbacks are emitted,
fixing
two problems that made the llvm#188372 mechanism fail in practice.

New behavior:

- The trapping fallback is now emitted as a weak GlobalAlias to
__empty_global_delete rather than via an /alternatename directive. This
lowers
to a COFF weak-external-with-default -- exactly what MSVC emits -- so a
real
__global_delete defined in any TU overrides the fallback in every other
TU at
  link time.

- A ::delete expression now registers the wrapper for its resolved
global
operator delete directly (in EmitCXXDeleteExpr), so the strong
forwarding body
is emitted in that TU regardless of where the class's deleting
destructor is
defined. This matches MSVC, which emits the forwarder at every ::delete
site.

Bugs fixed:

1. Arm64EC (miscompile -> LNK2019). /alternatename only names the plain
symbol,
not the backend-generated "$$h" hybrid EC symbol referenced by the exit
thunk, so Arm64EC images left __global_delete$exit_thunk unresolved. The
weak
GlobalAlias correctly produces the "$$h" symbol, exit thunk, and plain
alias
   (verified on both x64 and Arm64EC).

2. Cross-TU ::delete (runtime crash). Previously the forwarding body was
only
emitted in a TU that *both* emitted a matching vector deleting
destructor and
contained a ::delete. A TU that only performed `::delete p` -- with the
class
defined elsewhere -- emitted no forwarder, so nothing overrode the
weak-alias
trap and the program executed a trapping __empty_global_delete at
runtime
(STATUS_ILLEGAL_INSTRUCTION). Registering the wrapper at the ::delete
site
   fixes this.

To share wrapper-creation logic between the deleting-destructor path and
the
delete-expression path, getOrCreateMSVCGlobalDeleteWrapper is promoted
from a
static helper in CGClass.cpp to a CodeGenModule method.

Verified end-to-end: a weak alias in one TU and a strong forwarder in
another
resolve to the forwarder under both lld-link and MSVC link.exe, and the
previously-crashing delete-only TU now runs cleanly.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

clang:codegen IR generation bugs: mangling, exceptions, etc. clang:modules C++20 modules and Clang Header Modules clang Clang issues not falling into any other category

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants