Skip to content

[win][clang] Align scalar deleting destructors with MSABI #139566

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 19 commits into
base: main
Choose a base branch
from

Conversation

Fznamznon
Copy link
Contributor

@Fznamznon Fznamznon commented May 12, 2025

While working on vector deleting destructors support (GH19772), I noticed that MSVC produces different code in scalar deleting destructor body depending on whether class defined its own operator delete. In MSABI deleting destructors accept an additional implicit flag parameter allowing some sort of flexibility. The mismatch I noticed is that whenever a global operator delete is called, i.e. ::delete, in the code produced by MSVC the implicit flag argument has a value that makes the 3rd bit set, i.e. "5" for scalar deleting destructors "7" for vector deleting destructors.
Prior to this patch, clang handled ::delete via calling global operator delete direct after the destructor call and not calling class operator delete in scalar deleting destructor body by passing "0" as implicit flag argument value. This is fine until there is an attempt to link binaries compiled with clang with binaries compiled with MSVC. The problem is that in binaries produced by MSVC the callsite of the destructor won't call global operator delete because it is assumed that the destructor will do that and a destructor body generated by clang will never do.
This patch removes call to global operator delete from the callsite and add additional check of the 3rd bit of the implicit parameter inside of scalar deleting destructor body.

While working on vector deleting destructors support (GH19772), I
noticed that MSVC produces different code in scalar deleting destructor
body depending on whether class defined its own operator delete.
In MSABI deleting destructors accept an additional implicit flag
parameter allowing some sort of flexibility. The mismatch I noticed is
that whenever a global operator delete is called, i.e. ::delete, in the
code produced by MSVC the implicit flag argument has a value that makes
the 3rd bit set, i.e. "5" for scalar deleting destructors "7" for vector
deleting destructors.
Prior to this patch, clang handled ::delete via calling global operator
delete direct after the destructor call and not calling class operator
delete in scalar deleting destructor body by passing "0" as implicit
flag argument value. This is fine until there is an attempt to link binaries
compiled with clang with binaries compiled with MSVC. The problem is that in
binaries produced by MSVC the callsite of the destructor won't call global
operator delete because it is assumed that the destructor will do that and a
destructor body generated by clang will never do.
This patch removes call to global operator delete from the callsite and
add additional check of the 3rd bit of the implicit parameter inside of
scalar deleting destructor body.
@Fznamznon Fznamznon requested review from rnk, zmodem and tahonermann May 12, 2025 15:45
@llvmbot llvmbot added clang Clang issues not falling into any other category clang:frontend Language frontend issues, e.g. anything involving "Sema" clang:codegen IR generation bugs: mangling, exceptions, etc. labels May 12, 2025
@llvmbot
Copy link
Member

llvmbot commented May 12, 2025

@llvm/pr-subscribers-clang-codegen

@llvm/pr-subscribers-clang

Author: Mariya Podchishchaeva (Fznamznon)

Changes

While working on vector deleting destructors support (GH19772), I noticed that MSVC produces different code in scalar deleting destructor body depending on whether class defined its own operator delete. In MSABI deleting destructors accept an additional implicit flag parameter allowing some sort of flexibility. The mismatch I noticed is that whenever a global operator delete is called, i.e. ::delete, in the code produced by MSVC the implicit flag argument has a value that makes the 3rd bit set, i.e. "5" for scalar deleting destructors "7" for vector deleting destructors.
Prior to this patch, clang handled ::delete via calling global operator delete direct after the destructor call and not calling class operator delete in scalar deleting destructor body by passing "0" as implicit flag argument value. This is fine until there is an attempt to link binaries compiled with clang with binaries compiled with MSVC. The problem is that in binaries produced by MSVC the callsite of the destructor won't call global operator delete because it is assumed that the destructor will do that and a destructor body generated by clang will never do.
This patch removes call to global operator delete from the callsite and add additional check of the 3rd bit of the implicit parameter inside of scalar deleting destructor body.


Full diff: https://github.com/llvm/llvm-project/pull/139566.diff

8 Files Affected:

  • (modified) clang/include/clang/AST/DeclCXX.h (+11)
  • (modified) clang/include/clang/Sema/Sema.h (+6-4)
  • (modified) clang/lib/CodeGen/CGClass.cpp (+63-14)
  • (modified) clang/lib/CodeGen/MicrosoftCXXABI.cpp (+3-8)
  • (modified) clang/lib/Sema/SemaDeclCXX.cpp (+15)
  • (modified) clang/lib/Sema/SemaExprCXX.cpp (+11-8)
  • (modified) clang/test/CodeGenCXX/cxx2a-destroying-delete.cpp (+52-7)
  • (modified) clang/test/CodeGenCXX/microsoft-abi-structors.cpp (+41-3)
