[clang] [clang][WebAssembly] Don't register this-returning destructors of reference temporaries directly with `__cxa_atexit` (PR #226619)
via cfe-commits
cfe-commits at lists.llvm.org
Fri Sep 25 18:28:26 PDT 2026
llvmorg-github-actions[bot] wrote:
<!--LLVM PR SUMMARY COMMENT-->
@llvm/pr-subscribers-clang
Author: RobinTF
<details>
<summary>Changes</summary>
Hi, so this seems to be a long standing issue I stumbled upon that seemed easy to fix (with heavy AI help), so here's my best effort attempt to provide a working fix for it. I'm pretty much convinced that this is the correct fix, but I have no idea how the tests work, so I'm completely trusting the LLM on that one, I'd appreciate any suggestions on that. Thank you!
Under ABIs whose destructors return `this` (ARM, WebAssembly), a destructor can only be registered directly (with `__cxa_atexit` etc.) if the target tolerates the resulting function type mismatch. Clang already checks this for global variables and registers a helper function instead, but temporaries that are lifetime-extended by a reference with static or thread storage duration always had their destructor registered directly. On WebAssembly, which does not tolerate the mismatch, this traps with "function signature mismatch" when the exit-time destructors run. This PR changes the if condition to use the destroy helper that is already used for arrays if the destructor cannot be registered directly.
Previously the following code would result in this:
```cpp
#include <cstdio>
struct B { B() {} ~B() { std::puts("~B"); } };
const B& b = B();
int main() {}
```
```
$ em++ -O2 -sEXIT_RUNTIME=1 repro.cpp -o repro.js && node repro.js
RuntimeError: function signature mismatch
<node stack trace>
```
with this patch applied it simply prints "~B".
Fixes #<!-- -->45221
Assisted-by: Claude Opus 5.5
---
Full diff: https://github.com/llvm/llvm-project/pull/226619.diff
2 Files Affected:
- (modified) clang/lib/CodeGen/CGExpr.cpp (+8-1)
- (modified) clang/test/CodeGenCXX/static-destructor.cpp (+28)
``````````diff
diff --git a/clang/lib/CodeGen/CGExpr.cpp b/clang/lib/CodeGen/CGExpr.cpp
index 4a481c01f6a68b..f61fd0619af6b9 100644
--- a/clang/lib/CodeGen/CGExpr.cpp
+++ b/clang/lib/CodeGen/CGExpr.cpp
@@ -442,9 +442,16 @@ pushTemporaryCleanup(CodeGenFunction &CGF, const MaterializeTemporaryExpr *M,
if (!ReferenceTemporaryDtor)
return;
+ // Like in `EmitDeclDestroy`, destructors that return `this` need a helper
+ // if the target does not tolerate the mismatch (e.g. WebAssembly).
+ bool CanRegisterDestructor =
+ !CGF.CGM.getCXXABI().HasThisReturn(
+ GlobalDecl(ReferenceTemporaryDtor, Dtor_Complete)) ||
+ CGF.CGM.getCXXABI().canCallMismatchedFunctionType();
+
llvm::FunctionCallee CleanupFn;
llvm::Constant *CleanupArg;
- if (E->getType()->isArrayType()) {
+ if (E->getType()->isArrayType() || !CanRegisterDestructor) {
CleanupFn = CodeGenFunction(CGF.CGM).generateDestroyHelper(
ReferenceTemporary, E->getType(), CodeGenFunction::destroyCXXObject,
CGF.getLangOpts().Exceptions,
diff --git a/clang/test/CodeGenCXX/static-destructor.cpp b/clang/test/CodeGenCXX/static-destructor.cpp
index eea5a2e1765535..0bef52601a02fb 100644
--- a/clang/test/CodeGenCXX/static-destructor.cpp
+++ b/clang/test/CodeGenCXX/static-destructor.cpp
@@ -1,6 +1,7 @@
// RUN: %clang_cc1 %s -triple=x86_64-pc-linux -emit-llvm -o - | FileCheck --check-prefix=X86 %s
// RUN: %clang_cc1 %s -triple=wasm32 -emit-llvm -o - | FileCheck --check-prefix=WASM %s
// RUN: %clang_cc1 %s -triple=armv7-apple-darwin9 -emit-llvm -o - | FileCheck --check-prefix=ARM %s
+// RUN: %clang_cc1 %s -triple=wasm32 -emit-llvm -fno-use-cxa-atexit -DTLS -o - | FileCheck --check-prefix=WASM-TLS %s
// Test that destructors are not passed directly to __cxa_atexit when their
// signatures do not match the type of its first argument.
@@ -31,3 +32,30 @@ Foo global;
// WASM: define internal void @__cxx_global_array_dtor(ptr noundef %0)
// WASM: %call = call noundef ptr @_ZN3FooD1Ev(ptr {{[^,]*}} @global)
+
+// The same holds for temporaries that are lifetime-extended by a reference with
+// static storage duration.
+const Foo &global_ref = Foo();
+
+// X86: define internal void @__cxx_global_var_init.1()
+// X86: call i32 @__cxa_atexit(ptr @_ZN3FooD1Ev, ptr @_ZGR10global_ref_, ptr @__dso_handle)
+
+// ARM: define internal void @__cxx_global_var_init.1()
+// ARM: call i32 @__cxa_atexit(ptr @_ZN3FooD1Ev, ptr @_ZGR10global_ref_, ptr @__dso_handle)
+
+// WASM: define internal void @__cxx_global_var_init.1()
+// WASM: call i32 @__cxa_atexit(ptr @[[REF_DTOR:__cxx_global_array_dtor[.0-9]*]], ptr null, ptr @__dso_handle)
+
+// WASM: define internal void @[[REF_DTOR]](ptr noundef %0)
+// WASM: %call = call noundef ptr @_ZN3FooD1Ev(ptr {{[^,]*}} @_ZGR10global_ref_)
+
+// Thread-local ones are registered with `__cxa_thread_atexit`, even with
+// `-fno-use-cxa-atexit`.
+#ifdef TLS
+thread_local const Foo &tls_ref = Foo();
+#endif
+
+// WASM-TLS: call i32 @__cxa_thread_atexit(ptr @[[TLS_DTOR:__cxx_global_array_dtor[.0-9]*]], ptr null, ptr @__dso_handle)
+
+// WASM-TLS: define internal void @[[TLS_DTOR]](ptr noundef %0)
+// WASM-TLS: %call = call noundef ptr @_ZN3FooD1Ev(ptr {{[^,]*}} @_ZGR7tls_ref_)
``````````
</details>
https://github.com/llvm/llvm-project/pull/226619
More information about the cfe-commits
mailing list