[clang] [clang][WebAssembly] Handle casted function pointers with different number of arguments (PR #153168)
Jorge Zapata via cfe-commits
cfe-commits at lists.llvm.org
Thu Jun 11 03:44:42 PDT 2026
https://github.com/turran updated https://github.com/llvm/llvm-project/pull/153168
>From fc8c1a0e57efe07c9d4a9f46ad733fbba481cad6 Mon Sep 17 00:00:00 2001
From: Jorge Zapata <jorgeluis.zapata at gmail.com>
Date: Mon, 23 Jun 2025 12:53:57 +0200
Subject: [PATCH 1/7] [wasm] Support different signature function pointers
---
clang/lib/CodeGen/CGCall.cpp | 20 ++
clang/lib/CodeGen/CGExprConstant.cpp | 13 ++
clang/lib/CodeGen/TargetInfo.h | 11 ++
clang/lib/CodeGen/Targets/WebAssembly.cpp | 222 ++++++++++++++++++++++
4 files changed, 266 insertions(+)
diff --git a/clang/lib/CodeGen/CGCall.cpp b/clang/lib/CodeGen/CGCall.cpp
index b7b79e7051181..2b86004944118 100644
--- a/clang/lib/CodeGen/CGCall.cpp
+++ b/clang/lib/CodeGen/CGCall.cpp
@@ -5067,6 +5067,26 @@ void CodeGenFunction::EmitCallArg(CallArgList &args, const Expr *E,
return;
}
+ // For WebAssembly target we need to create thunk functions
+ // to properly handle function pointers args with a different signature.
+ // Due to opaque pointers, this can not be handled in LLVM
+ // (WebAssemblyFixFunctionBitcast) anymore
+ if (CGM.getTriple().isWasm() && type->isFunctionPointerType()) {
+ if (const DeclRefExpr *DRE =
+ CGM.getTargetCodeGenInfo().getWasmFunctionDeclRefExpr(
+ E, CGM.getContext())) {
+ llvm::Value *V = EmitLValue(DRE).getPointer(*this);
+ llvm::Function *Thunk =
+ CGM.getTargetCodeGenInfo().getOrCreateWasmFunctionPointerThunk(
+ CGM, V, DRE->getDecl()->getType(), type);
+ if (Thunk) {
+ RValue R = RValue::get(Thunk);
+ args.add(R, type);
+ return;
+ }
+ }
+ }
+
args.add(EmitAnyExprToTemp(E), type);
}
diff --git a/clang/lib/CodeGen/CGExprConstant.cpp b/clang/lib/CodeGen/CGExprConstant.cpp
index 24712d3325b2e..57d93887ce2e2 100644
--- a/clang/lib/CodeGen/CGExprConstant.cpp
+++ b/clang/lib/CodeGen/CGExprConstant.cpp
@@ -2285,6 +2285,19 @@ ConstantLValueEmitter::tryEmitBase(const APValue::LValueBase &base) {
if (const auto *FD = dyn_cast<FunctionDecl>(D)) {
llvm::Constant *C = CGM.getRawFunctionPointer(FD);
+ // ForWebAssembly target we need to create thunk functions
+ // to properly handle function pointers args with a different signature
+ // Due to opaque pointers, this can not be handled in LLVM
+ // (WebAssemblyFixFunctionBitcast) anymore
+ if (CGM.getTriple().isWasm() && DestType->isFunctionPointerType()) {
+ llvm::Function *Thunk =
+ CGM.getTargetCodeGenInfo().getOrCreateWasmFunctionPointerThunk(
+ CGM, C, D->getType(), DestType);
+ if (Thunk) {
+ C = Thunk;
+ }
+ }
+
if (FD->getType()->isCFIUncheckedCalleeFunctionType())
C = llvm::NoCFIValue::get(cast<llvm::GlobalValue>(C));
return PtrAuthSign(C);
diff --git a/clang/lib/CodeGen/TargetInfo.h b/clang/lib/CodeGen/TargetInfo.h
index 98ee894fe557f..c708f798627e5 100644
--- a/clang/lib/CodeGen/TargetInfo.h
+++ b/clang/lib/CodeGen/TargetInfo.h
@@ -403,6 +403,17 @@ class TargetCodeGenInfo {
/// Return the WebAssembly funcref reference type.
virtual llvm::Type *getWasmFuncrefReferenceType() const { return nullptr; }
+ virtual const DeclRefExpr *getWasmFunctionDeclRefExpr(const Expr *E,
+ ASTContext &Ctx) const {
+ return nullptr;
+ }
+
+ virtual llvm::Function *getOrCreateWasmFunctionPointerThunk(
+ CodeGenModule &CGM, llvm::Value *OriginalFnPtr, QualType SrcType,
+ QualType DstType) const {
+ return nullptr;
+ }
+
/// Emit the device-side copy of the builtin surface type.
virtual bool emitCUDADeviceBuiltinSurfaceDeviceCopy(CodeGenFunction &CGF,
LValue Dst,
diff --git a/clang/lib/CodeGen/Targets/WebAssembly.cpp b/clang/lib/CodeGen/Targets/WebAssembly.cpp
index ebe996a4edd8d..b0a4f35c80c3d 100644
--- a/clang/lib/CodeGen/Targets/WebAssembly.cpp
+++ b/clang/lib/CodeGen/Targets/WebAssembly.cpp
@@ -9,9 +9,14 @@
#include "ABIInfoImpl.h"
#include "TargetInfo.h"
+#include "clang/AST/ParentMapContext.h"
+#include <sstream>
+
using namespace clang;
using namespace clang::CodeGen;
+#define DEBUG_TYPE "clang-target-wasm"
+
//===----------------------------------------------------------------------===//
// WebAssembly ABI Implementation
//
@@ -93,6 +98,112 @@ class WebAssemblyTargetCodeGenInfo final : public TargetCodeGenInfo {
virtual llvm::Type *getWasmFuncrefReferenceType() const override {
return llvm::Type::getWasm_FuncrefTy(getABIInfo().getVMContext());
}
+
+ virtual const DeclRefExpr *
+ getWasmFunctionDeclRefExpr(const Expr *E, ASTContext &Ctx) const override {
+ // Go down in the tree until finding the DeclRefExpr
+ const DeclRefExpr *DRE = findDeclRefExpr(E);
+ if (!DRE)
+ return nullptr;
+
+ // Final case. The argument is a declared function
+ if (isa<FunctionDecl>(DRE->getDecl())) {
+ return DRE;
+ }
+
+ // Complex case. The argument is a variable, we need to check
+ // every assignment of the variable and see if we are bitcasting
+ // or not.
+ if (const auto *VD = dyn_cast<VarDecl>(DRE->getDecl())) {
+ DRE = findDeclRefExprForVarUp(E, VD, Ctx);
+ if (DRE)
+ return DRE;
+
+ // If no assignment exists on every parent scope, check for the
+ // initialization
+ if (!DRE && VD->hasInit()) {
+ return getWasmFunctionDeclRefExpr(VD->getInit(), Ctx);
+ }
+ }
+
+ return nullptr;
+ }
+
+ virtual llvm::Function *getOrCreateWasmFunctionPointerThunk(
+ CodeGenModule &CGM, llvm::Value *OriginalFnPtr, QualType SrcType,
+ QualType DstType) const override {
+
+ // Get the signatures
+ const FunctionProtoType *SrcProtoType = SrcType->getAs<FunctionProtoType>();
+ const FunctionProtoType *DstProtoType = DstType->getAs<PointerType>()
+ ->getPointeeType()
+ ->getAs<FunctionProtoType>();
+
+ // This should only work for different number of arguments
+ if (DstProtoType->getNumParams() <= SrcProtoType->getNumParams())
+ return nullptr;
+
+ // Get the llvm function types
+ llvm::FunctionType *DstFunctionType = llvm::cast<llvm::FunctionType>(
+ CGM.getTypes().ConvertType(QualType(DstProtoType, 0)));
+ llvm::FunctionType *SrcFunctionType = llvm::cast<llvm::FunctionType>(
+ CGM.getTypes().ConvertType(QualType(SrcProtoType, 0)));
+
+ // Construct the Thunk function with the Target (destination) signature
+ std::string ThunkName = getThunkName(OriginalFnPtr->getName().str(),
+ DstProtoType, CGM.getContext());
+ llvm::Module &M = CGM.getModule();
+ llvm::Function *Thunk = llvm::Function::Create(
+ DstFunctionType, llvm::Function::InternalLinkage, ThunkName, M);
+
+ // Build the thunk body
+ llvm::IRBuilder<> Builder(
+ llvm::BasicBlock::Create(M.getContext(), "entry", Thunk));
+
+ // Gather the arguments for calling the original function
+ std::vector<llvm::Value *> CallArgs;
+ unsigned CallN = SrcProtoType->getNumParams();
+
+ auto ArgIt = Thunk->arg_begin();
+ for (unsigned i = 0; i < CallN && ArgIt != Thunk->arg_end(); ++i, ++ArgIt) {
+ llvm::Value *A = &*ArgIt;
+ CallArgs.push_back(A);
+ }
+
+ // Create the call to the original function pointer
+ llvm::CallInst *Call =
+ Builder.CreateCall(SrcFunctionType, OriginalFnPtr, CallArgs);
+
+ // Handle return type
+ llvm::Type *ThunkRetTy = DstFunctionType->getReturnType();
+
+ if (ThunkRetTy->isVoidTy()) {
+ Builder.CreateRetVoid();
+ } else {
+ llvm::Value *Ret = Call;
+ if (Ret->getType() != ThunkRetTy)
+ Ret = Builder.CreateBitCast(Ret, ThunkRetTy);
+ Builder.CreateRet(Ret);
+ }
+ LLVM_DEBUG(llvm::dbgs() << "getOrCreateWasmFunctionPointerThunk:"
+ << " from " << OriginalFnPtr->getName().str()
+ << " to " << ThunkName << "\n");
+ return Thunk;
+ }
+
+private:
+ // Build the thunk name: "%s_{type1}_{type2}_..."
+ std::string getThunkName(std::string OrigName,
+ const FunctionProtoType *DstProto,
+ const ASTContext &Ctx) const;
+ std::string sanitizeTypeString(const std::string &typeStr) const;
+ std::string getTypeName(const QualType &qt, const ASTContext &Ctx) const;
+ const DeclRefExpr *findDeclRefExpr(const Expr *E) const;
+ const DeclRefExpr *findDeclRefExprForVarDown(const Stmt *Parent,
+ const VarDecl *V,
+ ASTContext &Ctx) const;
+ const DeclRefExpr *findDeclRefExprForVarUp(const Expr *E, const VarDecl *V,
+ ASTContext &Ctx) const;
};
/// Classify argument of given type \p Ty.
@@ -171,3 +282,114 @@ CodeGen::createWebAssemblyTargetCodeGenInfo(CodeGenModule &CGM,
WebAssemblyABIKind K) {
return std::make_unique<WebAssemblyTargetCodeGenInfo>(CGM.getTypes(), K);
}
+
+// Helper to sanitize type name string for use in function name
+std::string WebAssemblyTargetCodeGenInfo::sanitizeTypeString(
+ const std::string &typeStr) const {
+ std::string s;
+ for (char c : typeStr) {
+ if (isalnum(c))
+ s += c;
+ else if (c == ' ')
+ s += '_';
+ else
+ s += '_';
+ }
+ return s;
+}
+
+// Helper to generate the type string from QualType
+std::string
+WebAssemblyTargetCodeGenInfo::getTypeName(const QualType &qt,
+ const ASTContext &Ctx) const {
+ PrintingPolicy Policy(Ctx.getLangOpts());
+ Policy.SuppressTagKeyword = true;
+ Policy.SuppressScope = true;
+ Policy.AnonymousTagLocations = false;
+ std::string typeStr = qt.getAsString(Policy);
+ return sanitizeTypeString(typeStr);
+}
+
+std::string
+WebAssemblyTargetCodeGenInfo::getThunkName(std::string OrigName,
+ const FunctionProtoType *DstProto,
+ const ASTContext &Ctx) const {
+ std::ostringstream oss;
+ oss << "__" << OrigName;
+ for (unsigned i = 0; i < DstProto->getNumParams(); ++i) {
+ oss << "_" << getTypeName(DstProto->getParamType(i), Ctx);
+ }
+ return oss.str();
+}
+
+/// Recursively find the first DeclRefExpr in an Expr subtree.
+/// Returns nullptr if not found.
+const DeclRefExpr *
+WebAssemblyTargetCodeGenInfo::findDeclRefExpr(const Expr *E) const {
+ if (!E)
+ return nullptr;
+
+ // In case it is a function call, abort
+ if (isa<CallExpr>(E))
+ return nullptr;
+
+ // If this node is a DeclRefExpr, return it.
+ if (const auto *DRE = dyn_cast<DeclRefExpr>(E))
+ return DRE;
+
+ // Otherwise, recurse into children.
+ for (const Stmt *Child : E->children()) {
+ if (const auto *ChildExpr = dyn_cast_or_null<Expr>(Child)) {
+ if (const DeclRefExpr *Found = findDeclRefExpr(ChildExpr))
+ return Found;
+ }
+ }
+ return nullptr;
+}
+
+const DeclRefExpr *WebAssemblyTargetCodeGenInfo::findDeclRefExprForVarDown(
+ const Stmt *Parent, const VarDecl *V, ASTContext &Ctx) const {
+ if (!Parent)
+ return nullptr;
+
+ // Find down every assignment of V
+ // FIXME we need to stop before the expression where V is used
+ const BinaryOperator *A = nullptr;
+ for (const Stmt *Child : Parent->children()) {
+ if (const auto *BO = dyn_cast_or_null<BinaryOperator>(Child)) {
+ if (!BO->isAssignmentOp())
+ continue;
+ auto *LHS = llvm::dyn_cast<DeclRefExpr>(BO->getLHS());
+ if (LHS && LHS->getDecl() == V) {
+ A = BO;
+ }
+ }
+ }
+
+ // We have an assignment of the Var, recurse in it
+ if (A) {
+ return getWasmFunctionDeclRefExpr(A->getRHS(), Ctx);
+ }
+
+ return nullptr;
+}
+
+const DeclRefExpr *WebAssemblyTargetCodeGenInfo::findDeclRefExprForVarUp(
+ const Expr *E, const VarDecl *V, ASTContext &Ctx) const {
+ const clang::Stmt *cur = E;
+ while (cur) {
+ auto parents = Ctx.getParentMapContext().getParents(*cur);
+ if (parents.empty())
+ break;
+ const clang::Stmt *parentStmt = parents[0].get<clang::Stmt>();
+ if (!parentStmt)
+ break;
+ if (const auto *CS = dyn_cast<clang::CompoundStmt>(parentStmt)) {
+ const DeclRefExpr *DRE = findDeclRefExprForVarDown(CS, V, Ctx);
+ if (DRE)
+ return DRE;
+ }
+ cur = parentStmt;
+ }
+ return nullptr;
+}
\ No newline at end of file
>From 7d746cf68884ca7f7f90dc8fd107755890c483ad Mon Sep 17 00:00:00 2001
From: Kleis Auke Wolthuizen <github at kleisauke.nl>
Date: Mon, 11 Aug 2025 14:47:55 +0200
Subject: [PATCH 2/7] Prefer use of Wasm signatures when building the thunk
name
---
clang/lib/CodeGen/Targets/WebAssembly.cpp | 66 ++++++++++++-----------
1 file changed, 36 insertions(+), 30 deletions(-)
diff --git a/clang/lib/CodeGen/Targets/WebAssembly.cpp b/clang/lib/CodeGen/Targets/WebAssembly.cpp
index b0a4f35c80c3d..983f550d48349 100644
--- a/clang/lib/CodeGen/Targets/WebAssembly.cpp
+++ b/clang/lib/CodeGen/Targets/WebAssembly.cpp
@@ -10,7 +10,6 @@
#include "TargetInfo.h"
#include "clang/AST/ParentMapContext.h"
-#include <sstream>
using namespace clang;
using namespace clang::CodeGen;
@@ -192,10 +191,11 @@ class WebAssemblyTargetCodeGenInfo final : public TargetCodeGenInfo {
}
private:
- // Build the thunk name: "%s_{type1}_{type2}_..."
+ // Build the thunk name: "%s_{OrigName}_{WasmSig}"
std::string getThunkName(std::string OrigName,
const FunctionProtoType *DstProto,
const ASTContext &Ctx) const;
+ char getTypeSig(const QualType &Ty, const ASTContext &Ctx) const;
std::string sanitizeTypeString(const std::string &typeStr) const;
std::string getTypeName(const QualType &qt, const ASTContext &Ctx) const;
const DeclRefExpr *findDeclRefExpr(const Expr *E) const;
@@ -283,43 +283,49 @@ CodeGen::createWebAssemblyTargetCodeGenInfo(CodeGenModule &CGM,
return std::make_unique<WebAssemblyTargetCodeGenInfo>(CGM.getTypes(), K);
}
-// Helper to sanitize type name string for use in function name
-std::string WebAssemblyTargetCodeGenInfo::sanitizeTypeString(
- const std::string &typeStr) const {
- std::string s;
- for (char c : typeStr) {
- if (isalnum(c))
- s += c;
- else if (c == ' ')
- s += '_';
- else
- s += '_';
+// Helper to get the type signature character for a given QualType
+// Returns a character that represents the given QualType in a wasm signature.
+// See getInvokeSig() in WebAssemblyAsmPrinter for related logic.
+char WebAssemblyTargetCodeGenInfo::getTypeSig(const QualType &Ty,
+ const ASTContext &Ctx) const {
+ if (Ty->isAnyPointerType()) {
+ return Ctx.getTypeSize(Ctx.VoidPtrTy) == 32 ? 'i' : 'j';
+ }
+ if (Ty->isIntegerType()) {
+ return Ctx.getTypeSize(Ty) <= 32 ? 'i' : 'j';
+ }
+ if (Ty->isFloatingType()) {
+ return Ctx.getTypeSize(Ty) <= 32 ? 'f' : 'd';
+ }
+ if (Ty->isVectorType()) {
+ return 'V';
+ }
+ if (Ty->isWebAssemblyTableType()) {
+ return 'F';
+ }
+ if (Ty->isWebAssemblyExternrefType()) {
+ return 'X';
}
- return s;
-}
-// Helper to generate the type string from QualType
-std::string
-WebAssemblyTargetCodeGenInfo::getTypeName(const QualType &qt,
- const ASTContext &Ctx) const {
- PrintingPolicy Policy(Ctx.getLangOpts());
- Policy.SuppressTagKeyword = true;
- Policy.SuppressScope = true;
- Policy.AnonymousTagLocations = false;
- std::string typeStr = qt.getAsString(Policy);
- return sanitizeTypeString(typeStr);
+ llvm_unreachable("Unhandled QualType");
}
std::string
WebAssemblyTargetCodeGenInfo::getThunkName(std::string OrigName,
const FunctionProtoType *DstProto,
const ASTContext &Ctx) const {
- std::ostringstream oss;
- oss << "__" << OrigName;
+
+ std::string ThunkName = "__" + OrigName + "_";
+ QualType RetTy = DstProto->getReturnType();
+ if (RetTy->isVoidType()) {
+ ThunkName += 'v';
+ } else {
+ ThunkName += getTypeSig(RetTy, Ctx);
+ }
for (unsigned i = 0; i < DstProto->getNumParams(); ++i) {
- oss << "_" << getTypeName(DstProto->getParamType(i), Ctx);
+ ThunkName += getTypeSig(DstProto->getParamType(i), Ctx);
}
- return oss.str();
+ return ThunkName;
}
/// Recursively find the first DeclRefExpr in an Expr subtree.
@@ -392,4 +398,4 @@ const DeclRefExpr *WebAssemblyTargetCodeGenInfo::findDeclRefExprForVarUp(
cur = parentStmt;
}
return nullptr;
-}
\ No newline at end of file
+}
>From 6bfecf12bb569662d2e09d393f629b936fd9ebd5 Mon Sep 17 00:00:00 2001
From: Jorge Zapata <jorgeluis.zapata at gmail.com>
Date: Mon, 11 Aug 2025 16:31:04 +0200
Subject: [PATCH 3/7] [wasm] Add a thunk cache
---
clang/lib/CodeGen/Targets/WebAssembly.cpp | 16 ++++++++++++++++
1 file changed, 16 insertions(+)
diff --git a/clang/lib/CodeGen/Targets/WebAssembly.cpp b/clang/lib/CodeGen/Targets/WebAssembly.cpp
index 983f550d48349..1bbc9ba4311fc 100644
--- a/clang/lib/CodeGen/Targets/WebAssembly.cpp
+++ b/clang/lib/CodeGen/Targets/WebAssembly.cpp
@@ -8,6 +8,7 @@
#include "ABIInfoImpl.h"
#include "TargetInfo.h"
+#include "llvm/ADT/StringMap.h"
#include "clang/AST/ParentMapContext.h"
@@ -56,6 +57,7 @@ class WebAssemblyTargetCodeGenInfo final : public TargetCodeGenInfo {
: TargetCodeGenInfo(std::make_unique<WebAssemblyABIInfo>(CGT, K)) {
SwiftInfo =
std::make_unique<SwiftABIInfo>(CGT, /*SwiftErrorInRegister=*/false);
+ ThunkCache = llvm::StringMap<llvm::Function *>();
}
void setTargetAttributes(const Decl *D, llvm::GlobalValue *GV,
@@ -151,6 +153,16 @@ class WebAssemblyTargetCodeGenInfo final : public TargetCodeGenInfo {
// Construct the Thunk function with the Target (destination) signature
std::string ThunkName = getThunkName(OriginalFnPtr->getName().str(),
DstProtoType, CGM.getContext());
+ // Check if we already have a thunk for this function
+ if (auto It = ThunkCache.find(ThunkName); It != ThunkCache.end()) {
+ LLVM_DEBUG(llvm::dbgs() << "getOrCreateWasmFunctionPointerThunk: "
+ << "found existing thunk for "
+ << OriginalFnPtr->getName().str() << " as "
+ << ThunkName << "\n");
+ return It->second;
+ }
+
+ // Create the thunk function
llvm::Module &M = CGM.getModule();
llvm::Function *Thunk = llvm::Function::Create(
DstFunctionType, llvm::Function::InternalLinkage, ThunkName, M);
@@ -187,10 +199,14 @@ class WebAssemblyTargetCodeGenInfo final : public TargetCodeGenInfo {
LLVM_DEBUG(llvm::dbgs() << "getOrCreateWasmFunctionPointerThunk:"
<< " from " << OriginalFnPtr->getName().str()
<< " to " << ThunkName << "\n");
+ // Cache the thunk
+ ThunkCache[ThunkName] = Thunk;
return Thunk;
}
private:
+ // The thunk cache
+ mutable llvm::StringMap<llvm::Function *> ThunkCache;
// Build the thunk name: "%s_{OrigName}_{WasmSig}"
std::string getThunkName(std::string OrigName,
const FunctionProtoType *DstProto,
>From 90770b32fa0782f0ce57b770766f78423cb1ab2d Mon Sep 17 00:00:00 2001
From: Jorge Zapata <jorgeluis.zapata at gmail.com>
Date: Tue, 12 Aug 2025 12:02:29 +0200
Subject: [PATCH 4/7] [wasm] Add function pointer thunk tests
---
.../CodeGenWebAssembly/function-pointer-arg.c | 25 ++++++++++++++++
.../function-pointer-field.c | 30 +++++++++++++++++++
2 files changed, 55 insertions(+)
create mode 100644 clang/test/CodeGenWebAssembly/function-pointer-arg.c
create mode 100644 clang/test/CodeGenWebAssembly/function-pointer-field.c
diff --git a/clang/test/CodeGenWebAssembly/function-pointer-arg.c b/clang/test/CodeGenWebAssembly/function-pointer-arg.c
new file mode 100644
index 0000000000000..ff7b4186bbf7b
--- /dev/null
+++ b/clang/test/CodeGenWebAssembly/function-pointer-arg.c
@@ -0,0 +1,25 @@
+// REQUIRES: webassembly-registered-target
+// RUN: %clang_cc1 -triple wasm32-unknown-unknown -emit-llvm -O0 -o - %s | FileCheck %s
+
+// Test of function pointer bitcast in a function argument with different argument number in wasm32
+
+#define FUNCTION_POINTER(f) ((FunctionPointer)(f))
+typedef int (*FunctionPointer)(int a, int b);
+
+int fp_as_arg(FunctionPointer fp, int a, int b) {
+ return fp(a, b);
+}
+
+int fp_less(int a) {
+ return a;
+}
+
+// CHECK-LABEL: @test
+// CHECK: call i32 @fp_as_arg(ptr noundef @__fp_less_iii, i32 noundef 10, i32 noundef 20)
+void test() {
+ fp_as_arg(FUNCTION_POINTER(fp_less), 10, 20);
+}
+
+// CHECK: define internal i32 @__fp_less_iii(i32 %0, i32 %1)
+// CHECK: %2 = call i32 @fp_less(i32 %0)
+// CHECK: ret i32 %2
\ No newline at end of file
diff --git a/clang/test/CodeGenWebAssembly/function-pointer-field.c b/clang/test/CodeGenWebAssembly/function-pointer-field.c
new file mode 100644
index 0000000000000..103a265ebf5fb
--- /dev/null
+++ b/clang/test/CodeGenWebAssembly/function-pointer-field.c
@@ -0,0 +1,30 @@
+// REQUIRES: webassembly-registered-target
+// RUN: %clang_cc1 -triple wasm32-unknown-unknown -emit-llvm -O0 -o - %s | FileCheck %s
+
+// Test of function pointer bitcast in a struct field with different argument number in wasm32
+
+#define FUNCTION_POINTER(f) ((FunctionPointer)(f))
+typedef int (*FunctionPointer)(int a, int b);
+
+// CHECK: @__const.test.sfp = private unnamed_addr constant %struct._StructWithFunctionPointer { ptr @__fp_less_iii }, align 4
+
+typedef struct _StructWithFunctionPointer {
+ FunctionPointer fp;
+} StructWithFunctionPointer;
+
+int fp_less(int a) {
+ return a;
+}
+
+// CHECK-LABEL: @test
+void test() {
+ StructWithFunctionPointer sfp = {
+ FUNCTION_POINTER(fp_less)
+ };
+
+ int a1 = sfp.fp(10, 20);
+}
+
+// CHECK: define internal i32 @__fp_less_iii(i32 %0, i32 %1)
+// CHECK: %2 = call i32 @fp_less(i32 %0)
+// CHECK: ret i32 %2
>From 2686f61152163f03adf67e7139f09eaa1f51e4d8 Mon Sep 17 00:00:00 2001
From: Jorge Zapata <jorgeluis.zapata at gmail.com>
Date: Wed, 3 Sep 2025 17:14:02 +0200
Subject: [PATCH 5/7] [wasm] Add -fwasm-fix-function-bitcasts to enable the
thunk generation
---
clang/include/clang/Basic/LangOptions.def | 2 ++
clang/include/clang/Options/Options.td | 6 ++++++
clang/lib/CodeGen/CGCall.cpp | 3 ++-
clang/lib/CodeGen/CGExprConstant.cpp | 4 +++-
clang/lib/Driver/ToolChains/WebAssembly.cpp | 4 ++++
clang/test/CodeGenWebAssembly/function-pointer-arg.c | 2 +-
clang/test/CodeGenWebAssembly/function-pointer-field.c | 2 +-
7 files changed, 19 insertions(+), 4 deletions(-)
diff --git a/clang/include/clang/Basic/LangOptions.def b/clang/include/clang/Basic/LangOptions.def
index 6bba142aaf428..ae6fe51fd9794 100644
--- a/clang/include/clang/Basic/LangOptions.def
+++ b/clang/include/clang/Basic/LangOptions.def
@@ -530,6 +530,8 @@ LANGOPT(EnableLifetimeSafetyTUAnalysis, 1, 0, NotCompatible, "Lifetime safety at
LANGOPT(PreserveVec3Type, 1, 0, NotCompatible, "Preserve 3-component vector type")
LANGOPT(Reflection , 1, 0, NotCompatible, "C++26 Reflection")
+LANGOPT(WasmFixFunctionBitcasts, 1, 0, Compatible, "Enable auto-generation of thunks for mismatched function pointer casts in WebAssembly")
+
#undef LANGOPT
#undef ENUM_LANGOPT
#undef VALUE_LANGOPT
diff --git a/clang/include/clang/Options/Options.td b/clang/include/clang/Options/Options.td
index 7e612aad92cda..c811a58d98f05 100644
--- a/clang/include/clang/Options/Options.td
+++ b/clang/include/clang/Options/Options.td
@@ -10083,6 +10083,7 @@ def fexperimental_emit_sgep
"(experimental).">,
MarshallingInfoFlag<LangOpts<"EmitStructuredGEP">>;
+// WebAssembly-only Options
def no_wasm_opt : Flag<["--"], "no-wasm-opt">,
Group<m_Group>,
HelpText<"Disable the wasm-opt optimizer">,
@@ -10091,3 +10092,8 @@ def wasm_opt : Flag<["--"], "wasm-opt">,
Group<m_Group>,
HelpText<"Enable the wasm-opt optimizer (default)">,
MarshallingInfoNegativeFlag<LangOpts<"NoWasmOpt">>;
+def fwasm_fix_function_bitcasts : Flag<["-"], "fwasm-fix-function-bitcasts">,
+ Group<f_Group>,
+ HelpText<"Enable auto-generation of thunks for mismatched function pointer casts in WebAssembly">,
+ Visibility<[ClangOption, CC1Option]>,
+ MarshallingInfoFlag<LangOpts<"WasmFixFunctionBitcasts">>;
\ No newline at end of file
diff --git a/clang/lib/CodeGen/CGCall.cpp b/clang/lib/CodeGen/CGCall.cpp
index 2b86004944118..c7d616c724de7 100644
--- a/clang/lib/CodeGen/CGCall.cpp
+++ b/clang/lib/CodeGen/CGCall.cpp
@@ -5071,7 +5071,8 @@ void CodeGenFunction::EmitCallArg(CallArgList &args, const Expr *E,
// to properly handle function pointers args with a different signature.
// Due to opaque pointers, this can not be handled in LLVM
// (WebAssemblyFixFunctionBitcast) anymore
- if (CGM.getTriple().isWasm() && type->isFunctionPointerType()) {
+ if (CGM.getTriple().isWasm() && CGM.getLangOpts().WasmFixFunctionBitcasts &&
+ type->isFunctionPointerType()) {
if (const DeclRefExpr *DRE =
CGM.getTargetCodeGenInfo().getWasmFunctionDeclRefExpr(
E, CGM.getContext())) {
diff --git a/clang/lib/CodeGen/CGExprConstant.cpp b/clang/lib/CodeGen/CGExprConstant.cpp
index 57d93887ce2e2..a7025dc65e45a 100644
--- a/clang/lib/CodeGen/CGExprConstant.cpp
+++ b/clang/lib/CodeGen/CGExprConstant.cpp
@@ -2289,7 +2289,9 @@ ConstantLValueEmitter::tryEmitBase(const APValue::LValueBase &base) {
// to properly handle function pointers args with a different signature
// Due to opaque pointers, this can not be handled in LLVM
// (WebAssemblyFixFunctionBitcast) anymore
- if (CGM.getTriple().isWasm() && DestType->isFunctionPointerType()) {
+ if (CGM.getTriple().isWasm() &&
+ CGM.getLangOpts().WasmFixFunctionBitcasts &&
+ DestType->isFunctionPointerType()) {
llvm::Function *Thunk =
CGM.getTargetCodeGenInfo().getOrCreateWasmFunctionPointerThunk(
CGM, C, D->getType(), DestType);
diff --git a/clang/lib/Driver/ToolChains/WebAssembly.cpp b/clang/lib/Driver/ToolChains/WebAssembly.cpp
index e532ef0743cc2..1bd893f311bc2 100644
--- a/clang/lib/Driver/ToolChains/WebAssembly.cpp
+++ b/clang/lib/Driver/ToolChains/WebAssembly.cpp
@@ -432,6 +432,10 @@ void WebAssembly::addClangTargetOptions(const ArgList &DriverArgs,
CC1Args.push_back("-wasm-enable-eh");
}
+ if (DriverArgs.getLastArg(options::OPT_fwasm_fix_function_bitcasts)) {
+ CC1Args.push_back("-fwasm-fix-function-bitcasts");
+ }
+
for (const Arg *A : DriverArgs.filtered(options::OPT_mllvm)) {
StringRef Opt = A->getValue(0);
if (Opt.starts_with("-emscripten-cxx-exceptions-allowed")) {
diff --git a/clang/test/CodeGenWebAssembly/function-pointer-arg.c b/clang/test/CodeGenWebAssembly/function-pointer-arg.c
index ff7b4186bbf7b..4ca85f11fe15f 100644
--- a/clang/test/CodeGenWebAssembly/function-pointer-arg.c
+++ b/clang/test/CodeGenWebAssembly/function-pointer-arg.c
@@ -1,5 +1,5 @@
// REQUIRES: webassembly-registered-target
-// RUN: %clang_cc1 -triple wasm32-unknown-unknown -emit-llvm -O0 -o - %s | FileCheck %s
+// RUN: %clang_cc1 -triple wasm32-unknown-unknown -emit-llvm -O0 -fwasm-fix-function-bitcasts -o - %s | FileCheck %s
// Test of function pointer bitcast in a function argument with different argument number in wasm32
diff --git a/clang/test/CodeGenWebAssembly/function-pointer-field.c b/clang/test/CodeGenWebAssembly/function-pointer-field.c
index 103a265ebf5fb..8c8424dc922e8 100644
--- a/clang/test/CodeGenWebAssembly/function-pointer-field.c
+++ b/clang/test/CodeGenWebAssembly/function-pointer-field.c
@@ -1,5 +1,5 @@
// REQUIRES: webassembly-registered-target
-// RUN: %clang_cc1 -triple wasm32-unknown-unknown -emit-llvm -O0 -o - %s | FileCheck %s
+// RUN: %clang_cc1 -triple wasm32-unknown-unknown -emit-llvm -O0 -fwasm-fix-function-bitcasts -o - %s | FileCheck %s
// Test of function pointer bitcast in a struct field with different argument number in wasm32
>From 2e5a015b97914b3cce271aa4215c22187284643e Mon Sep 17 00:00:00 2001
From: Jorge Zapata <jorgeluis.zapata at gmail.com>
Date: Tue, 7 Apr 2026 01:52:05 +0200
Subject: [PATCH 6/7] [wasm] Add a more general approach at bitcast
---
clang/lib/CodeGen/CGCall.cpp | 21 ----------
clang/lib/CodeGen/CGExprScalar.cpp | 21 ++++++++++
clang/lib/CodeGen/Targets/WebAssembly.cpp | 38 ++++++++++---------
.../function-pointer-local.c | 28 ++++++++++++++
.../function-pointer-void-assign.c | 24 ++++++++++++
5 files changed, 93 insertions(+), 39 deletions(-)
create mode 100644 clang/test/CodeGenWebAssembly/function-pointer-local.c
create mode 100644 clang/test/CodeGenWebAssembly/function-pointer-void-assign.c
diff --git a/clang/lib/CodeGen/CGCall.cpp b/clang/lib/CodeGen/CGCall.cpp
index c7d616c724de7..b7b79e7051181 100644
--- a/clang/lib/CodeGen/CGCall.cpp
+++ b/clang/lib/CodeGen/CGCall.cpp
@@ -5067,27 +5067,6 @@ void CodeGenFunction::EmitCallArg(CallArgList &args, const Expr *E,
return;
}
- // For WebAssembly target we need to create thunk functions
- // to properly handle function pointers args with a different signature.
- // Due to opaque pointers, this can not be handled in LLVM
- // (WebAssemblyFixFunctionBitcast) anymore
- if (CGM.getTriple().isWasm() && CGM.getLangOpts().WasmFixFunctionBitcasts &&
- type->isFunctionPointerType()) {
- if (const DeclRefExpr *DRE =
- CGM.getTargetCodeGenInfo().getWasmFunctionDeclRefExpr(
- E, CGM.getContext())) {
- llvm::Value *V = EmitLValue(DRE).getPointer(*this);
- llvm::Function *Thunk =
- CGM.getTargetCodeGenInfo().getOrCreateWasmFunctionPointerThunk(
- CGM, V, DRE->getDecl()->getType(), type);
- if (Thunk) {
- RValue R = RValue::get(Thunk);
- args.add(R, type);
- return;
- }
- }
- }
-
args.add(EmitAnyExprToTemp(E), type);
}
diff --git a/clang/lib/CodeGen/CGExprScalar.cpp b/clang/lib/CodeGen/CGExprScalar.cpp
index a8dcf22992983..7369060e60354 100644
--- a/clang/lib/CodeGen/CGExprScalar.cpp
+++ b/clang/lib/CodeGen/CGExprScalar.cpp
@@ -2670,6 +2670,27 @@ Value *ScalarExprEmitter::VisitCastExpr(CastExpr *CE) {
case CK_BlockPointerToObjCPointerCast:
case CK_AnyPointerToBlockPointerCast:
case CK_BitCast: {
+ // For WebAssembly, intercept function pointer bitcasts to a type with a
+ // different number of arguments and generate a thunk instead. This is
+ // necessary because WebAssembly enforces strict call-site/callee signature
+ // matching at runtime. The fix is gated on -fwasm-fix-function-bitcasts
+ // and only triggers when the source expression can be statically traced
+ // back to a concrete function declaration.
+ if (CGF.CGM.getTriple().isWasm() &&
+ CGF.CGM.getLangOpts().WasmFixFunctionBitcasts &&
+ DestTy->isFunctionPointerType()) {
+ if (const DeclRefExpr *DRE =
+ CGF.CGM.getTargetCodeGenInfo().getWasmFunctionDeclRefExpr(
+ E, CGF.CGM.getContext())) {
+ llvm::Value *V = EmitLValue(DRE).getPointer(CGF);
+ llvm::Function *Thunk =
+ CGF.CGM.getTargetCodeGenInfo().getOrCreateWasmFunctionPointerThunk(
+ CGF.CGM, V, DRE->getDecl()->getType(), DestTy);
+ if (Thunk)
+ return Thunk;
+ }
+ }
+
Value *Src = Visit(E);
llvm::Type *SrcTy = Src->getType();
llvm::Type *DstTy = ConvertType(DestTy);
diff --git a/clang/lib/CodeGen/Targets/WebAssembly.cpp b/clang/lib/CodeGen/Targets/WebAssembly.cpp
index 1bbc9ba4311fc..3fc2693a11e15 100644
--- a/clang/lib/CodeGen/Targets/WebAssembly.cpp
+++ b/clang/lib/CodeGen/Targets/WebAssembly.cpp
@@ -374,18 +374,21 @@ const DeclRefExpr *WebAssemblyTargetCodeGenInfo::findDeclRefExprForVarDown(
if (!Parent)
return nullptr;
- // Find down every assignment of V
- // FIXME we need to stop before the expression where V is used
+ // Find down every assignment of V.
+ // Standalone expression statements appear as Expr nodes directly under the
+ // CompoundStmt, so cast each child to Expr (if possible) and check for
+ // a BinaryOperator assignment.
const BinaryOperator *A = nullptr;
for (const Stmt *Child : Parent->children()) {
- if (const auto *BO = dyn_cast_or_null<BinaryOperator>(Child)) {
- if (!BO->isAssignmentOp())
- continue;
- auto *LHS = llvm::dyn_cast<DeclRefExpr>(BO->getLHS());
- if (LHS && LHS->getDecl() == V) {
- A = BO;
- }
- }
+ const auto *BO = dyn_cast_or_null<BinaryOperator>(Child);
+ if (!BO)
+ if (const auto *E = dyn_cast_or_null<Expr>(Child))
+ BO = dyn_cast<BinaryOperator>(E->IgnoreParenCasts());
+ if (!BO || !BO->isAssignmentOp())
+ continue;
+ auto *LHS = llvm::dyn_cast<DeclRefExpr>(BO->getLHS());
+ if (LHS && LHS->getDecl() == V)
+ A = BO;
}
// We have an assignment of the Var, recurse in it
@@ -398,20 +401,19 @@ const DeclRefExpr *WebAssemblyTargetCodeGenInfo::findDeclRefExprForVarDown(
const DeclRefExpr *WebAssemblyTargetCodeGenInfo::findDeclRefExprForVarUp(
const Expr *E, const VarDecl *V, ASTContext &Ctx) const {
- const clang::Stmt *cur = E;
- while (cur) {
- auto parents = Ctx.getParentMapContext().getParents(*cur);
+ // Use a DynTypedNode to walk parents, since a Stmt may be parented by a Decl
+ // (e.g. a VarDecl initializer) and get<Stmt>() would return nullptr there.
+ auto cur = clang::DynTypedNode::create(*E);
+ while (true) {
+ auto parents = Ctx.getParentMapContext().getParents(cur);
if (parents.empty())
break;
- const clang::Stmt *parentStmt = parents[0].get<clang::Stmt>();
- if (!parentStmt)
- break;
- if (const auto *CS = dyn_cast<clang::CompoundStmt>(parentStmt)) {
+ cur = parents[0];
+ if (const auto *CS = cur.get<clang::CompoundStmt>()) {
const DeclRefExpr *DRE = findDeclRefExprForVarDown(CS, V, Ctx);
if (DRE)
return DRE;
}
- cur = parentStmt;
}
return nullptr;
}
diff --git a/clang/test/CodeGenWebAssembly/function-pointer-local.c b/clang/test/CodeGenWebAssembly/function-pointer-local.c
new file mode 100644
index 0000000000000..6320832abd19c
--- /dev/null
+++ b/clang/test/CodeGenWebAssembly/function-pointer-local.c
@@ -0,0 +1,28 @@
+// REQUIRES: webassembly-registered-target
+// RUN: %clang_cc1 -triple wasm32-unknown-unknown -emit-llvm -O0 -fwasm-fix-function-bitcasts -o - %s | FileCheck %s
+
+// Test of function pointer bitcast stored in a local variable with different
+// argument number in wasm32. The cast happens via a CK_BitCast in the scalar
+// expression path (local variable assignment), which is intercepted in
+// CGExprScalar.cpp to generate a thunk — the same mechanism used for
+// function-argument and struct-field cases.
+
+#define FUNCTION_POINTER(f) ((FunctionPointer)(f))
+typedef int (*FunctionPointer)(int a, int b);
+
+int fp_less(int a) {
+ return a;
+}
+
+// CHECK-LABEL: @test
+// CHECK: store ptr @__fp_less_iii, ptr %fp
+// CHECK: %[[FP:.*]] = load ptr, ptr %fp
+// CHECK: call i32 %[[FP]](i32 noundef 10, i32 noundef 20)
+void test() {
+ FunctionPointer fp = FUNCTION_POINTER(fp_less);
+ fp(10, 20);
+}
+
+// CHECK: define internal i32 @__fp_less_iii(i32 %0, i32 %1)
+// CHECK: %2 = call i32 @fp_less(i32 %0)
+// CHECK: ret i32 %2
diff --git a/clang/test/CodeGenWebAssembly/function-pointer-void-assign.c b/clang/test/CodeGenWebAssembly/function-pointer-void-assign.c
new file mode 100644
index 0000000000000..640f84a33d127
--- /dev/null
+++ b/clang/test/CodeGenWebAssembly/function-pointer-void-assign.c
@@ -0,0 +1,24 @@
+// RUN: %clang_cc1 -triple wasm32-unknown-unknown -emit-llvm -O0 \
+// RUN: -fwasm-fix-function-bitcasts -o - %s | FileCheck %s
+
+// Test that a function pointer stored via a bare assignment into a void*
+// variable is correctly wrapped in a thunk when later cast to a different
+// function pointer type with more parameters.
+
+typedef int (*FP)(int, int);
+
+static int fp_one(int a) { return a < 0; }
+
+void test_void_ptr_bare_assign() {
+ // Bare assignment (no initializer) into void* — not via VD->hasInit()
+ void *p;
+ p = (void *)fp_one;
+ FP fp = (FP)p;
+ fp(10, 20);
+}
+
+// CHECK-LABEL: define void @test_void_ptr_bare_assign
+// CHECK: store ptr @__fp_one_iii, ptr %fp
+// CHECK: define internal i32 @__fp_one_iii(i32 %0, i32 %1)
+// CHECK-NEXT: entry:
+// CHECK-NEXT: %2 = call i32 @fp_one(i32 %0)
>From 079df8ea9458ebcc8ae82de38cf4411bda9c936f Mon Sep 17 00:00:00 2001
From: Jorge Zapata <jorgeluis.zapata at gmail.com>
Date: Thu, 11 Jun 2026 12:20:23 +0200
Subject: [PATCH 7/7] [wasm] Add runtime wrapper for function pointer casts
This complements the existing compile-time thunk generation for statically
traceable function references.
---
clang/lib/CodeGen/CGExprScalar.cpp | 19 +-
clang/lib/CodeGen/TargetInfo.h | 9 +
clang/lib/CodeGen/Targets/WebAssembly.cpp | 163 +++++++++++++++++-
.../function-pointer-runtime-cast.c | 54 ++++++
4 files changed, 241 insertions(+), 4 deletions(-)
create mode 100644 clang/test/CodeGenWebAssembly/function-pointer-runtime-cast.c
diff --git a/clang/lib/CodeGen/CGExprScalar.cpp b/clang/lib/CodeGen/CGExprScalar.cpp
index 7369060e60354..544559a0ae732 100644
--- a/clang/lib/CodeGen/CGExprScalar.cpp
+++ b/clang/lib/CodeGen/CGExprScalar.cpp
@@ -2674,11 +2674,13 @@ Value *ScalarExprEmitter::VisitCastExpr(CastExpr *CE) {
// different number of arguments and generate a thunk instead. This is
// necessary because WebAssembly enforces strict call-site/callee signature
// matching at runtime. The fix is gated on -fwasm-fix-function-bitcasts
- // and only triggers when the source expression can be statically traced
- // back to a concrete function declaration.
+ // and handles both compile-time (static) and runtime function pointer casts.
if (CGF.CGM.getTriple().isWasm() &&
CGF.CGM.getLangOpts().WasmFixFunctionBitcasts &&
- DestTy->isFunctionPointerType()) {
+ DestTy->isFunctionPointerType() &&
+ CE->getSubExpr()->getType()->isFunctionPointerType()) {
+
+ // Try compile-time thunk first (for statically traceable function refs)
if (const DeclRefExpr *DRE =
CGF.CGM.getTargetCodeGenInfo().getWasmFunctionDeclRefExpr(
E, CGF.CGM.getContext())) {
@@ -2689,6 +2691,17 @@ Value *ScalarExprEmitter::VisitCastExpr(CastExpr *CE) {
if (Thunk)
return Thunk;
}
+
+ // If not statically traceable, use runtime binding for non-constant values
+ Value *Src = Visit(E);
+ if (!isa<llvm::Constant>(Src)) {
+ llvm::Value *RuntimeThunk =
+ CGF.CGM.getTargetCodeGenInfo().emitWasmRuntimeFunctionPointerBinding(
+ CGF, Src, CE->getSubExpr()->getType(), DestTy);
+ if (RuntimeThunk)
+ return RuntimeThunk;
+ }
+ // Fall through to normal bitcast if runtime binding returns nullptr
}
Value *Src = Visit(E);
diff --git a/clang/lib/CodeGen/TargetInfo.h b/clang/lib/CodeGen/TargetInfo.h
index c708f798627e5..91b748f6e9f27 100644
--- a/clang/lib/CodeGen/TargetInfo.h
+++ b/clang/lib/CodeGen/TargetInfo.h
@@ -414,6 +414,15 @@ class TargetCodeGenInfo {
return nullptr;
}
+ /// Emit runtime binding for WebAssembly function pointer casts.
+ /// This handles runtime function pointer values (not compile-time constants)
+ /// that need signature adaptation.
+ virtual llvm::Value *emitWasmRuntimeFunctionPointerBinding(
+ CodeGenFunction &CGF, llvm::Value *FnPtr, QualType SrcType,
+ QualType DstType) const {
+ return nullptr;
+ }
+
/// Emit the device-side copy of the builtin surface type.
virtual bool emitCUDADeviceBuiltinSurfaceDeviceCopy(CodeGenFunction &CGF,
LValue Dst,
diff --git a/clang/lib/CodeGen/Targets/WebAssembly.cpp b/clang/lib/CodeGen/Targets/WebAssembly.cpp
index 3fc2693a11e15..3f8fafd8d0eb5 100644
--- a/clang/lib/CodeGen/Targets/WebAssembly.cpp
+++ b/clang/lib/CodeGen/Targets/WebAssembly.cpp
@@ -204,13 +204,26 @@ class WebAssemblyTargetCodeGenInfo final : public TargetCodeGenInfo {
return Thunk;
}
+ llvm::Value *emitWasmRuntimeFunctionPointerBinding(
+ CodeGenFunction &CGF, llvm::Value *FnPtr, QualType SrcType,
+ QualType DstType) const override;
+
private:
- // The thunk cache
+ // The thunk cache for compile-time thunks
mutable llvm::StringMap<llvm::Function *> ThunkCache;
+
+ // Runtime thunk cache: maps (SrcSig, DstSig) -> wrapper function
+ // The wrapper takes a function pointer and returns a thunk for it
+ mutable llvm::DenseMap<std::pair<const FunctionProtoType*, const FunctionProtoType*>,
+ llvm::Function*> RuntimeWrapperCache;
+
// Build the thunk name: "%s_{OrigName}_{WasmSig}"
std::string getThunkName(std::string OrigName,
const FunctionProtoType *DstProto,
const ASTContext &Ctx) const;
+ std::string getRuntimeWrapperName(const FunctionProtoType *SrcProto,
+ const FunctionProtoType *DstProto,
+ const ASTContext &Ctx) const;
char getTypeSig(const QualType &Ty, const ASTContext &Ctx) const;
std::string sanitizeTypeString(const std::string &typeStr) const;
std::string getTypeName(const QualType &qt, const ASTContext &Ctx) const;
@@ -293,6 +306,154 @@ RValue WebAssemblyABIInfo::EmitVAArg(CodeGenFunction &CGF, Address VAListAddr,
/*AllowHigherAlign=*/true, Slot);
}
+// Generate wrapper name for runtime function pointer binding
+std::string WebAssemblyTargetCodeGenInfo::getRuntimeWrapperName(
+ const FunctionProtoType *SrcProto, const FunctionProtoType *DstProto,
+ const ASTContext &Ctx) const {
+ std::string Name = "__wasm_runtime_wrapper_";
+
+ // Encode source signature
+ QualType SrcRetTy = SrcProto->getReturnType();
+ if (SrcRetTy->isVoidType()) {
+ Name += 'v';
+ } else {
+ Name += getTypeSig(SrcRetTy, Ctx);
+ }
+ for (QualType ParamType : SrcProto->param_types()) {
+ Name += getTypeSig(ParamType, Ctx);
+ }
+
+ Name += "_to_";
+
+ // Encode destination signature
+ QualType DstRetTy = DstProto->getReturnType();
+ if (DstRetTy->isVoidType()) {
+ Name += 'v';
+ } else {
+ Name += getTypeSig(DstRetTy, Ctx);
+ }
+ for (QualType ParamType : DstProto->param_types()) {
+ Name += getTypeSig(ParamType, Ctx);
+ }
+
+ return Name;
+}
+
+// Emit runtime binding for function pointer cast
+// This handles cases like g_list_free_full where a runtime parameter
+// needs to be cast from fewer params to more params
+llvm::Value *WebAssemblyTargetCodeGenInfo::emitWasmRuntimeFunctionPointerBinding(
+ CodeGenFunction &CGF, llvm::Value *FnPtr, QualType SrcType,
+ QualType DstType) const {
+
+ const FunctionProtoType *SrcProto =
+ SrcType->getPointeeType()->getAs<FunctionProtoType>();
+ const FunctionProtoType *DstProto =
+ DstType->getPointeeType()->getAs<FunctionProtoType>();
+
+ if (!SrcProto || !DstProto)
+ return nullptr;
+
+ // Only handle the case where we're adding params (fewer -> more)
+ unsigned SrcParams = SrcProto->getNumParams();
+ unsigned DstParams = DstProto->getNumParams();
+
+ if (SrcParams >= DstParams)
+ return nullptr;
+
+ // Return types must match exactly
+ QualType SrcRetTy = SrcProto->getReturnType();
+ QualType DstRetTy = DstProto->getReturnType();
+ if (!CGF.getContext().hasSameType(SrcRetTy, DstRetTy))
+ return nullptr;
+
+ LLVM_DEBUG(llvm::dbgs() << "emitWasmRuntimeFunctionPointerBinding: "
+ << "src params=" << SrcParams
+ << " dst params=" << DstParams << "\n");
+
+ auto Key = std::make_pair(SrcProto, DstProto);
+ auto It = RuntimeWrapperCache.find(Key);
+
+ llvm::Module &M = CGF.CGM.getModule();
+ llvm::LLVMContext &Context = M.getContext();
+ llvm::Type *PtrTy = llvm::PointerType::getUnqual(Context);
+
+ std::string WrapperName = getRuntimeWrapperName(SrcProto, DstProto, CGF.CGM.getContext());
+ std::string GlobalName = WrapperName + "_fptr";
+
+ // Get or create the global variable for storing the function pointer
+ llvm::GlobalVariable *FnPtrGlobal = M.getNamedGlobal(GlobalName);
+ if (!FnPtrGlobal) {
+ FnPtrGlobal = new llvm::GlobalVariable(
+ M, PtrTy, /*isConstant=*/false, llvm::GlobalValue::InternalLinkage,
+ llvm::Constant::getNullValue(PtrTy), GlobalName);
+ // Make it thread-local to support WebAssembly threads
+ FnPtrGlobal->setThreadLocalMode(llvm::GlobalValue::GeneralDynamicTLSModel);
+ }
+
+ llvm::Function *Wrapper;
+ if (It != RuntimeWrapperCache.end()) {
+ Wrapper = It->second;
+ } else {
+ // Create a new wrapper function that takes a function pointer
+ // and returns a thunk with the destination signature
+
+ llvm::FunctionType *SrcFnType = llvm::cast<llvm::FunctionType>(
+ CGF.CGM.getTypes().ConvertType(QualType(SrcProto, 0)));
+ llvm::FunctionType *DstFnType = llvm::cast<llvm::FunctionType>(
+ CGF.CGM.getTypes().ConvertType(QualType(DstProto, 0)));
+
+ // Wrapper signature: takes src function pointer, has dst signature
+ // Use LinkOnceODRLinkage to:
+ // 1. Prevent dead argument elimination (optimizer can't see all callers)
+ // 2. Allow linker to merge duplicates across modules (no symbol collisions)
+ // 3. Preserve exact signature required by WebAssembly type checking
+ Wrapper = llvm::Function::Create(
+ DstFnType, llvm::GlobalValue::LinkOnceODRLinkage, WrapperName, M);
+
+ // Mark as noinline to prevent inlining that would expose unused parameters
+ Wrapper->addFnAttr(llvm::Attribute::NoInline);
+ Wrapper->addFnAttr(llvm::Attribute::NoUnwind);
+
+ // Build wrapper body
+ llvm::BasicBlock *EntryBB = llvm::BasicBlock::Create(Context, "entry", Wrapper);
+ llvm::IRBuilder<> Builder(EntryBB);
+
+ // Load the stored function pointer
+ llvm::Value *StoredFnPtr = Builder.CreateLoad(PtrTy, FnPtrGlobal);
+
+ // Prepare arguments for the call (only pass what the source function expects)
+ llvm::SmallVector<llvm::Value *, 8> CallArgs;
+ auto ArgIt = Wrapper->arg_begin();
+ for (unsigned i = 0; i < SrcParams && i < DstParams; ++i) {
+ CallArgs.push_back(&*ArgIt);
+ ++ArgIt;
+ }
+
+ // Call the source function
+ llvm::CallInst *Call = Builder.CreateCall(SrcFnType, StoredFnPtr, CallArgs);
+
+ // Return the result
+ if (DstFnType->getReturnType()->isVoidTy()) {
+ Builder.CreateRetVoid();
+ } else {
+ Builder.CreateRet(Call);
+ }
+
+ RuntimeWrapperCache[Key] = Wrapper;
+ }
+
+ // Store the function pointer in the global variable
+ CharUnits Alignment = CGF.CGM.getPointerAlign();
+ Address GlobalAddr(FnPtrGlobal, PtrTy, Alignment);
+
+ // Store the function pointer to be used by the wrapper
+ CGF.Builder.CreateStore(FnPtr, GlobalAddr);
+
+ // Return the wrapper function
+ return Wrapper;
+}
+
std::unique_ptr<TargetCodeGenInfo>
CodeGen::createWebAssemblyTargetCodeGenInfo(CodeGenModule &CGM,
WebAssemblyABIKind K) {
diff --git a/clang/test/CodeGenWebAssembly/function-pointer-runtime-cast.c b/clang/test/CodeGenWebAssembly/function-pointer-runtime-cast.c
new file mode 100644
index 0000000000000..724fe69daa1e7
--- /dev/null
+++ b/clang/test/CodeGenWebAssembly/function-pointer-runtime-cast.c
@@ -0,0 +1,54 @@
+// REQUIRES: webassembly-registered-target
+// RUN: %clang_cc1 -triple wasm32-unknown-unknown -emit-llvm -O0 -fwasm-fix-function-bitcasts -o - %s | FileCheck %s
+
+// Test runtime function pointer cast with different argument counts
+// This simulates cases like g_list_free_full where a function pointer parameter
+// is cast from fewer params to more params
+
+typedef void (*OneArgFunc)(void *);
+typedef void (*TwoArgFunc)(void *, void *);
+
+// CHECK: @__wasm_runtime_wrapper_vi_to_vii_fptr = internal thread_local global ptr null
+
+// A function with one argument
+void my_one_arg_func(void *ptr) {
+ // Do something
+}
+
+// Test case 1: Direct call of casted runtime function pointer
+// CHECK-LABEL: @runtime_cast_caller
+void runtime_cast_caller(OneArgFunc fp, void *data) {
+ // Cast the runtime parameter from 1-arg to 2-arg signature and call directly
+ // CHECK: store ptr %{{.*}}, ptr @__wasm_runtime_wrapper_vi_to_vii_fptr
+ // CHECK: call void @__wasm_runtime_wrapper_vi_to_vii(ptr
+ ((TwoArgFunc)fp)(data, (void*)0);
+}
+
+// The runtime wrapper should be generated once and shared by both cases
+// CHECK-LABEL: define linkonce_odr void @__wasm_runtime_wrapper_vi_to_vii(ptr %0, ptr %1)
+// CHECK: %{{.*}} = load ptr, ptr @__wasm_runtime_wrapper_vi_to_vii_fptr
+// CHECK: call void %{{.*}}(ptr %0)
+// CHECK: ret void
+
+// Test case 2: Pass casted runtime function pointer to another function
+// This is closer to the real g_list_free_full scenario
+// CHECK-LABEL: @library_function
+void library_function(TwoArgFunc func, void *data) {
+ // CHECK: call void %{{.*}}(ptr noundef %{{.*}}, ptr noundef null)
+ func(data, (void*)0);
+}
+
+// CHECK-LABEL: @indirect_caller
+void indirect_caller(OneArgFunc fp, void *data) {
+ // Cast and pass to another function (like g_list_free_full does)
+ // CHECK: store ptr %{{.*}}, ptr @__wasm_runtime_wrapper_vi_to_vii_fptr
+ // CHECK: call void @library_function(ptr noundef @__wasm_runtime_wrapper_vi_to_vii
+ library_function((TwoArgFunc)fp, data);
+}
+
+// CHECK-LABEL: @test
+void test() {
+ // Test both scenarios
+ runtime_cast_caller(my_one_arg_func, (void*)0);
+ indirect_caller(my_one_arg_func, (void*)0);
+}
More information about the cfe-commits
mailing list