https://github.com/RobinTF created
https://github.com/llvm/llvm-project/pull/226619
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
>From d2d10c39f61cd4e6f805b8de3e6842b1992459ce Mon Sep 17 00:00:00 2001
From: Robin Textor-Falconi <[email protected]>
Date: Sat, 26 Sep 2026 03:12:03 +0200
Subject: [PATCH] [clang][WebAssembly] Don't register this-returning dtors of
reference temporaries directly with __cxa_atexit
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. `EmitDeclDestroy` checks this
for global variables and otherwise registers a helper function, but the path
for temporaries that are lifetime-extended by a reference with static or
thread storage duration (`pushTemporaryCleanup`) always registered the
destructor directly. On WebAssembly, which does not tolerate the mismatch,
this traps with "function signature mismatch" when the exit-time destructors
run.
Use the same check in `pushTemporaryCleanup`, and use the destroy helper
that is already used for arrays if the destructor cannot be registered
directly.
Fixes #45221.
Assisted-by: Claude Opus 5.5
---
clang/lib/CodeGen/CGExpr.cpp | 9 ++++++-
clang/test/CodeGenCXX/static-destructor.cpp | 28 +++++++++++++++++++++
2 files changed, 36 insertions(+), 1 deletion(-)
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_)
_______________________________________________
cfe-commits mailing list
[email protected]
https://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-commits