[clang] Expose direct method thunk (PR #170615)
Peter Rong via cfe-commits
cfe-commits at lists.llvm.org
Wed Dec 3 22:48:07 PST 2025
https://github.com/DataCorrupted created https://github.com/llvm/llvm-project/pull/170615
None
>From 76489044b1150f846af5fe86f07d8d8f32e56179 Mon Sep 17 00:00:00 2001
From: Peter Rong <PeterRong at meta.com>
Date: Wed, 3 Dec 2025 22:18:02 -0800
Subject: [PATCH 1/3] [ExposeObjCDirect] Adding a flag to allow new objc direct
ABI
1. Add a flag
2. Clean up and set up helper functions to implement later
Signed-off-by: Peter Rong <PeterRong at meta.com>
---
clang/include/clang/AST/DeclObjC.h | 6 ++++
clang/include/clang/Basic/CodeGenOptions.def | 2 ++
clang/include/clang/Options/Options.td | 5 +++
clang/lib/CodeGen/CGObjCRuntime.cpp | 26 +++++++++------
clang/lib/CodeGen/CGObjCRuntime.h | 35 ++++++++++++++++++--
clang/lib/CodeGen/CodeGenModule.h | 26 +++++++++++++++
clang/lib/Driver/ToolChains/Clang.cpp | 4 +++
7 files changed, 90 insertions(+), 14 deletions(-)
diff --git a/clang/include/clang/AST/DeclObjC.h b/clang/include/clang/AST/DeclObjC.h
index 2541edba83855..e2292cbdea042 100644
--- a/clang/include/clang/AST/DeclObjC.h
+++ b/clang/include/clang/AST/DeclObjC.h
@@ -482,6 +482,12 @@ class ObjCMethodDecl : public NamedDecl, public DeclContext {
/// True if the method is tagged as objc_direct
bool isDirectMethod() const;
+ /// Check if this direct method can move nil-check to thunk.
+ /// Variadic functions cannot use thunks (musttail incompatible with va_arg)
+ bool canHaveNilCheckThunk() const {
+ return isDirectMethod() && !isVariadic();
+ }
+
/// True if the method has a parameter that's destroyed in the callee.
bool hasParamDestroyedInCallee() const;
diff --git a/clang/include/clang/Basic/CodeGenOptions.def b/clang/include/clang/Basic/CodeGenOptions.def
index 76a6463881c6f..a7e8564c9e83c 100644
--- a/clang/include/clang/Basic/CodeGenOptions.def
+++ b/clang/include/clang/Basic/CodeGenOptions.def
@@ -210,6 +210,8 @@ ENUM_CODEGENOPT(ObjCDispatchMethod, ObjCDispatchMethodKind, 2, Legacy, Benign)
/// Replace certain message sends with calls to ObjC runtime entrypoints
CODEGENOPT(ObjCConvertMessagesToRuntimeCalls , 1, 1, Benign)
CODEGENOPT(ObjCAvoidHeapifyLocalBlocks, 1, 0, Benign)
+/// Expose objc_direct method symbols publicly and optimize nil checks.
+CODEGENOPT(ObjCExposeDirectMethods, 1, 0, Benign)
// The optimization options affect frontend options, which in turn do affect the AST.
diff --git a/clang/include/clang/Options/Options.td b/clang/include/clang/Options/Options.td
index 756d6deed7130..ddc04b30cbaa2 100644
--- a/clang/include/clang/Options/Options.td
+++ b/clang/include/clang/Options/Options.td
@@ -3775,6 +3775,11 @@ defm objc_avoid_heapify_local_blocks : BoolFOption<"objc-avoid-heapify-local-blo
PosFlag<SetTrue, [], [ClangOption], "Try">,
NegFlag<SetFalse, [], [ClangOption], "Don't try">,
BothFlags<[], [CC1Option], " to avoid heapifying local blocks">>;
+defm objc_expose_direct_methods : BoolFOption<"objc-expose-direct-methods",
+ CodeGenOpts<"ObjCExposeDirectMethods">, DefaultFalse,
+ PosFlag<SetTrue, [], [ClangOption, CC1Option],
+ "Expose direct method symbols and move nil checks to caller-side thunks">,
+ NegFlag<SetFalse>>;
defm disable_block_signature_string : BoolFOption<"disable-block-signature-string",
CodeGenOpts<"DisableBlockSignatureString">, DefaultFalse,
PosFlag<SetTrue, [], [ClangOption], "Disable">,
diff --git a/clang/lib/CodeGen/CGObjCRuntime.cpp b/clang/lib/CodeGen/CGObjCRuntime.cpp
index 76e0054f4c9da..38efd4d865284 100644
--- a/clang/lib/CodeGen/CGObjCRuntime.cpp
+++ b/clang/lib/CodeGen/CGObjCRuntime.cpp
@@ -382,11 +382,9 @@ CGObjCRuntime::getMessageSendInfo(const ObjCMethodDecl *method,
return MessageSendInfo(argsInfo, signatureType);
}
-bool CGObjCRuntime::canMessageReceiverBeNull(CodeGenFunction &CGF,
- const ObjCMethodDecl *method,
- bool isSuper,
- const ObjCInterfaceDecl *classReceiver,
- llvm::Value *receiver) {
+bool CGObjCRuntime::canMessageReceiverBeNull(
+ CodeGenFunction &CGF, const ObjCMethodDecl *method, bool isSuper,
+ const ObjCInterfaceDecl *classReceiver, llvm::Value *receiver) {
// Super dispatch assumes that self is non-null; even the messenger
// doesn't have a null check internally.
if (isSuper)
@@ -399,8 +397,7 @@ bool CGObjCRuntime::canMessageReceiverBeNull(CodeGenFunction &CGF,
// If we're emitting a method, and self is const (meaning just ARC, for now),
// and the receiver is a load of self, then self is a valid object.
- if (auto curMethod =
- dyn_cast_or_null<ObjCMethodDecl>(CGF.CurCodeDecl)) {
+ if (auto curMethod = dyn_cast_or_null<ObjCMethodDecl>(CGF.CurCodeDecl)) {
auto self = curMethod->getSelfDecl();
if (self->getType().isConstQualified()) {
if (auto LI = dyn_cast<llvm::LoadInst>(receiver->stripPointerCasts())) {
@@ -416,6 +413,13 @@ bool CGObjCRuntime::canMessageReceiverBeNull(CodeGenFunction &CGF,
return true;
}
+bool CGObjCRuntime::canClassObjectBeUnrealized(
+ const ObjCInterfaceDecl *CalleeClassDecl, CodeGenFunction &CGF) const {
+
+ // Otherwise, assume it can be unrealized.
+ return true;
+}
+
bool CGObjCRuntime::isWeakLinkedClass(const ObjCInterfaceDecl *ID) {
do {
if (ID->isWeakImported())
@@ -466,11 +470,11 @@ clang::CodeGen::emitObjCProtocolObject(CodeGenModule &CGM,
}
std::string CGObjCRuntime::getSymbolNameForMethod(const ObjCMethodDecl *OMD,
- bool includeCategoryName) {
+ bool includeCategoryName,
+ bool includePrefixByte) {
std::string buffer;
llvm::raw_string_ostream out(buffer);
- CGM.getCXXABI().getMangleContext().mangleObjCMethodName(OMD, out,
- /*includePrefixByte=*/true,
- includeCategoryName);
+ CGM.getCXXABI().getMangleContext().mangleObjCMethodName(
+ OMD, out, includePrefixByte, includeCategoryName);
return buffer;
}
diff --git a/clang/lib/CodeGen/CGObjCRuntime.h b/clang/lib/CodeGen/CGObjCRuntime.h
index 72997bf6348ae..1ee3b85e8a779 100644
--- a/clang/lib/CodeGen/CGObjCRuntime.h
+++ b/clang/lib/CodeGen/CGObjCRuntime.h
@@ -117,7 +117,8 @@ class CGObjCRuntime {
virtual ~CGObjCRuntime();
std::string getSymbolNameForMethod(const ObjCMethodDecl *method,
- bool includeCategoryName = true);
+ bool includeCategoryName = true,
+ bool includePrefixByte = true);
/// Generate the function required to register all Objective-C components in
/// this compilation unit with the runtime library.
@@ -322,10 +323,38 @@ class CGObjCRuntime {
MessageSendInfo getMessageSendInfo(const ObjCMethodDecl *method,
QualType resultType,
CallArgList &callArgs);
- bool canMessageReceiverBeNull(CodeGenFunction &CGF,
- const ObjCMethodDecl *method, bool isSuper,
+
+ /// Check if the receiver of an ObjC message send can be null.
+ /// Returns true if the receiver may be null, false if provably non-null.
+ ///
+ /// This can be overridden by subclasses to add runtime-specific heuristics.
+ /// Base implementation checks:
+ /// - Super dispatch (always non-null)
+ /// - Self in const-qualified methods (ARC)
+ /// - Weak-linked classes
+ ///
+ /// Future enhancements in CGObjCCommonMac override:
+ /// - _Nonnull attributes
+ /// - Results of alloc, new, ObjC literals
+ virtual bool canMessageReceiverBeNull(CodeGenFunction &CGF,
+ const ObjCMethodDecl *method,
+ bool isSuper,
const ObjCInterfaceDecl *classReceiver,
llvm::Value *receiver);
+
+ /// Check if a class object can be unrealized (not yet initialized).
+ /// Returns true if the class may be unrealized, false if provably realized.
+ ///
+ /// STUB IMPLEMENTATION: Base class always returns true (conservative).
+ /// Subclasses can override to add runtime-specific dominating-call analysis.
+ ///
+ /// Future: Returns false if:
+ /// - An instance method on the same class was called in a dominating path
+ /// - The class was explicitly realized earlier in control flow
+ /// - Note: [Parent foo] does NOT realize Child (inheritance care needed)
+ virtual bool canClassObjectBeUnrealized(const ObjCInterfaceDecl *ClassDecl,
+ CodeGenFunction &CGF) const;
+
static bool isWeakLinkedClass(const ObjCInterfaceDecl *cls);
/// Destroy the callee-destroyed arguments of the given method,
diff --git a/clang/lib/CodeGen/CodeGenModule.h b/clang/lib/CodeGen/CodeGenModule.h
index a253bcda2d06c..f65739de10957 100644
--- a/clang/lib/CodeGen/CodeGenModule.h
+++ b/clang/lib/CodeGen/CodeGenModule.h
@@ -717,6 +717,32 @@ class CodeGenModule : public CodeGenTypeCache {
/// Return true iff an Objective-C runtime has been configured.
bool hasObjCRuntime() { return !!ObjCRuntime; }
+ /// Check if a direct method should have its symbol exposed (no \01 prefix).
+ /// This applies to ALL direct methods (including variadic).
+ /// Returns false if OMD is null or not a direct method.
+ bool shouldExposeSymbol(const ObjCMethodDecl *OMD) const {
+ return OMD && OMD->isDirectMethod() &&
+ getLangOpts().ObjCRuntime.isNeXTFamily() &&
+ getCodeGenOpts().ObjCExposeDirectMethods;
+ }
+
+ /// Check if a direct method should use nil-check thunks at call sites.
+ /// This applies only to non-variadic direct methods.
+ /// Variadic methods cannot use thunks (musttail incompatible with va_arg).
+ /// Returns false if OMD is null or not eligible for thunks.
+ bool shouldHaveNilCheckThunk(const ObjCMethodDecl *OMD) const {
+ return OMD && shouldExposeSymbol(OMD) && OMD->canHaveNilCheckThunk();
+ }
+
+ /// Check if a direct method should have inline nil checks at call sites.
+ /// This applies to direct methods that cannot use thunks (e.g., variadic
+ /// methods). These methods get exposed symbols but need inline nil checks
+ /// instead of thunks. Returns false if OMD is null or not eligible for inline
+ /// nil checks.
+ bool shouldHaveNilCheckInline(const ObjCMethodDecl *OMD) const {
+ return OMD && shouldExposeSymbol(OMD) && !OMD->canHaveNilCheckThunk();
+ }
+
const std::string &getModuleNameHash() const { return ModuleNameHash; }
/// Return a reference to the configured OpenCL runtime.
diff --git a/clang/lib/Driver/ToolChains/Clang.cpp b/clang/lib/Driver/ToolChains/Clang.cpp
index 0380568412e62..2c3beb94f2ef8 100644
--- a/clang/lib/Driver/ToolChains/Clang.cpp
+++ b/clang/lib/Driver/ToolChains/Clang.cpp
@@ -4096,6 +4096,10 @@ static void RenderObjCOptions(const ToolChain &TC, const Driver &D,
}
}
+ // Forward -fobjc-expose-direct-methods to cc1
+ if (Args.hasArg(options::OPT_fobjc_expose_direct_methods))
+ CmdArgs.push_back("-fobjc-expose-direct-methods");
+
// When ObjectiveC legacy runtime is in effect on MacOSX, turn on the option
// to do Array/Dictionary subscripting by default.
if (Arch == llvm::Triple::x86 && T.isMacOSX() &&
>From ff65d5543f5187998bf7a35017e70c5605008739 Mon Sep 17 00:00:00 2001
From: Peter Rong <PeterRong at meta.com>
Date: Wed, 3 Dec 2025 22:35:15 -0800
Subject: [PATCH 2/3] [ExposeObjCDirect] Setup helper functions
1. GenerateDirectMethodsPreconditionCheck: Move some functionalities to a separate functions.
Those functions will be reused if we move precondition checks into a thunk
2. Create `DirectMethodInfo`, which will be used to manage true implementation and its thunk
---
clang/lib/CodeGen/CGObjCGNU.cpp | 9 +++
clang/lib/CodeGen/CGObjCMac.cpp | 95 ++++++++++++++++++++++++-------
clang/lib/CodeGen/CGObjCRuntime.h | 6 ++
3 files changed, 88 insertions(+), 22 deletions(-)
diff --git a/clang/lib/CodeGen/CGObjCGNU.cpp b/clang/lib/CodeGen/CGObjCGNU.cpp
index 06643d4bdc211..9c814487860ac 100644
--- a/clang/lib/CodeGen/CGObjCGNU.cpp
+++ b/clang/lib/CodeGen/CGObjCGNU.cpp
@@ -600,6 +600,9 @@ class CGObjCGNU : public CGObjCRuntime {
// Map to unify direct method definitions.
llvm::DenseMap<const ObjCMethodDecl *, llvm::Function *>
DirectMethodDefinitions;
+ void GenerateDirectMethodsPreconditionCheck(
+ CodeGenFunction &CGF, llvm::Function *Fn, const ObjCMethodDecl *OMD,
+ const ObjCContainerDecl *CD) override;
void GenerateDirectMethodPrologue(CodeGenFunction &CGF, llvm::Function *Fn,
const ObjCMethodDecl *OMD,
const ObjCContainerDecl *CD) override;
@@ -4196,6 +4199,12 @@ llvm::Function *CGObjCGNU::GenerateMethod(const ObjCMethodDecl *OMD,
return Fn;
}
+void CGObjCGNU::GenerateDirectMethodsPreconditionCheck(
+ CodeGenFunction &CGF, llvm::Function *Fn, const ObjCMethodDecl *OMD,
+ const ObjCContainerDecl *CD) {
+ // GNU runtime doesn't support direct calls at this time
+}
+
void CGObjCGNU::GenerateDirectMethodPrologue(CodeGenFunction &CGF,
llvm::Function *Fn,
const ObjCMethodDecl *OMD,
diff --git a/clang/lib/CodeGen/CGObjCMac.cpp b/clang/lib/CodeGen/CGObjCMac.cpp
index cb5bb403bb53b..3f4b11c634ce4 100644
--- a/clang/lib/CodeGen/CGObjCMac.cpp
+++ b/clang/lib/CodeGen/CGObjCMac.cpp
@@ -847,9 +847,19 @@ class CGObjCCommonMac : public CodeGen::CGObjCRuntime {
/// this translation unit.
llvm::DenseMap<const ObjCMethodDecl *, llvm::Function *> MethodDefinitions;
+ /// Information about a direct method definition
+ struct DirectMethodInfo {
+ llvm::Function
+ *Implementation; // The true implementation (where body is emitted)
+ llvm::Function *Thunk; // The nil-check thunk (nullptr if not generated)
+
+ DirectMethodInfo(llvm::Function *Impl, llvm::Function *Thunk = nullptr)
+ : Implementation(Impl), Thunk(Thunk) {}
+ };
+
/// DirectMethodDefinitions - map of direct methods which have been defined in
/// this translation unit.
- llvm::DenseMap<const ObjCMethodDecl *, llvm::Function *>
+ llvm::DenseMap<const ObjCMethodDecl *, DirectMethodInfo>
DirectMethodDefinitions;
/// PropertyNames - uniqued method variable names.
@@ -1053,9 +1063,20 @@ class CGObjCCommonMac : public CodeGen::CGObjCRuntime {
GenerateMethod(const ObjCMethodDecl *OMD,
const ObjCContainerDecl *CD = nullptr) override;
- llvm::Function *GenerateDirectMethod(const ObjCMethodDecl *OMD,
+ DirectMethodInfo &GenerateDirectMethod(const ObjCMethodDecl *OMD,
const ObjCContainerDecl *CD);
+ /// Generate class realization code: [self self]
+ /// This is used for class methods to ensure the class is initialized.
+ /// Returns the realized class object.
+ llvm::Value *GenerateClassRealization(CodeGenFunction &CGF,
+ llvm::Value *classObject,
+ const ObjCInterfaceDecl *OID);
+
+ void GenerateDirectMethodsPreconditionCheck(
+ CodeGenFunction &CGF, llvm::Function *Fn, const ObjCMethodDecl *OMD,
+ const ObjCContainerDecl *CD) override;
+
void GenerateDirectMethodPrologue(CodeGenFunction &CGF, llvm::Function *Fn,
const ObjCMethodDecl *OMD,
const ObjCContainerDecl *CD) override;
@@ -3847,7 +3868,9 @@ llvm::Function *CGObjCCommonMac::GenerateMethod(const ObjCMethodDecl *OMD,
llvm::Function *Method;
if (OMD->isDirectMethod()) {
- Method = GenerateDirectMethod(OMD, CD);
+ // Returns DirectMethodInfo& containing both Implementation and Thunk
+ DirectMethodInfo &Info = GenerateDirectMethod(OMD, CD);
+ Method = Info.Implementation; // Extract implementation for body generation
} else {
auto Name = getSymbolNameForMethod(OMD);
@@ -3863,7 +3886,7 @@ llvm::Function *CGObjCCommonMac::GenerateMethod(const ObjCMethodDecl *OMD,
return Method;
}
-llvm::Function *
+CGObjCCommonMac::DirectMethodInfo &
CGObjCCommonMac::GenerateDirectMethod(const ObjCMethodDecl *OMD,
const ObjCContainerDecl *CD) {
auto *COMD = OMD->getCanonicalDecl();
@@ -3882,7 +3905,7 @@ CGObjCCommonMac::GenerateDirectMethod(const ObjCMethodDecl *OMD,
// a new one that has the proper type below.
if (!OMD->getBody() || COMD->getReturnType() == OMD->getReturnType())
return I->second;
- OldFn = I->second;
+ OldFn = I->second.Implementation;
}
CodeGenTypes &Types = CGM.getTypes();
@@ -3896,20 +3919,41 @@ CGObjCCommonMac::GenerateDirectMethod(const ObjCMethodDecl *OMD,
OldFn->replaceAllUsesWith(Fn);
OldFn->eraseFromParent();
- // Replace the cached function in the map.
- I->second = Fn;
+ // Replace the cached implementation in the map.
+ I->second.Implementation = Fn;
+
} else {
auto Name = getSymbolNameForMethod(OMD, /*include category*/ false);
Fn = llvm::Function::Create(MethodTy, llvm::GlobalValue::ExternalLinkage,
Name, &CGM.getModule());
- DirectMethodDefinitions.insert(std::make_pair(COMD, Fn));
+ auto [It, inserted] = DirectMethodDefinitions.insert(std::make_pair(COMD, DirectMethodInfo(Fn)));
+ I = It;
}
- return Fn;
+ // Return reference to DirectMethodInfo (contains both Implementation and
+ // Thunk)
+ return I->second;
}
-void CGObjCCommonMac::GenerateDirectMethodPrologue(
+llvm::Value *
+CGObjCCommonMac::GenerateClassRealization(CodeGenFunction &CGF,
+ llvm::Value *classObject,
+ const ObjCInterfaceDecl *OID) {
+ // Generate: self = [self self]
+ // This forces class lazy initialization
+ Selector SelfSel = GetNullarySelector("self", CGM.getContext());
+ auto ResultType = CGF.getContext().getObjCIdType();
+ CallArgList Args;
+
+ RValue result = GeneratePossiblySpecializedMessageSend(
+ CGF, ReturnValueSlot(), ResultType, SelfSel, classObject, Args, OID,
+ nullptr, true);
+
+ return result.getScalarVal();
+}
+
+void CGObjCCommonMac::GenerateDirectMethodsPreconditionCheck(
CodeGenFunction &CGF, llvm::Function *Fn, const ObjCMethodDecl *OMD,
const ObjCContainerDecl *CD) {
auto &Builder = CGF.Builder;
@@ -3926,18 +3970,11 @@ void CGObjCCommonMac::GenerateDirectMethodPrologue(
// if (self == nil) {
// return (ReturnType){ };
// }
- //
- // _cmd = @selector(...)
- // ...
if (OMD->isClassMethod()) {
const ObjCInterfaceDecl *OID = cast<ObjCInterfaceDecl>(CD);
assert(OID &&
"GenerateDirectMethod() should be called with the Class Interface");
- Selector SelfSel = GetNullarySelector("self", CGM.getContext());
- auto ResultType = CGF.getContext().getObjCIdType();
- RValue result;
- CallArgList Args;
// TODO: If this method is inlined, the caller might know that `self` is
// already initialized; for example, it might be an ordinary Objective-C
@@ -3946,10 +3983,10 @@ void CGObjCCommonMac::GenerateDirectMethodPrologue(
//
// We should find a way to eliminate this unnecessary initialization in such
// cases in LLVM.
- result = GeneratePossiblySpecializedMessageSend(
- CGF, ReturnValueSlot(), ResultType, SelfSel, selfValue, Args, OID,
- nullptr, true);
- Builder.CreateStore(result.getScalarVal(), selfAddr);
+
+ // Perform class realization using the helper function
+ llvm::Value *realizedClass = GenerateClassRealization(CGF, selfValue, OID);
+ Builder.CreateStore(realizedClass, selfAddr);
// Nullable `Class` expressions cannot be messaged with a direct method
// so the only reason why the receive can be null would be because
@@ -3957,6 +3994,7 @@ void CGObjCCommonMac::GenerateDirectMethodPrologue(
ReceiverCanBeNull = isWeakLinkedClass(OID);
}
+ // Generate nil check
if (ReceiverCanBeNull) {
llvm::BasicBlock *SelfIsNilBlock =
CGF.createBasicBlock("objc_direct_method.self_is_nil");
@@ -3986,8 +4024,21 @@ void CGObjCCommonMac::GenerateDirectMethodPrologue(
CGF.EmitBlock(ContBlock);
Builder.SetInsertPoint(ContBlock);
}
+}
- // only synthesize _cmd if it's referenced
+void CGObjCCommonMac::GenerateDirectMethodPrologue(
+ CodeGenFunction &CGF, llvm::Function *Fn, const ObjCMethodDecl *OMD,
+ const ObjCContainerDecl *CD) {
+ // Generate precondition checks (class realization + nil check) if needed
+ // Without flag: precondition checks are in the implementation
+ // With flag: precondition checks will be in the thunk (not here)
+ if (!CGM.shouldExposeSymbol(OMD)) {
+ GenerateDirectMethodsPreconditionCheck(CGF, Fn, OMD, CD);
+ }
+
+ auto &Builder = CGF.Builder;
+ // Only synthesize _cmd if it's referenced
+ // This is the actual "prologue" work that always happens
if (OMD->getCmdDecl()->isUsed()) {
// `_cmd` is not a parameter to direct methods, so storage must be
// explicitly declared for it.
diff --git a/clang/lib/CodeGen/CGObjCRuntime.h b/clang/lib/CodeGen/CGObjCRuntime.h
index 1ee3b85e8a779..8d5ee1310e51f 100644
--- a/clang/lib/CodeGen/CGObjCRuntime.h
+++ b/clang/lib/CodeGen/CGObjCRuntime.h
@@ -226,6 +226,12 @@ class CGObjCRuntime {
virtual llvm::Function *GenerateMethod(const ObjCMethodDecl *OMD,
const ObjCContainerDecl *CD) = 0;
+/// Generates precondition checks for direct Objective-C Methods.
+ /// This includes [self self] for class methods and nil checks.
+ virtual void GenerateDirectMethodsPreconditionCheck(
+ CodeGenFunction &CGF, llvm::Function *Fn, const ObjCMethodDecl *OMD,
+ const ObjCContainerDecl *CD) = 0;
+
/// Generates prologue for direct Objective-C Methods.
virtual void GenerateDirectMethodPrologue(CodeGenFunction &CGF,
llvm::Function *Fn,
>From 81b0d800623f03b0c3fa9b4412e36dc585236ed8 Mon Sep 17 00:00:00 2001
From: Peter Rong <PeterRong at meta.com>
Date: Wed, 3 Dec 2025 22:42:51 -0800
Subject: [PATCH 3/3] [ExposeDirectMethod] Nil chech thunk generation
- Generation
- Dispatch
---
clang/lib/CodeGen/CGDecl.cpp | 4 +-
clang/lib/CodeGen/CGObjC.cpp | 17 +-
clang/lib/CodeGen/CGObjCMac.cpp | 243 +++++++++++++++++++++++++++-
clang/lib/CodeGen/CodeGenFunction.h | 7 +
4 files changed, 263 insertions(+), 8 deletions(-)
diff --git a/clang/lib/CodeGen/CGDecl.cpp b/clang/lib/CodeGen/CGDecl.cpp
index 8b1cd83af2396..9f0e09eac8866 100644
--- a/clang/lib/CodeGen/CGDecl.cpp
+++ b/clang/lib/CodeGen/CGDecl.cpp
@@ -2757,7 +2757,9 @@ void CodeGenFunction::EmitParmDecl(const VarDecl &D, ParamValue Arg,
llvm::Value *ArgVal = (DoStore ? Arg.getDirectValue() : nullptr);
LValue lv = MakeAddrLValue(DeclPtr, Ty);
- if (IsScalar) {
+ // If this is a thunk, don't bother with ARC lifetime management.
+ // The true implementation will take care of that.
+ if (IsScalar && !CurFuncIsThunk) {
Qualifiers qs = Ty.getQualifiers();
if (Qualifiers::ObjCLifetime lt = qs.getObjCLifetime()) {
// We honor __attribute__((ns_consumed)) for types with lifetime.
diff --git a/clang/lib/CodeGen/CGObjC.cpp b/clang/lib/CodeGen/CGObjC.cpp
index 10aad2e26938d..f1b8627fc119a 100644
--- a/clang/lib/CodeGen/CGObjC.cpp
+++ b/clang/lib/CodeGen/CGObjC.cpp
@@ -761,7 +761,18 @@ void CodeGenFunction::StartObjCMethod(const ObjCMethodDecl *OMD,
const CGFunctionInfo &FI = CGM.getTypes().arrangeObjCMethodDeclaration(OMD);
if (OMD->isDirectMethod()) {
- Fn->setVisibility(llvm::Function::HiddenVisibility);
+ Fn->setVisibility(llvm::GlobalValue::HiddenVisibility);
+ if (CGM.shouldExposeSymbol(OMD)) {
+ // Find the decl that may have visibility set (property or method)
+ const NamedDecl *Decl = OMD;
+ if (const auto *PD = OMD->findPropertyDecl()) {
+ Decl = PD;
+ }
+ // and respect source level visibility setting
+ if (auto V = Decl->getExplicitVisibility(NamedDecl::VisibilityForValue)) {
+ Fn->setVisibility(CGM.GetLLVMVisibility(*V));
+ }
+ }
CGM.SetLLVMFunctionAttributes(OMD, FI, Fn, /*IsThunk=*/false);
CGM.SetLLVMFunctionAttributesForDefinition(OMD, Fn);
} else {
@@ -781,10 +792,6 @@ void CodeGenFunction::StartObjCMethod(const ObjCMethodDecl *OMD,
OMD->getLocation(), StartLoc);
if (OMD->isDirectMethod()) {
- // This function is a direct call, it has to implement a nil check
- // on entry.
- //
- // TODO: possibly have several entry points to elide the check
CGM.getObjCRuntime().GenerateDirectMethodPrologue(*this, Fn, OMD, CD);
}
diff --git a/clang/lib/CodeGen/CGObjCMac.cpp b/clang/lib/CodeGen/CGObjCMac.cpp
index 3f4b11c634ce4..741e5d85b5935 100644
--- a/clang/lib/CodeGen/CGObjCMac.cpp
+++ b/clang/lib/CodeGen/CGObjCMac.cpp
@@ -1066,6 +1066,15 @@ class CGObjCCommonMac : public CodeGen::CGObjCRuntime {
DirectMethodInfo &GenerateDirectMethod(const ObjCMethodDecl *OMD,
const ObjCContainerDecl *CD);
+ llvm::Function *GenerateObjCDirectThunk(const ObjCMethodDecl *OMD,
+ const ObjCContainerDecl *CD,
+ llvm::Function *Implementation);
+
+ llvm::Function *GetDirectMethodCallee(const ObjCMethodDecl *OMD,
+ const ObjCContainerDecl *CD,
+ bool ReceiverCanBeNull,
+ bool ClassObjectCanBeUnrealized);
+
/// Generate class realization code: [self self]
/// This is used for class methods to ensure the class is initialized.
/// Returns the realized class object.
@@ -2094,6 +2103,9 @@ CodeGen::RValue CGObjCCommonMac::EmitMessageSend(
bool ReceiverCanBeNull =
canMessageReceiverBeNull(CGF, Method, IsSuper, ClassReceiver, Arg0);
+ bool ClassObjectCanBeUnrealized =
+ Method && Method->isClassMethod() &&
+ canClassObjectBeUnrealized(ClassReceiver, CGF);
bool RequiresNullCheck = false;
bool RequiresSelValue = true;
@@ -2101,7 +2113,11 @@ CodeGen::RValue CGObjCCommonMac::EmitMessageSend(
llvm::FunctionCallee Fn = nullptr;
if (Method && Method->isDirectMethod()) {
assert(!IsSuper);
- Fn = GenerateDirectMethod(Method, Method->getClassInterface());
+ // Use GetDirectMethodCallee to decide whether to use implementation or
+ // thunk.
+ Fn = GetDirectMethodCallee(Method, Method->getClassInterface(),
+ ReceiverCanBeNull, ClassObjectCanBeUnrealized);
+
// Direct methods will synthesize the proper `_cmd` internally,
// so just don't bother with setting the `_cmd` argument.
RequiresSelValue = false;
@@ -2138,6 +2154,23 @@ CodeGen::RValue CGObjCCommonMac::EmitMessageSend(
if (!RequiresNullCheck && Method && Method->hasParamDestroyedInCallee())
RequiresNullCheck = true;
+ if (CGM.shouldHaveNilCheckInline(Method)) {
+ // For variadic class methods, we need to inline pre condition checks. That
+ // include two things:
+ // 1. if this is a class method, we have to realize the class if we are not
+ // sure.
+ if (ClassReceiver && ClassObjectCanBeUnrealized) {
+ // Perform class realization using the helper function
+ Arg0 = GenerateClassRealization(CGF, Arg0, ClassReceiver);
+ ActualArgs[0] = CallArg(RValue::get(Arg0), ActualArgs[0].Ty);
+ }
+ // 2. inline the nil check if we are not sure if the receiver can be null.
+ // Luckly, `NullReturnState` already does that for corner cases like
+ // ns_consume, we only need to override the flag, even if return value is
+ // unused.
+ RequiresNullCheck |= ReceiverCanBeNull;
+ }
+
NullReturnState nullReturn;
if (RequiresNullCheck) {
nullReturn.init(CGF, Arg0);
@@ -3912,6 +3945,8 @@ CGObjCCommonMac::GenerateDirectMethod(const ObjCMethodDecl *OMD,
llvm::FunctionType *MethodTy =
Types.GetFunctionType(Types.arrangeObjCMethodDeclaration(OMD));
+ bool ExposeSymbol = CGM.shouldExposeSymbol(OMD);
+
if (OldFn) {
Fn = llvm::Function::Create(MethodTy, llvm::GlobalValue::ExternalLinkage,
"", &CGM.getModule());
@@ -3921,10 +3956,30 @@ CGObjCCommonMac::GenerateDirectMethod(const ObjCMethodDecl *OMD,
// Replace the cached implementation in the map.
I->second.Implementation = Fn;
+ llvm::Function *OldThunk = I->second.Thunk;
+ // If implementation was replaced, and old thunk exists, invalidate the old
+ // thunk
+ //
+ // TODO: ideally, new thunk shouldn't be necessary, if the different return
+ // type are just subclasses, at IR level they are just pointers, i.e. the
+ // NewThunk and the OldThunk are identical.
+ if (OldThunk) {
+ llvm::Function *NewThunk = GenerateObjCDirectThunk(OMD, CD, Fn);
+
+ // Replace all uses before erasing
+ NewThunk->takeName(OldThunk);
+ OldThunk->replaceAllUsesWith(NewThunk);
+ OldThunk->eraseFromParent();
+
+ I->second.Thunk = NewThunk;
+ }
} else {
- auto Name = getSymbolNameForMethod(OMD, /*include category*/ false);
+ // Generate symbol without \01 prefix when optimization enabled
+ auto Name = getSymbolNameForMethod(OMD, /*include category*/ false,
+ /*includePrefixByte*/ !ExposeSymbol);
+ // ALWAYS use ExternalLinkage for true implementation
Fn = llvm::Function::Create(MethodTy, llvm::GlobalValue::ExternalLinkage,
Name, &CGM.getModule());
auto [It, inserted] = DirectMethodDefinitions.insert(std::make_pair(COMD, DirectMethodInfo(Fn)));
@@ -3936,6 +3991,190 @@ CGObjCCommonMac::GenerateDirectMethod(const ObjCMethodDecl *OMD,
return I->second;
}
+/// Start an Objective-C direct method thunk.
+///
+/// The thunk must use musttail to remain transparent to ARC - any
+/// ARC operations must happen in the caller, not in the thunk.
+void CodeGenFunction::StartObjCDirectThunk(const ObjCMethodDecl *OMD,
+ llvm::Function *Fn,
+ const CGFunctionInfo &FI)
+ {
+ // Mark this as a thunk function to disable ARC parameter processing
+ // and other thunk-inappropriate behavior.
+ CurFuncIsThunk = true;
+
+ // Build argument list for StartFunction.
+ // We must include all parameters to match the thunk's LLVM function type.
+ // The thunk uses musttail to forward all arguments directly, so ARC
+ // processing in the prolog is harmless - the parameters are forwarded
+ // as-is without local copies.
+ FunctionArgList FunctionArgs;
+ FunctionArgs.push_back(OMD->getSelfDecl());
+ FunctionArgs.append(OMD->param_begin(), OMD->param_end());
+
+ // The Start/Finish thunk pattern is borrowed from CGVTables.cpp
+ // for C++ virtual method thunks, but adapted for ObjC direct methods.
+ //
+ // Like C++ thunks, we don't have an actual AST body for the thunk - we only
+ // have the method's parameter declarations. Therefore, we pass empty
+ // `GlobalDecl` to `StartFunction` ...
+ StartFunction(GlobalDecl(), OMD->getReturnType(), Fn, FI, FunctionArgs,
+ OMD->getLocation(), OMD->getLocation());
+
+ // and manually set the decl afterwards so other utilities / helpers in CGF
+ // can still access the AST (e.g. arrange function arguments)
+ CurCodeDecl = OMD;
+ CurFuncDecl = OMD;
+}
+
+/// Finish an Objective-C direct method thunk.
+void CodeGenFunction::FinishObjCDirectThunk() {
+ // Create a dummy block to return the value of the thunk.
+ //
+ // The non-nil branch alredy returned because of musttail.
+ // Only nil branch will jump to this return block.
+ // If the nil check is not emitted (for class methods), this will be a dead
+ // block.
+ //
+ // Either way, the LLVM optimizer will simplify it later. This is just to make
+ // CFG happy.
+ EmitBlock(createBasicBlock("dummy_ret_block"));
+
+ // Disable the final ARC autorelease.
+ // Thunk functions are tailcall to actual implementation, so it doesn't need
+ // to worry about ARC.
+ AutoreleaseResult = false;
+
+ // Clear these to restore the invariants expected by
+ // StartFunction/FinishFunction.
+ CurCodeDecl = nullptr;
+ CurFuncDecl = nullptr;
+
+ FinishFunction();
+}
+
+llvm::Function *
+CGObjCCommonMac::GenerateObjCDirectThunk(const ObjCMethodDecl *OMD,
+ const ObjCContainerDecl *CD,
+ llvm::Function *Implementation) {
+
+ assert(CGM.shouldHaveNilCheckThunk(OMD) &&
+ "Should only generate thunk when optimization enabled");
+ assert(Implementation && "Implementation must exist");
+
+ llvm::FunctionType *ThunkTy = Implementation->getFunctionType();
+ std::string ThunkName = Implementation->getName().str() + "_thunk";
+
+ // Create thunk with linkonce_odr linkage (allows deduplication)
+ llvm::Function *Thunk =
+ llvm::Function::Create(ThunkTy, llvm::GlobalValue::LinkOnceODRLinkage,
+ ThunkName, &CGM.getModule());
+
+ // Thunks should always have hidden visibility, other link units will have
+ // their own version of the (identical) thunk. If they make cross link-unit
+ // call, they are either calling through their thunk or directly dispatching
+ // to the true implementation, so making thunk visibile is meaningless.
+ Thunk->setVisibility(llvm::GlobalValue::HiddenVisibility);
+ Thunk->setUnnamedAddr(llvm::GlobalValue::UnnamedAddr::Global);
+
+ // Start the ObjC direct thunk (sets up state and calls StartFunction)
+ const CGFunctionInfo &FI = CGM.getTypes().arrangeObjCMethodDeclaration(OMD);
+
+ // Create a CodeGenFunction to generate the thunk body
+ CodeGenFunction CGF(CGM);
+ CGF.StartObjCDirectThunk(OMD, Thunk, FI);
+
+ // Copy function-level attributes from implementation to make musttail happy
+ llvm::AttributeList ImplAttrs = Implementation->getAttributes();
+ Thunk->setAttributes(ImplAttrs);
+
+ // - [self self] for class methods (class realization)
+ // - if (self == nil) branch to nil block with zero return
+ // - continuation block for non-nil case
+ GenerateDirectMethodsPreconditionCheck(CGF, Thunk, OMD, CD);
+
+ // Now emit the musttail call to the true implementation
+ // Collect all arguments for forwarding
+ SmallVector<llvm::Value *, 8> Args;
+ for (auto &Arg : Thunk->args())
+ Args.push_back(&Arg);
+
+ // Create musttail call to the implementation
+ llvm::CallInst *Call = CGF.Builder.CreateCall(Implementation, Args);
+ Call->setTailCallKind(llvm::CallInst::TCK_MustTail);
+
+ // Apply call-site attributes using ConstructAttributeList
+ // When sret is used, the call must have matching sret attributes on the first
+ // parameter for musttail to work correctly. This mirrors what C++ thunks do
+ // in EmitMustTailThunk.
+ unsigned CallingConv;
+ llvm::AttributeList Attrs;
+ CGM.ConstructAttributeList(Implementation->getName(), FI, GlobalDecl(OMD),
+ Attrs, CallingConv, /*AttrOnCallSite=*/true,
+ /*IsThunk=*/false);
+ Call->setAttributes(Attrs);
+ Call->setCallingConv(static_cast<llvm::CallingConv::ID>(CallingConv));
+
+ // Immediately return the call result (musttail requirement)
+ if (FI.getReturnInfo().isIndirect()) {
+ // SRet case: return void
+ CGF.Builder.CreateRetVoid();
+ } else {
+ if (ThunkTy->getReturnType()->isVoidTy())
+ CGF.Builder.CreateRetVoid();
+ else
+ CGF.Builder.CreateRet(Call);
+ }
+
+ // Finish the ObjC direct thunk (creates dummy block and calls FinishFunction)
+ CGF.FinishObjCDirectThunk();
+ return Thunk;
+}
+
+llvm::Function *CGObjCCommonMac::GetDirectMethodCallee(
+ const ObjCMethodDecl *OMD, const ObjCContainerDecl *CD,
+ bool ReceiverCanBeNull, bool ClassObjectCanBeUnrealized) {
+
+ // Get from cache or populate the function declaration lazily
+ DirectMethodInfo &Info = GenerateDirectMethod(OMD, CD);
+
+ // If optimization not enabled, always use implementation (which includes the
+ // nil check)
+ if (!CGM.shouldExposeSymbol(OMD)) {
+ return Info.Implementation;
+ }
+
+ // Varidic methods doesn't have thunk, the caller need to inline the nil check
+ if (CGM.shouldHaveNilCheckInline(OMD)) {
+ return Info.Implementation;
+ }
+
+ // Thunk is lazily generated.
+ auto getOrCreateThunk = [&]() {
+ if (!Info.Thunk)
+ Info.Thunk = GenerateObjCDirectThunk(OMD, CD, Info.Implementation);
+ return Info.Thunk;
+ };
+
+ assert(CGM.shouldHaveNilCheckThunk(OMD) &&
+ "a method either has nil check thunk or have thunk inlined when "
+ "exposing its symbol");
+
+ if (OMD->isInstanceMethod()) {
+ // If we can prove instance methods receiver is not null, return the true
+ // implementation
+ return ReceiverCanBeNull ? getOrCreateThunk() : Info.Implementation;
+ }
+ if (OMD->isClassMethod()) {
+ // For class methods, it need to be non-null and realized before we dispatch
+ // to true implementation
+ return (ReceiverCanBeNull || ClassObjectCanBeUnrealized)
+ ? getOrCreateThunk()
+ : Info.Implementation;
+ }
+ llvm_unreachable("OMD should either be a class method or instance method");
+}
+
llvm::Value *
CGObjCCommonMac::GenerateClassRealization(CodeGenFunction &CGF,
llvm::Value *classObject,
diff --git a/clang/lib/CodeGen/CodeGenFunction.h b/clang/lib/CodeGen/CodeGenFunction.h
index 8c4c1c8c2dc95..f507146b37cc5 100644
--- a/clang/lib/CodeGen/CodeGenFunction.h
+++ b/clang/lib/CodeGen/CodeGenFunction.h
@@ -2422,6 +2422,13 @@ class CodeGenFunction : public CodeGenTypeCache {
void FinishThunk();
+ /// Start an Objective-C direct method thunk.
+ void StartObjCDirectThunk(const ObjCMethodDecl *OMD, llvm::Function *Fn,
+ const CGFunctionInfo &FI);
+
+ /// Finish an Objective-C direct method thunk.
+ void FinishObjCDirectThunk();
+
/// Emit a musttail call for a thunk with a potentially adjusted this pointer.
void EmitMustTailThunk(GlobalDecl GD, llvm::Value *AdjustedThisPtr,
llvm::FunctionCallee Callee);
More information about the cfe-commits
mailing list