diff --git a/clang/include/clang/AST/DeclCXX.h b/clang/include/clang/AST/DeclCXX.h
index b7980137002aa..cc2832ea99d4a 100644
--- a/clang/include/clang/AST/DeclCXX.h
+++ b/clang/include/clang/AST/DeclCXX.h
@@ -2855,6 +2855,7 @@ class CXXDestructorDecl : public CXXMethodDecl {
   // FIXME: Don't allocate storage for these except in the first declaration
   // of a virtual destructor.
   FunctionDecl *OperatorDelete = nullptr;
+  FunctionDecl *OperatorGlobalDelete = nullptr;
   Expr *OperatorDeleteThisArg = nullptr;
 
   CXXDestructorDecl(ASTContext &C, CXXRecordDecl *RD, SourceLocation StartLoc,
@@ -2885,6 +2886,16 @@ class CXXDestructorDecl : public CXXMethodDecl {
     return getCanonicalDecl()->OperatorDelete;
   }
 
+  const FunctionDecl *getOperatorGlobalDelete() const {
+    return getCanonicalDecl()->OperatorGlobalDelete;
+  }
+
+  void setOperatorGlobalDelete(FunctionDecl *OD) {
+    auto *First = cast<CXXDestructorDecl>(getFirstDecl());
+    if (OD && !First->OperatorGlobalDelete)
+      First->OperatorGlobalDelete = OD;
+  }
+
   Expr *getOperatorDeleteThisArg() const {
     return getCanonicalDecl()->OperatorDeleteThisArg;
   }
diff --git a/clang/include/clang/Sema/Sema.h b/clang/include/clang/Sema/Sema.h
index 6ea7ee281e14d..5f2ceebcd42e3 100644
--- a/clang/include/clang/Sema/Sema.h
+++ b/clang/include/clang/Sema/Sema.h
@@ -8495,10 +8495,12 @@ class Sema final : public SemaBase {
                                 bool Diagnose = true);
   FunctionDecl *FindUsualDeallocationFunction(SourceLocation StartLoc,
                                               ImplicitDeallocationParameters,
-                                              DeclarationName Name);
-  FunctionDecl *FindDeallocationFunctionForDestructor(SourceLocation StartLoc,
-                                                      CXXRecordDecl *RD,
-                                                      bool Diagnose = true);
+                                              DeclarationName Name,
+                                              bool Diagnose = true);
+  FunctionDecl *
+  FindDeallocationFunctionForDestructor(SourceLocation StartLoc,
+                                        CXXRecordDecl *RD, bool Diagnose = true,
+                                        bool LookForGlobal = false);
 
   /// ActOnCXXDelete - Parsed a C++ 'delete' expression (C++ 5.3.5), as in:
   /// @code ::delete ptr; @endcode
diff --git a/clang/lib/CodeGen/CGClass.cpp b/clang/lib/CodeGen/CGClass.cpp
index befbfc64a680c..360876244cad9 100644
--- a/clang/lib/CodeGen/CGClass.cpp
+++ b/clang/lib/CodeGen/CGClass.cpp
@@ -1589,25 +1589,74 @@ namespace {
   void EmitConditionalDtorDeleteCall(CodeGenFunction &CGF,
                                      llvm::Value *ShouldDeleteCondition,
                                      bool ReturnAfterDelete) {
+    const CXXDestructorDecl *Dtor = cast<CXXDestructorDecl>(CGF.CurCodeDecl);
+    const CXXRecordDecl *ClassDecl = Dtor->getParent();
+    const FunctionDecl *OD = Dtor->getOperatorDelete();
+    assert(OD->isDestroyingOperatorDelete() == ReturnAfterDelete &&
+           "unexpected value for ReturnAfterDelete");
+    auto *CondTy = cast<llvm::IntegerType>(ShouldDeleteCondition->getType());
+    if (OD->isDestroyingOperatorDelete()) {
+      llvm::BasicBlock *CallDtor = CGF.createBasicBlock("dtor.call_dtor");
+      llvm::BasicBlock *DontCallDtor = CGF.createBasicBlock("dtor.entry_cont");
+      // Third bit set signals that global operator delete is called. That means
+      // despite class having destroying operator delete which is responsible
+      // for calling dtor, we need to call dtor because global operator delete
+      // won't do that.
+      llvm::Value *Check3rdBit = CGF.Builder.CreateAnd(
+          ShouldDeleteCondition, llvm::ConstantInt::get(CondTy, 4));
+      llvm::Value *ShouldCallDtor = CGF.Builder.CreateIsNull(Check3rdBit);
+      CGF.Builder.CreateCondBr(ShouldCallDtor, DontCallDtor, CallDtor);
+      CGF.EmitBlock(CallDtor);
+      QualType ThisTy = Dtor->getFunctionObjectParameterType();
+      CGF.EmitCXXDestructorCall(Dtor, Dtor_Complete, /*ForVirtualBase=*/false,
+                                /*Delegating=*/false, CGF.LoadCXXThisAddress(),
+                                ThisTy);
+      CGF.Builder.CreateBr(DontCallDtor);
+      CGF.EmitBlock(DontCallDtor);
+    }
     llvm::BasicBlock *callDeleteBB = CGF.createBasicBlock("dtor.call_delete");
     llvm::BasicBlock *continueBB = CGF.createBasicBlock("dtor.continue");
-    llvm::Value *ShouldCallDelete
-      = CGF.Builder.CreateIsNull(ShouldDeleteCondition);
+    // First bit set signals that operator delete must be called.
+    llvm::Value *Check1stBit = CGF.Builder.CreateAnd(
+        ShouldDeleteCondition, llvm::ConstantInt::get(CondTy, 1));
+    llvm::Value *ShouldCallDelete = CGF.Builder.CreateIsNull(Check1stBit);
     CGF.Builder.CreateCondBr(ShouldCallDelete, continueBB, callDeleteBB);
 
     CGF.EmitBlock(callDeleteBB);
-    const CXXDestructorDecl *Dtor = cast<CXXDestructorDecl>(CGF.CurCodeDecl);
-    const CXXRecordDecl *ClassDecl = Dtor->getParent();
-    CGF.EmitDeleteCall(Dtor->getOperatorDelete(),
-                       LoadThisForDtorDelete(CGF, Dtor),
-                       CGF.getContext().getTagDeclType(ClassDecl));
-    assert(Dtor->getOperatorDelete()->isDestroyingOperatorDelete() ==
-               ReturnAfterDelete &&
-           "unexpected value for ReturnAfterDelete");
-    if (ReturnAfterDelete)
-      CGF.EmitBranchThroughCleanup(CGF.ReturnBlock);
-    else
-      CGF.Builder.CreateBr(continueBB);
+    auto EmitDeleteAndGoToEnd = [&](const FunctionDecl *DeleteOp) {
+      CGF.EmitDeleteCall(DeleteOp, LoadThisForDtorDelete(CGF, Dtor),
+                         CGF.getContext().getTagDeclType(ClassDecl));
+      if (ReturnAfterDelete)
+        CGF.EmitBranchThroughCleanup(CGF.ReturnBlock);
+      else
+        CGF.Builder.CreateBr(continueBB);
+    };
+    // If Sema only found a global operator delete previously, the dtor can
+    // always call it. Otherwise we need to check the third bit and call the
+    // appropriate operator delete, i.e. global or class-specific.
+    if (isa<CXXMethodDecl>(OD)) {
+      // Third bit set signals that global operator delete is called, i.e.
+      // ::delete appears on the callsite.
+      llvm::Value *CheckTheBitForGlobDeleteCall = CGF.Builder.CreateAnd(
+          ShouldDeleteCondition, llvm::ConstantInt::get(CondTy, 4));
+      llvm::Value *ShouldCallGlobDelete =
+          CGF.Builder.CreateIsNull(CheckTheBitForGlobDeleteCall);
+      llvm::BasicBlock *GlobDelete =
+          CGF.createBasicBlock("dtor.call_glob_delete");
+      llvm::BasicBlock *ClassDelete =
+          CGF.createBasicBlock("dtor.call_class_delete");
+      CGF.Builder.CreateCondBr(ShouldCallGlobDelete, ClassDelete, GlobDelete);
+      CGF.EmitBlock(GlobDelete);
+
+      const FunctionDecl *GlobOD = Dtor->getOperatorGlobalDelete();
+      if (GlobOD)
+        EmitDeleteAndGoToEnd(GlobOD);
+      else
+        CGF.Builder.CreateUnreachable();
+
+      CGF.EmitBlock(ClassDelete);
+    }
+    EmitDeleteAndGoToEnd(OD);
 
     CGF.EmitBlock(continueBB);
   }
diff --git a/clang/lib/CodeGen/MicrosoftCXXABI.cpp b/clang/lib/CodeGen/MicrosoftCXXABI.cpp
index aa9a55ae05927..4128b7306687a 100644
--- a/clang/lib/CodeGen/MicrosoftCXXABI.cpp
+++ b/clang/lib/CodeGen/MicrosoftCXXABI.cpp
@@ -893,13 +893,8 @@ void MicrosoftCXXABI::emitVirtualObjectDelete(CodeGenFunction &CGF,
                                               QualType ElementType,
                                               const CXXDestructorDecl *Dtor) {
   // FIXME: Provide a source location here even though there's no
-  // CXXMemberCallExpr for dtor call.
-  bool UseGlobalDelete = DE->isGlobalDelete();
-  CXXDtorType DtorType = UseGlobalDelete ? Dtor_Complete : Dtor_Deleting;
-  llvm::Value *MDThis = EmitVirtualDestructorCall(CGF, Dtor, DtorType, Ptr, DE,
-                                                  /*CallOrInvoke=*/nullptr);
-  if (UseGlobalDelete)
-    CGF.EmitDeleteCall(DE->getOperatorDelete(), MDThis, ElementType);
+  EmitVirtualDestructorCall(CGF, Dtor, Dtor_Deleting, Ptr, DE,
+                            /*CallOrInvoke=*/nullptr);
 }
 
 void MicrosoftCXXABI::emitRethrow(CodeGenFunction &CGF, bool isNoReturn) {
@@ -2016,7 +2011,7 @@ llvm::Value *MicrosoftCXXABI::EmitVirtualDestructorCall(
   ASTContext &Context = getContext();
   llvm::Value *ImplicitParam = llvm::ConstantInt::get(
       llvm::IntegerType::getInt32Ty(CGF.getLLVMContext()),
-      DtorType == Dtor_Deleting);
+      (DtorType == Dtor_Deleting) | 4 * (D && D->isGlobalDelete()));
 
   QualType ThisTy;
   if (CE) {
diff --git a/clang/lib/Sema/SemaDeclCXX.cpp b/clang/lib/Sema/SemaDeclCXX.cpp
index cbccb567e2adf..258d78c2c9425 100644
--- a/clang/lib/Sema/SemaDeclCXX.cpp
+++ b/clang/lib/Sema/SemaDeclCXX.cpp
@@ -11400,6 +11400,21 @@ bool Sema::CheckDestructor(CXXDestructorDecl *Destructor) {
       DiagnoseUseOfDecl(OperatorDelete, Loc);
       MarkFunctionReferenced(Loc, OperatorDelete);
       Destructor->setOperatorDelete(OperatorDelete, ThisArg);
+
+      if (isa<CXXMethodDecl>(OperatorDelete) &&
+          Context.getTargetInfo().getCXXABI().isMicrosoft()) {
+        // In Microsoft ABI whenever a class has a defined operator delete,
+        // scalar deleting destructors check the 3rd bit of the implicit
+        // parameter and if it is set, then, global operator delete must be
+        // called instead of class-specific one. Find and save global operator
+        // for that case. Do not diagnose because even if we fail to find the
+        // operator, it won't be possible to compile and execute the code that
+        // requires it.
+        FunctionDecl *GlobalOperatorDelete =
+            FindDeallocationFunctionForDestructor(Loc, RD, /*Diagnose*/ false,
+                                                  /*LookForGlobal*/ true);
+        Destructor->setOperatorGlobalDelete(GlobalOperatorDelete);
+      }
     }
   }
 
diff --git a/clang/lib/Sema/SemaExprCXX.cpp b/clang/lib/Sema/SemaExprCXX.cpp
index b2a982e953012..e6b4a09db9d62 100644
--- a/clang/lib/Sema/SemaExprCXX.cpp
+++ b/clang/lib/Sema/SemaExprCXX.cpp
@@ -3565,7 +3565,7 @@ void Sema::DeclareGlobalAllocationFunction(DeclarationName Name,
 FunctionDecl *
 Sema::FindUsualDeallocationFunction(SourceLocation StartLoc,
                                     ImplicitDeallocationParameters IDP,
-                                    DeclarationName Name) {
+                                    DeclarationName Name, bool Diagnose) {
   DeclareGlobalNewDelete();
 
   LookupResult FoundDelete(*this, Name, StartLoc, LookupOrdinaryName);
@@ -3580,7 +3580,7 @@ Sema::FindUsualDeallocationFunction(SourceLocation StartLoc,
   if (!Result)
     return nullptr;
 
-  if (CheckDeleteOperator(*this, StartLoc, StartLoc, /*Diagnose=*/true,
+  if (CheckDeleteOperator(*this, StartLoc, StartLoc, Diagnose,
                           FoundDelete.getNamingClass(), Result.Found,
                           Result.FD))
     return nullptr;
@@ -3591,7 +3591,8 @@ Sema::FindUsualDeallocationFunction(SourceLocation StartLoc,
 
 FunctionDecl *Sema::FindDeallocationFunctionForDestructor(SourceLocation Loc,
                                                           CXXRecordDecl *RD,
-                                                          bool Diagnose) {
+                                                          bool Diagnose,
+                                                          bool LookForGlobal) {
   DeclarationName Name = Context.DeclarationNames.getCXXOperatorName(OO_Delete);
 
   FunctionDecl *OperatorDelete = nullptr;
@@ -3600,18 +3601,20 @@ FunctionDecl *Sema::FindDeallocationFunctionForDestructor(SourceLocation Loc,
       DeallocType, ShouldUseTypeAwareOperatorNewOrDelete(),
       AlignedAllocationMode::No, SizedDeallocationMode::No};
 
-  if (FindDeallocationFunction(Loc, RD, Name, OperatorDelete, IDP, Diagnose))
-    return nullptr;
+  if (!LookForGlobal) {
+    if (FindDeallocationFunction(Loc, RD, Name, OperatorDelete, IDP, Diagnose))
+      return nullptr;
 
-  if (OperatorDelete)
-    return OperatorDelete;
+    if (OperatorDelete)
+      return OperatorDelete;
+  }
 
   // If there's no class-specific operator delete, look up the global
   // non-array delete.
   IDP.PassAlignment = alignedAllocationModeFromBool(
       hasNewExtendedAlignment(*this, DeallocType));
   IDP.PassSize = SizedDeallocationMode::Yes;
-  return FindUsualDeallocationFunction(Loc, IDP, Name);
+  return FindUsualDeallocationFunction(Loc, IDP, Name, Diagnose);
 }
 
 bool Sema::FindDeallocationFunction(SourceLocation StartLoc, CXXRecordDecl *RD,
diff --git a/clang/test/CodeGenCXX/cxx2a-destroying-delete.cpp b/clang/test/CodeGenCXX/cxx2a-destroying-delete.cpp
index f6f4a2ff735cc..8af2b42e98e7b 100644
--- a/clang/test/CodeGenCXX/cxx2a-destroying-delete.cpp
+++ b/clang/test/CodeGenCXX/cxx2a-destroying-delete.cpp
@@ -159,20 +159,43 @@ H::~H() { call_in_dtor(); }
 // CHECK-ITANIUM-NOT: call
 // CHECK-ITANIUM: }
 
-// CHECK-MSABI64-LABEL: define {{.*}} @"??_GH@@UEAAPEAXI@Z"(
+// CHECK-MSABI64-LABEL: define {{.*}} @"??_GH@@UEAAPEAXI@Z"({{.*}},
 // CHECK-MSABI32-LABEL: define {{.*}} @"??_GH@@UAEPAXI@Z"(
+// CHECK-MSABI-SAME: i32 noundef %[[IP:.*]])
 // CHECK-MSABI-NOT: call{{ }}
-// CHECK-MSABI: load i32
-// CHECK-MSABI: icmp eq i32 {{.*}}, 0
-// CHECK-MSABI: br i1
+// CHECK-MSABI: store i32 %[[IP]], ptr %[[IP_ALLOCA:.*]]
+// CHECK-MSABI: %[[IMP_PARAM:.*]] = load i32, ptr %[[IP_ALLOCA]]
+// CHECK-MSABI-NEXT: %[[THIRDBIT:.*]] = and i32 %[[IMP_PARAM]], 4
+// CHECK-MSABI-NEXT: %[[CHCK:.*]] = icmp eq i32 %[[THIRDBIT]], 0
+// CHECK-MSABI-NEXT: br i1 %[[CHCK]], label %dtor.entry_cont, label %dtor.call_dtor
+//
+// CHECK_MSABI-LABEL: dtor.call_dtor:
+// CHECK_MSABI-NEXT: call void @"??1H@@UEAA@XZ"({{.*}})
+// CHECK_MSABI-NEXT: br label %dtor.entry_cont
+//
+// CHECK_MSABI-LABEL: dtor.entry_cont:
+// CHECK_MSABI-NEXT: %[[FIRSTBIT:.*]] = and i32 %[[IMP_PARAM]], 1
+// CHECK_MSABI-NEXT: %[[CHCK1:.*]] = icmp eq i32 %[[FIRSTBIT]], 0
+// CHECK_MSABI-NEXT: br i1 %[[CHCK1]], label %dtor.continue, label %dtor.call_delete
+//
+// CHECK_MSABI-LABEL: dtor.call_delete:
+// CHECK-MSABI: %[[THIRDBIT1:.*]] = and i32 %[[IMP_PARAM]], 4
+// CHECK-MSABI-NEXT: %[[CHCK2:.*]] = icmp eq i32 %[[THIRDBIT1]], 0
+// CHECK-MSABI-NEXT: br i1 %[[CHCK2]], label %dtor.call_class_delete, label %dtor.call_glob_delete
+//
+// CHECK-MSABI-LABEL: dtor.call_glob_delete:
+// CHECK-MSABI64: call void @"??3@YAXPEAX_K@Z"(ptr noundef %{{.*}}, i64 noundef 48)
+// CHECK-MSABI32: call void @"??3@YAXPAXIW4align_val_t@std@@@Z"(ptr noundef %{{.*}}, i32 noundef 32, i32 noundef 16)
+// CHECK-MSABI-NEXT: br label %[[RETURN:.*]]
 //
+// CHECK-MSABI: dtor.call_class_delete:
 // CHECK-MSABI-NOT: call{{ }}
 // CHECK-MSABI64: getelementptr {{.*}}, i64 24
 // CHECK-MSABI32: getelementptr {{.*}}, i32 20
 // CHECK-MSABI-NOT: call{{ }}
 // CHECK-MSABI64: call void @"??3F@@SAXPEAU0@Udestroying_delete_t@std@@_KW4align_val_t@2@@Z"({{.*}}, i64 noundef 48, i64 noundef 16)
 // CHECK-MSABI32: call void @"??3F@@SAXPAU0@Udestroying_delete_t@std@@IW4align_val_t@2@@Z"({{.*}}, i32 noundef 32, i32 noundef 16)
-// CHECK-MSABI: br label %[[RETURN:.*]]
+// CHECK-MSABI: br label %[[RETURN:]]
 //
 // CHECK-MSABI64: call void @"??1H@@UEAA@XZ"(
 // CHECK-MSABI32: call x86_thiscallcc void @"??1H@@UAE@XZ"(
@@ -194,9 +217,31 @@ I::~I() { call_in_dtor(); }
 // CHECK-MSABI32-LABEL: define {{.*}} @"??_GI@@UAEPAXI@Z"(
 // CHECK-MSABI-NOT: call{{ }}
 // CHECK-MSABI: load i32
-// CHECK-MSABI: icmp eq i32 {{.*}}, 0
-// CHECK-MSABI: br i1
+// CHECK-MSABI-NEXT: and i32 %[[IMP_PARAM:.*]], 4
+// CHECK-MSABI-NEXT: icmp eq i32 {{.*}}, 0
+// CHECK-MSABI-NEXT: br i1 %[[CHCK]], label %dtor.entry_cont, label %dtor.call_dtor
+//
+// CHECK-MSABI: dtor.call_dtor:
+// CHECK-MSABI64-NEXT: call void @"??1I@@UEAA@XZ"({{.*}})
+// CHECK-MSABI32-NEXT: call x86_thiscallcc void @"??1I@@UAE@XZ"({{.*}})
+// CHECK-MSABI-NEXT: br label %dtor.entry_cont
+//
+// CHECK-MSABI: dtor.entry_cont:
+// CHECK-MSABI-NEXT: and i32 %[[IMP_PARAM]], 1
+// CHECK-MSABI-NEXT: icmp eq i32 %{{.*}}, 0
+// CHECK-MSABI-NEXT: br i1 %{{.*}}, label %dtor.continue, label %dtor.call_delete
+//
+// CHECK-MSABI: dtor.call_delete:
+// CHECK-MSABI-NEXT: %[[THIRDBIT1:.*]] = and i32 %[[IMP_PARAM]], 4
+// CHECK-MSABI-NEXT: %[[CHCK2:.*]] = icmp eq i32 %[[THIRDBIT1]], 0
+// CHECK-MSABI-NEXT: br i1 %[[CHCK2]], label %dtor.call_class_delete, label %dtor.call_glob_delete
+//
+// CHECK-MSABI-LABEL: dtor.call_glob_delete:
+// CHECK-MSABI64: call void @"??3@YAXPEAX_KW4align_val_t@std@@@Z"(ptr noundef %{{.*}}, i64 noundef 96, i64 noundef 32)
+// CHECK-MSABI32: call void @"??3@YAXPAXIW4align_val_t@std@@@Z"(ptr noundef %{{.*}}, i32 noundef 64, i32 noundef 32)
+// CHECK-MSABI-NEXT: br label %[[RETURN:.*]]
 //
+// CHECK_MSABI: dtor.call_class_delete:
 // CHECK-MSABI-NOT: call{{ }}
 // CHECK-MSABI64: getelementptr {{.*}}, i64 24
 // CHECK-MSABI32: getelementptr {{.*}}, i32 20
diff --git a/clang/test/CodeGenCXX/microsoft-abi-structors.cpp b/clang/test/CodeGenCXX/microsoft-abi-structors.cpp
index 07abc3d065e5e..9b43483439cf8 100644
--- a/clang/test/CodeGenCXX/microsoft-abi-structors.cpp
+++ b/clang/test/CodeGenCXX/microsoft-abi-structors.cpp
@@ -52,7 +52,8 @@ struct C {
 // DTORS:        store ptr %{{.*}}, ptr %[[RETVAL:retval]]
 // DTORS:        %[[SHOULD_DELETE_VALUE:[0-9a-z._]+]] = load i32, ptr %[[SHOULD_DELETE_VAR]]
 // DTORS:        call x86_thiscallcc void @"??1C@basic@@UAE@XZ"(ptr {{[^,]*}} %[[THIS:[0-9a-z]+]])
-// DTORS-NEXT:   %[[CONDITION:[0-9]+]] = icmp eq i32 %[[SHOULD_DELETE_VALUE]], 0
+// DTORS-NEXT:   %[[AND:[0-9]+]] = and i32 %[[SHOULD_DELETE_VALUE]], 1
+// DTORS-NEXT:   %[[CONDITION:[0-9]+]] = icmp eq i32 %[[AND]], 0
 // DTORS-NEXT:   br i1 %[[CONDITION]], label %[[CONTINUE_LABEL:[0-9a-z._]+]], label %[[CALL_DELETE_LABEL:[0-9a-z._]+]]
 //
 // DTORS:      [[CALL_DELETE_LABEL]]
@@ -113,8 +114,7 @@ void call_deleting_dtor_and_global_delete(C *obj_ptr) {
 // CHECK-NEXT:   %[[VTABLE:.*]] = load ptr, ptr %[[OBJ_PTR_VALUE]]
 // CHECK-NEXT:   %[[PVDTOR:.*]] = getelementptr inbounds ptr, ptr %[[VTABLE]], i64 0
 // CHECK-NEXT:   %[[VDTOR:.*]] = load ptr, ptr %[[PVDTOR]]
-// CHECK-NEXT:   %[[CALL:.*]] = call x86_thiscallcc ptr %[[VDTOR]](ptr {{[^,]*}} %[[OBJ_PTR_VALUE]], i32 0)
-// CHECK-NEXT:   call void @"??3@YAXPAX@Z"(ptr %[[CALL]])
+// CHECK-NEXT:   %[[CALL:.*]] = call x86_thiscallcc ptr %[[VDTOR]](ptr {{[^,]*}} %[[OBJ_PTR_VALUE]], i32 5)
 // CHECK:      ret void
 }
 
@@ -458,3 +458,41 @@ class G {
 extern void testG() {
   G g;
 }
+
+namespace operator_delete {
+
+class H { virtual ~H();
+  void operator delete(void *);
+};
+H::~H() { }
+
+void checkH() {
+  new H();
+}
+// DTORS:      define linkonce_odr dso_local x86_thiscallcc ptr @"??_GH@operator_delete@@EAEPAXI@Z"(ptr {{[^,]*}} %this, i32 %should_call_delete) {{.*}} comdat {{.*}} {
+// DTORS:        store i32 %should_call_delete, ptr %[[SHOULD_DELETE_VAR:[0-9a-z._]+]], align 4
+// DTORS:        store ptr %{{.*}}, ptr %[[RETVAL:retval]]
+// DTORS:        %[[SHOULD_DELETE_VALUE:[0-9a-z._]+]] = load i32, ptr %[[SHOULD_DELETE_VAR]]
+// DTORS:        call x86_thiscallcc void @"??1H@operator_delete@@EAE@XZ"(ptr {{[^,]*}} %[[THIS:[0-9a-z]+]])
+// DTORS-NEXT:   %[[AND:[0-9]+]] = and i32 %[[SHOULD_DELETE_VALUE]], 1
+// DTORS-NEXT:   %[[CONDITION:[0-9]+]] = icmp eq i32 %[[AND]], 0
+// DTORS-NEXT:   br i1 %[[CONDITION]], label %[[CONTINUE_LABEL:[0-9a-z._]+]], label %[[CALL_DELETE_LABEL:[0-9a-z._]+]]
+//
+// DTORS:      [[CALL_DELETE_LABEL]]
+// DTORS-NEXT:   %[[AND:[0-9]+]] = and i32 %[[SHOULD_DELETE_VALUE]], 4
+// DTORS-NEXT:   %[[CONDITION1:[0-9]+]] = icmp eq i32 %[[AND]], 0
+// 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:   br label %[[CONTINUE_LABEL]]
+//
+// DTORS:      [[CALL_CLASS_DELETE]]
+// DTORS-NEXT:   call void @"??3H@operator_delete@@CAXPAX@Z"(ptr %[[THIS]])
+// DTORS-NEXT:   br label %[[CONTINUE_LABEL]]
+//
+// DTORS:      [[CONTINUE_LABEL]]
+// DTORS-NEXT:   %[[RET:.*]] = load ptr, ptr %[[RETVAL]]
+// DTORS-NEXT:   ret ptr %[[RET]]
+
+}

@Fznamznon
Copy link
Contributor Author

Ping.

@Fznamznon
Copy link
Contributor Author

ping.

@Fznamznon Fznamznon requested a review from efriedma-quic June 11, 2025 16:51
Copy link
Collaborator

@efriedma-quic efriedma-quic left a comment

Choose a reason for hiding this comment

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

LGTM with one minor comment

@Fznamznon
Copy link
Contributor Author

Thanks for review @efriedma-quic .
I've been thinking though, that this change is kind of ABI breaking.
Example: if library A was compiled with clang 20, library B compiled with clang 21 (or any release where this patch ends up, I don't really understand current llvm release model). class B is implemented in library A, so its destructor doesn't call operator ::delete, then an object of class B is deleted via ::delete in library B, so the callsite doesn't call ::delete because of this patch. So there will be no ::delete call at all. I do realize that is probably a corner case limited to windows, but still, should that go through breaking changes process?
cc @AaronBallman

@AaronBallman
Copy link
Collaborator

Thanks for review @efriedma-quic . I've been thinking though, that this change is kind of ABI breaking. Example: if library A was compiled with clang 20, library B compiled with clang 21 (or any release where this patch ends up, I don't really understand current llvm release model). class B is implemented in library A, so its destructor doesn't call operator ::delete, then an object of class B is deleted via ::delete in library B, so the callsite doesn't call ::delete because of this patch. So there will be no ::delete call at all. I do realize that is probably a corner case limited to windows, but still, should that go through breaking changes process? cc @AaronBallman

I think this is a case where we'd call out the breaking change in the release notes and if anyone needed the older ABI, we'd use ABI version tags for it at that point.

CC @llvm/clang-vendors for awareness for the potentially breaking changes.

@Fznamznon
Copy link
Contributor Author

Ok, I added the release note.

we'd use ABI version tags for it at that point.

@AaronBallman Could you please elaborate a bit more on that?

@AaronBallman
Copy link
Collaborator

Ok, I added the release note.

we'd use ABI version tags for it at that point.

@AaronBallman Could you please elaborate a bit more on that?

We have a command line option, -fclang-abi-compat=, which lets the user say "I want to be ABI compatible with Clang version X". In code, we have LangOptions::ClangABICompat which tracks that information so we can either go down the "old path" or the "new path" as needed, like in:

Context.getLangOpts().getClangABICompat() >

@Fznamznon
Copy link
Contributor Author

Thanks @AaronBallman , It seems -fclang-abi-compat=20 doesn't work, so first we need #144109 .

Copy link
Contributor

@tahonermann tahonermann left a comment

Choose a reason for hiding this comment

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

Thanks for working on this @Fznamznon! I added some suggestions and comments, most of which are minor. There are two concerns that must be addressed.

  • The ABI impact suggests this change should be tied to -fclang-abi-compat= as has already been discussed.
  • AST serialization support is needed for the new OperatorGlobalDelete member of CXXDestructorDecl.

Comment on lines 2896 to 2897
if (OD && !First->OperatorGlobalDelete)
First->OperatorGlobalDelete = OD;
Copy link
Contributor

Choose a reason for hiding this comment

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

I think a call to setOperatorGlobalDelete(nullptr) should clear OperatorGlobalDelete.

Hmm, setOperatorDelete() silently ignores a call that passes nullptr. That seems wrong. Maybe there should be a non-null assertion?

setOperatorDelete() also registers the delete operator with ASTMutationListener::ResolvedOperatorDelete() and ASTWriter overrides that function. That seems important for PCH and module support. I think updates are needed to clang/lib/Serialization/ASTReaderDecl.cpp as well to restore OperatorGlobalDelete during AST deserialization; see ASTDeclReader::VisitCXXDestructorDecl() and ASTDeclReader::UpdateDecl() for assignments to OperatorDelete.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hmm, what would be the better approach to implement this? Should I invent ResolvedOperatorDelete but for global operator delete or can we tie global operator delete to a regular one like we do with ThisArg in setOperatorDelete() and other serialization parts?
cc @ChuanqiXu9

Copy link
Contributor

Choose a reason for hiding this comment

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

Good question. Adding a parallel set of functions seems reasonable to me. It might also be reasonable to tie the updates together, but I think that might unnecessarily increase the serialized AST size for Itanium targets. Perhaps that can be avoided in either case; it isn't obvious to me.

While looking at this some more, I noticed that it looks like updates will be needed to clang/lib/AST/ASTImporter.cpp as well; see the call to setOperatorDelete() in ASTNodeImporter::VisitFunctionDecl().

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@tahonermann , added AST serialization support along with tests.

//
// CHECK_MSABI: dtor.call_class_delete:
Copy link
Contributor

Choose a reason for hiding this comment

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

I think the additions here look good, but we're missing a test for a call to ::delete for a class that declares a destroying operator delete. Such a test is interesting for both the Itanium and MSVC ABIs for different reasons (the complete object destructor is called followed by global operator delete for Itanium; the deleting destructor is called and it calls the complete destructor and then the global operator delete for MSVC).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added the test. I noticed though that MSVC always calls scalar deleting dtor even when class declares non-virtual destructor. clang doesn't even generate deleting destructors when class destructor is not virtual. I'm not yet sure which implications it has, need more experimenting. At least unlike the virtual case the wrong operator delete is not called.

Copy link
Contributor

Choose a reason for hiding this comment

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

Hmm, I'm not seeing that with this simple example (https://godbolt.org/z/xdThaEb78). g() calls the complete destructor followed by operator delete.

struct S {
  ~S();
};
S* f() {
  return new S;
}
void g(S *p) {
  ::delete p;
}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Nice, thanks. I probably hallucinated. I wasn't able to break this case while testing object files built with clang and msvc either.

Copy link

github-actions bot commented Jun 18, 2025

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

@Fznamznon Fznamznon requested a review from tahonermann June 23, 2025 13:10
Copy link
Contributor

@tahonermann tahonermann left a comment

Choose a reason for hiding this comment

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

A few more comments. I'm still digesting the changes to EmitConditionalDtorDeleteCall(). If I'm reading the code correctly, CodeGenFunction::EmitDestructorBody() will already emit a call to the complete destructor unconditionally (assuming HaveInsertPoint() returns true) after the call to EnterDtorCleanups() (which calls EmitConditionalDtorDeleteCall()). I could easily be misreading things though; I might have to hop into a debugger. At a minimum, I'll have some additional suggestions for comments intended to make things more clear.

Comment on lines 3105 to 3106
assert(!OD || (OD->getDeclName().getCXXOverloadedOperator() == OO_Delete &&
OD->isUsableAsGlobalAllocationFunctionInConstantEvaluation()));
Copy link
Contributor

Choose a reason for hiding this comment

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

Per previous discussion, I don't think isUsableAsGlobalAllocationFunctionInConstantEvaluation() performs quite the right set of checks. It is really close though. That function will return true for a delete operator that has a trailing std::nothrow_t parameter; such declarations are not included in the usual deallocation function requirements (and ::delete will not call such a function; it won't manifest a std::nothrow_t argument).

I suggest we assert that the function is a delete operator declared at global scope and add a FIXME comment regarding the usual deallocation function requirement.

Suggested change
assert(!OD || (OD->getDeclName().getCXXOverloadedOperator() == OO_Delete &&
OD->isUsableAsGlobalAllocationFunctionInConstantEvaluation()));
// FIXME: C++23 [expr.delete] specifies that the delete operator will be
// a usual deallocation function declared at global scope. A convenient
// function to assert that is lacking; Sema::isUsualDeallocationFunction()
// only works for CXXMethodDecl.
assert(!OD || (OD->getDeclName().getCXXOverloadedOperator() == OO_Delete &&
OD->getDeclContext()->getRedeclContext()->isTranslationUnit()));

Copy link
Contributor Author

Choose a reason for hiding this comment

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

ok, done.

Comment on lines 1604 to 1607
// Clang 20 calls global operator delete after dtor call. Clang 21 and newer
// call global operator delete inside of dtor body, as MSVC does.
bool Clang21AndNewer = CGF.getContext().getLangOpts().getClangABICompat() >
LangOptions::ClangABI::Ver20;
Copy link
Contributor

Choose a reason for hiding this comment

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

The ABI check occurs in multiple places. Can this check be encapsulated in a common function somewhere? Perhaps a TargetInfo member function named callGlobalDeleteInDestroyingDtor() or similar? There is precedent in terms of TargetInfo::getCallingConvKind() and TargetInfo::areDefaultedSMFStillPOD(). Or perhaps an ABIInfo member function? There is precedent in terms of ARMABIInfo::shouldIgnoreEmptyArg(), X86_64ABIInfo::classifyIntegerMMXAsSSE(), X86_64ABIInfo::passInt128VectorsInMem(), and X86_64ABIInfo::returnCXXRecordGreaterThan128InMem().

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ok, I added a member to TargetInfo which seems to be easily available in both Sema and CodeGen. Thanks.

//
// CHECK_MSABI: dtor.call_class_delete:
Copy link
Contributor

Choose a reason for hiding this comment

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

Hmm, I'm not seeing that with this simple example (https://godbolt.org/z/xdThaEb78). g() calls the complete destructor followed by operator delete.

struct S {
  ~S();
};
S* f() {
  return new S;
}
void g(S *p) {
  ::delete p;
}

@Fznamznon
Copy link
Contributor Author

If I'm reading the code correctly, CodeGenFunction::EmitDestructorBody() will already emit a call to the complete destructor unconditionally (assuming HaveInsertPoint() returns true) after the call to EnterDtorCleanups() (which calls EmitConditionalDtorDeleteCall()). I could easily be misreading things though; I might have to hop into a debugger. At a minimum, I'll have some additional suggestions for comments intended to make things more clear.

@tahonermann , yes, at some basic block there will be always a call to complete destructor inside of scalar deleting dtor body. The difference is whether the class has a destroying operator delete or not. If it does, then the delete operator is responsible for calling complete dtor. So we first call EmitConditionalDtorDeleteCall() and then we emit the complete dtor call. EmitConditionalDtorDeleteCall() works in a way that the insertion point will be at the basic block that only will be entered if delete operator call was not requested via an implicit flag, i.e. it is 0 or its first bit is not set. But note that if the class doesn't have destroying operator delete, EmitConditionalDtorDeleteCall() is pushed to a cleanups stack instead:

EHStack.pushCleanup<CallDtorDeleteConditional>(

Which actually causes it to be called after CodeGenFunction::EmitDestructorBody() which in that case will insert call to complete dtor into the beginning of the scalar deleting dtor body.

@Fznamznon Fznamznon requested a review from tahonermann June 26, 2025 13:38
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:frontend Language frontend issues, e.g. anything involving "Sema" clang Clang issues not falling into any other category
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants