https://github.com/Maoyao233 updated https://github.com/llvm/llvm-project/pull/235673
>From d3dc5b27fac82821a2454558f6ec5b3e8eaa26bc Mon Sep 17 00:00:00 2001 From: Maoyao Ye <[email protected]> Date: Mon, 26 Oct 2026 17:10:34 +0800 Subject: [PATCH 1/2] [Clang][modules] Don't drop resolved operator delete updates in reduced BMIs On a Microsoft ABI target, a module interface unit that instantiates a class whose virtual destructor is declared but not defined in that module makes an importing TU crash while generating code: CodeGenFunction::EnterDtorCleanups() finds a null CXXDestructorDecl::getOperatorDelete(), which is an assertion failure in an assertions build and a null pointer dereference otherwise. In the Microsoft ABI the deleting destructor is emitted together with the vtable, so Sema::MarkVTableUsed() calls CheckDestructor() for a user-declared, not-yet-defined virtual destructor and that resolves the destructor's 'operator delete'. When the destructor declaration is owned by another PCM (for example it is part of an 'extern template' specialization), the resolution can only be recorded as a DeclUpdate. Module interface units are compiled as reduced BMIs by default, and ASTWriter::prepareLazyUpdates() keeps every update except CXXAddedFunctionDefinition lazy until the module writes a declaration reference to the updated declaration; if it never does, the update is dropped. A consumer that emits the vtable and deleting destructor itself (there are no key functions in the Microsoft ABI) does not re-resolve 'operator delete' on that codegen path, so it ends up in EnterDtorCleanups() without one. Keep the CXXResolvedDtor* updates eager, like CXXAddedFunctionDefinition: an importer cannot recompute them, but codegen needs them to emit the deleting destructor. Fixes #231859. Assisted by: Deepseek V4.1-flash-1017 --- clang/lib/Serialization/ASTWriter.cpp | 10 ++++ .../msvc-dtor-operator-delete-reduced-bmi.cpp | 46 +++++++++++++++++++ 2 files changed, 56 insertions(+) create mode 100644 clang/test/Modules/msvc-dtor-operator-delete-reduced-bmi.cpp diff --git a/clang/lib/Serialization/ASTWriter.cpp b/clang/lib/Serialization/ASTWriter.cpp index 0d9ec7a460dfbf..35b1a17d015d12 100644 --- a/clang/lib/Serialization/ASTWriter.cpp +++ b/clang/lib/Serialization/ASTWriter.cpp @@ -5731,6 +5731,16 @@ void ASTWriter::prepareLazyUpdates() { if (Kind == DeclUpdateKind::CXXAddedFunctionDefinition) DeclUpdatesTmp[D].push_back( ASTWriter::DeclUpdate(DeclUpdateKind::CXXAddedFunctionDefinition)); + else if (Kind == DeclUpdateKind::CXXResolvedDtorDelete || + Kind == DeclUpdateKind::CXXResolvedDtorGlobDelete || + Kind == DeclUpdateKind::CXXResolvedDtorArrayDelete || + Kind == DeclUpdateKind::CXXResolvedDtorGlobArrayDelete) + // These record the 'operator delete' that Sema resolved for a virtual + // destructor in this TU. An importer cannot recompute it: the + // destructor may already have a body or be defined in another TU. But + // codegen needs it, since in the Microsoft ABI the deleting destructor + // is emitted along with the vtable. Keep them eager. + DeclUpdatesTmp[D].push_back(Update); else DeclUpdatesLazy[D].push_back(Update); } diff --git a/clang/test/Modules/msvc-dtor-operator-delete-reduced-bmi.cpp b/clang/test/Modules/msvc-dtor-operator-delete-reduced-bmi.cpp new file mode 100644 index 00000000000000..4ab4dfa12ceea2 --- /dev/null +++ b/clang/test/Modules/msvc-dtor-operator-delete-reduced-bmi.cpp @@ -0,0 +1,46 @@ +// RUN: rm -rf %t +// RUN: split-file %s %t +// +// RUN: %clang_cc1 -triple x86_64-pc-windows-msvc -std=c++20 %t/a.cppm \ +// RUN: -emit-obj -fmodules-reduced-bmi -fmodule-output=%t/a.pcm -o %t/a.o +// RUN: %clang_cc1 -triple x86_64-pc-windows-msvc -std=c++20 %t/b.cppm \ +// RUN: -emit-obj -fmodules-reduced-bmi -fmodule-output=%t/b.pcm \ +// RUN: -fmodule-file=a=%t/a.pcm -o %t/b.o +// RUN: %clang_cc1 -triple x86_64-pc-windows-msvc -std=c++20 %t/main.cpp \ +// RUN: -emit-obj -fmodule-file=a=%t/a.pcm -fmodule-file=b=%t/b.pcm -o %t/main.o + +// In the Microsoft ABI the deleting destructor is emitted together with the +// vtable, so it needs the 'operator delete' that Sema resolved for the virtual +// destructor. Here 'operator delete' is resolved for a destructor declaration +// that is owned by module 'a' (the 'extern template' declaration puts it in +// a.pcm), so the resolution is recorded as a DeclUpdate on an imported +// declaration. With reduced BMIs such an update used to be kept lazily and +// then dropped, because the module never wrote a declaration reference to the +// destructor; a consumer that emits the vtable then crashed in +// CGClass.cpp's EnterDtorCleanups with a null getOperatorDelete(). + +//--- a.cppm +export module a; + +export template <class T> struct Base { + __attribute__((exclude_from_explicit_instantiation)) Base() {} + __attribute__((exclude_from_explicit_instantiation)) virtual ~Base(); +}; +extern template struct Base<int>; + +//--- b.cppm +export module b; +import a; + +// Instantiating this constructor marks the vtable used (ActOnFinishFunctionBody) +// and Sema then resolves 'operator delete' for the imported destructor. +export void make() { new Base<int>; } + +//--- main.cpp +import a; +import b; + +struct Local : Base<int> {}; +Local l; + +int main() { return 0; } >From ab9d67095bf495da3b161e88eed656a06ea6c3ea Mon Sep 17 00:00:00 2001 From: Maoyao Ye <[email protected]> Date: Mon, 26 Oct 2026 18:36:56 +0800 Subject: [PATCH 2/2] fixup! [Clang][modules] Don't drop resolved operator delete updates in reduced BMIs --- ...reduced-bmi-msvc-dtor-operator-delete.cpp} | 24 ++++++++++++------- 1 file changed, 16 insertions(+), 8 deletions(-) rename clang/test/Modules/{msvc-dtor-operator-delete-reduced-bmi.cpp => reduced-bmi-msvc-dtor-operator-delete.cpp} (62%) diff --git a/clang/test/Modules/msvc-dtor-operator-delete-reduced-bmi.cpp b/clang/test/Modules/reduced-bmi-msvc-dtor-operator-delete.cpp similarity index 62% rename from clang/test/Modules/msvc-dtor-operator-delete-reduced-bmi.cpp rename to clang/test/Modules/reduced-bmi-msvc-dtor-operator-delete.cpp index 4ab4dfa12ceea2..90535ee21c7bb1 100644 --- a/clang/test/Modules/msvc-dtor-operator-delete-reduced-bmi.cpp +++ b/clang/test/Modules/reduced-bmi-msvc-dtor-operator-delete.cpp @@ -1,14 +1,17 @@ +// REQUIRES: x86-registered-target +// // RUN: rm -rf %t // RUN: split-file %s %t // // RUN: %clang_cc1 -triple x86_64-pc-windows-msvc -std=c++20 %t/a.cppm \ -// RUN: -emit-obj -fmodules-reduced-bmi -fmodule-output=%t/a.pcm -o %t/a.o +// RUN: -emit-reduced-module-interface -fmodules-reduced-bmi -o %t/a.pcm // RUN: %clang_cc1 -triple x86_64-pc-windows-msvc -std=c++20 %t/b.cppm \ -// RUN: -emit-obj -fmodules-reduced-bmi -fmodule-output=%t/b.pcm \ -// RUN: -fmodule-file=a=%t/a.pcm -o %t/b.o +// RUN: -emit-reduced-module-interface -fmodules-reduced-bmi \ +// RUN: -fmodule-file=a=%t/a.pcm -o %t/b.pcm // RUN: %clang_cc1 -triple x86_64-pc-windows-msvc -std=c++20 %t/main.cpp \ -// RUN: -emit-obj -fmodule-file=a=%t/a.pcm -fmodule-file=b=%t/b.pcm -o %t/main.o - +// RUN: -fmodule-file=a=%t/a.pcm -fmodule-file=b=%t/b.pcm \ +// RUN: -emit-llvm -o - | FileCheck %s +// // In the Microsoft ABI the deleting destructor is emitted together with the // vtable, so it needs the 'operator delete' that Sema resolved for the virtual // destructor. Here 'operator delete' is resolved for a destructor declaration @@ -17,7 +20,12 @@ // declaration. With reduced BMIs such an update used to be kept lazily and // then dropped, because the module never wrote a declaration reference to the // destructor; a consumer that emits the vtable then crashed in -// CGClass.cpp's EnterDtorCleanups with a null getOperatorDelete(). +// EnterDtorCleanups() in CGClass.cpp with a null getOperatorDelete(). + +// The consumer emits Base<int>'s deleting destructor, which calls the resolved +// global operator delete. +// CHECK-LABEL: define {{.*}}@"??_G?$Base@H@@UEAAPEAXI@Z" +// CHECK: call void @"??3@YAXPEAX_K@Z" //--- a.cppm export module a; @@ -32,8 +40,8 @@ extern template struct Base<int>; export module b; import a; -// Instantiating this constructor marks the vtable used (ActOnFinishFunctionBody) -// and Sema then resolves 'operator delete' for the imported destructor. +// Instantiating this constructor marks the vtable used, and Sema then resolves +// 'operator delete' for the imported destructor (ActOnFinishFunctionBody). export void make() { new Base<int>; } //--- main.cpp _______________________________________________ cfe-commits mailing list [email protected] https://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-commits
