[lldb] [llvm] [lldb][GNUstep] Objective-C debugging support for the libobjc2 runtime (overview, do not merge) (PR #216709)
via llvm-commits
llvm-commits at lists.llvm.org
Thu Aug 20 06:00:50 PDT 2026
https://github.com/robk-dev updated https://github.com/llvm/llvm-project/pull/216709
>From 35c6e9140489b66b07f4df818b9c6f268434d191 Mon Sep 17 00:00:00 2001
From: Rob <robk at robk.dev>
Date: Tue, 11 Aug 2026 20:58:28 +0100
Subject: [PATCH 01/49] [lldb][GNUstep] Add class descriptors, ISA map, and
dynamic type resolution
Implement runtime introspection for the GNUstep libobjc2 runtime using
only memory reads - no code is ever executed in the inferior:
- GNUstepObjCClassDescriptor parses libobjc2's struct objc_class
(metaclass, superclass, name, instance size) directly from memory.
- GNUstepTaggedPointerVendor mirrors libobjc2's classForObject(): tagged
("small object") pointers are detected via the low tag bits and their
class resolved by reading the runtime's SmallObjectClasses table,
located by symbol.
- UpdateISAToDescriptorMapIfNeeded seeds the ISA-to-descriptor map from
the `._OBJC_CLASS_<name>` data symbols the gnustep-2.x ABI emits for
every compiled class; GetClassDescriptorFromISA falls back to parsing
unknown ISAs so runtime-registered classes resolve too.
- GetDynamicTypeAndAddress resolves dynamic types via the descriptors,
upgrading the class name to a real type through the complete-class
cache when the inferior's debug info defines the class.
Assisted-by: Claude Fable 5
---
.../ObjC/GNUstepObjCRuntime/CMakeLists.txt | 1 +
.../GNUstepObjCClassDescriptor.cpp | 197 ++++++++++++++++++
.../GNUstepObjCClassDescriptor.h | 144 +++++++++++++
.../GNUstepObjCRuntime/GNUstepObjCRuntime.cpp | 130 +++++++++++-
.../GNUstepObjCRuntime/GNUstepObjCRuntime.h | 19 ++
.../Shell/Expr/objc-gnustep-dynamic-types.m | 55 +++++
6 files changed, 543 insertions(+), 3 deletions(-)
create mode 100644 lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCClassDescriptor.cpp
create mode 100644 lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCClassDescriptor.h
create mode 100644 lldb/test/Shell/Expr/objc-gnustep-dynamic-types.m
diff --git a/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/CMakeLists.txt b/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/CMakeLists.txt
index 05caad3e7d220..8fd8ffb4bc57f 100644
--- a/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/CMakeLists.txt
+++ b/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/CMakeLists.txt
@@ -1,4 +1,5 @@
add_lldb_library(lldbPluginGNUstepObjCRuntime PLUGIN
+ GNUstepObjCClassDescriptor.cpp
GNUstepObjCRuntime.cpp
LINK_COMPONENTS
diff --git a/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCClassDescriptor.cpp b/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCClassDescriptor.cpp
new file mode 100644
index 0000000000000..816b2cae1ade8
--- /dev/null
+++ b/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCClassDescriptor.cpp
@@ -0,0 +1,197 @@
+//===-- GNUstepObjCClassDescriptor.cpp ------------------------------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+#include "GNUstepObjCClassDescriptor.h"
+
+#include "lldb/Core/Module.h"
+#include "lldb/Core/ModuleList.h"
+#include "lldb/Symbol/Symbol.h"
+#include "lldb/Symbol/SymbolContext.h"
+#include "lldb/Target/Process.h"
+#include "lldb/Target/Target.h"
+#include "lldb/Utility/ConstString.h"
+#include "lldb/Utility/LLDBLog.h"
+#include "lldb/Utility/Log.h"
+#include "lldb/Utility/Status.h"
+
+using namespace lldb;
+using namespace lldb_private;
+
+// Field indices into libobjc2's `struct objc_class` (see class documentation
+// in the header).
+static constexpr uint64_t kClassFieldIsa = 0;
+static constexpr uint64_t kClassFieldSuperclass = 1;
+static constexpr uint64_t kClassFieldName = 2;
+static constexpr uint64_t kClassFieldInstanceSize = 5;
+
+// An upper bound for plausible class names; longer strings indicate that the
+// name pointer does not actually point at a class name.
+static constexpr size_t kMaxClassNameLength = 512;
+
+GNUstepObjCClassDescriptor::GNUstepObjCClassDescriptor(
+ ProcessSP process_sp, ObjCLanguageRuntime::ObjCISA isa)
+ : m_process_wp(process_sp), m_isa(isa) {
+ Read();
+}
+
+void GNUstepObjCClassDescriptor::Read() {
+ ProcessSP process_sp = m_process_wp.lock();
+ if (!process_sp || m_isa == 0 || m_isa == LLDB_INVALID_ADDRESS)
+ return;
+
+ const uint32_t addr_size = process_sp->GetAddressByteSize();
+ // Class objects are at least pointer-aligned.
+ if (m_isa % addr_size != 0)
+ return;
+
+ Status error;
+ auto read_field = [&](uint64_t index) -> addr_t {
+ addr_t value = process_sp->ReadPointerFromMemory(
+ m_isa + index * addr_size, error);
+ return error.Fail() ? LLDB_INVALID_ADDRESS : value;
+ };
+
+ const addr_t metaclass = read_field(kClassFieldIsa);
+ if (metaclass == LLDB_INVALID_ADDRESS)
+ return;
+ const addr_t superclass = read_field(kClassFieldSuperclass);
+ if (superclass == LLDB_INVALID_ADDRESS)
+ return;
+ const addr_t name_ptr = read_field(kClassFieldName);
+ if (name_ptr == LLDB_INVALID_ADDRESS || name_ptr == 0)
+ return;
+
+ std::string name;
+ process_sp->ReadCStringFromMemory(name_ptr, name, error);
+ if (error.Fail() || name.empty() || name.size() >= kMaxClassNameLength)
+ return;
+
+ // `instance_size` is a signed `long`. With the non-fragile ABI it is
+ // negative until the runtime registers the class; take the magnitude so a
+ // not-yet-registered class still yields a usable size.
+ const int64_t instance_size = process_sp->ReadSignedIntegerFromMemory(
+ m_isa + kClassFieldInstanceSize * addr_size, addr_size, 0, error);
+ if (error.Fail())
+ return;
+
+ m_metaclass_isa = metaclass;
+ m_superclass_isa = superclass;
+ m_name = ConstString(name);
+ m_instance_size = static_cast<uint64_t>(
+ instance_size < 0 ? -instance_size : instance_size);
+ m_valid = true;
+}
+
+ObjCLanguageRuntime::ClassDescriptorSP
+GNUstepObjCClassDescriptor::GetSuperclass() {
+ if (!m_valid || m_superclass_isa == 0)
+ return ObjCLanguageRuntime::ClassDescriptorSP();
+ // A class that is its own superclass would make any walk up the chain spin
+ // forever. The chain comes from inferior memory, so refuse the edge here
+ // rather than leaving every caller to remember. Longer cycles cannot be
+ // seen from a single descriptor and still need the caller's depth bound.
+ if (m_superclass_isa == m_isa)
+ return ObjCLanguageRuntime::ClassDescriptorSP();
+ ProcessSP process_sp = m_process_wp.lock();
+ if (!process_sp)
+ return ObjCLanguageRuntime::ClassDescriptorSP();
+ return std::make_shared<GNUstepObjCClassDescriptor>(process_sp,
+ m_superclass_isa);
+}
+
+std::unique_ptr<ObjCLanguageRuntime::ClassDescriptor>
+GNUstepObjCClassDescriptor::GetMetaclass() const {
+ if (!m_valid || m_metaclass_isa == 0)
+ return nullptr;
+ ProcessSP process_sp = m_process_wp.lock();
+ if (!process_sp)
+ return nullptr;
+ return std::make_unique<GNUstepObjCClassDescriptor>(process_sp,
+ m_metaclass_isa);
+}
+
+bool GNUstepObjCTaggedPointerClassDescriptor::GetTaggedPointerInfo(
+ uint64_t *info_bits, uint64_t *value_bits, uint64_t *payload) {
+ if (info_bits)
+ *info_bits = m_tag;
+ if (value_bits)
+ *value_bits = m_pointer_value >> m_payload_shift;
+ if (payload)
+ *payload = m_pointer_value;
+ return true;
+}
+
+bool GNUstepObjCTaggedPointerClassDescriptor::GetTaggedPointerInfoSigned(
+ uint64_t *info_bits, int64_t *value_bits, uint64_t *payload) {
+ if (info_bits)
+ *info_bits = m_tag;
+ if (value_bits)
+ *value_bits =
+ static_cast<int64_t>(m_pointer_value) >> m_payload_shift;
+ if (payload)
+ *payload = m_pointer_value;
+ return true;
+}
+
+bool GNUstepTaggedPointerVendor::IsPossibleTaggedPointer(lldb::addr_t ptr) {
+ const uint64_t mask = m_process.GetAddressByteSize() == 8 ? 7 : 1;
+ return (ptr & mask) != 0;
+}
+
+std::unique_ptr<ObjCLanguageRuntime::ClassDescriptor>
+GNUstepTaggedPointerVendor::GetClassDescriptor(lldb::addr_t ptr) {
+ const bool is_64_bit = m_process.GetAddressByteSize() == 8;
+ const uint64_t mask = is_64_bit ? 7 : 1;
+ const uint32_t payload_shift = is_64_bit ? 3 : 1;
+ const uint64_t tag = ptr & mask;
+ if (tag == 0)
+ return nullptr;
+
+ // Mirror libobjc2's classForObject(): 32-bit targets have a single small
+ // object class at index 0; 64-bit targets index the table by the tag. The
+ // table has 7 entries, so reject out-of-range tags.
+ const uint64_t index = is_64_bit ? tag : 0;
+ if (index > 6)
+ return nullptr;
+
+ if (!m_table_addr) {
+ m_table_addr = LLDB_INVALID_ADDRESS;
+ Target &target = m_process.GetTarget();
+ SymbolContextList sc_list;
+ target.GetImages().FindSymbolsWithNameAndType(
+ ConstString("SmallObjectClasses"), eSymbolTypeAny, sc_list);
+ for (const SymbolContext &sc : sc_list) {
+ if (!sc.symbol)
+ continue;
+ const addr_t table = sc.symbol->GetAddress().GetLoadAddress(&target);
+ if (table != LLDB_INVALID_ADDRESS) {
+ m_table_addr = table;
+ break;
+ }
+ }
+ if (*m_table_addr == LLDB_INVALID_ADDRESS)
+ LLDB_LOG(GetLog(LLDBLog::Language),
+ "GNUstepTaggedPointerVendor: SmallObjectClasses symbol not "
+ "found (stripped libobjc?); tagged pointer classes unknown");
+ }
+ if (*m_table_addr == LLDB_INVALID_ADDRESS)
+ return nullptr;
+
+ Status error;
+ const addr_t isa = m_process.ReadPointerFromMemory(
+ *m_table_addr + index * m_process.GetAddressByteSize(), error);
+ if (error.Fail() || isa == 0 || isa == LLDB_INVALID_ADDRESS)
+ return nullptr;
+
+ auto descriptor_up =
+ std::make_unique<GNUstepObjCTaggedPointerClassDescriptor>(
+ m_process.shared_from_this(), isa, ptr, tag, payload_shift);
+ if (!descriptor_up->IsValid())
+ return nullptr;
+ return descriptor_up;
+}
diff --git a/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCClassDescriptor.h b/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCClassDescriptor.h
new file mode 100644
index 0000000000000..949b9f98ca9d6
--- /dev/null
+++ b/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCClassDescriptor.h
@@ -0,0 +1,144 @@
+//===-- GNUstepObjCClassDescriptor.h ----------------------------*- C++ -*-===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+#ifndef LLDB_SOURCE_PLUGINS_LANGUAGERUNTIME_OBJC_GNUSTEPOBJCRUNTIME_GNUSTEPOBJCCLASSDESCRIPTOR_H
+#define LLDB_SOURCE_PLUGINS_LANGUAGERUNTIME_OBJC_GNUSTEPOBJCRUNTIME_GNUSTEPOBJCCLASSDESCRIPTOR_H
+
+#include "Plugins/LanguageRuntime/ObjC/ObjCLanguageRuntime.h"
+
+#include "lldb/lldb-forward.h"
+#include "lldb/lldb-types.h"
+
+#include <optional>
+
+namespace lldb_private {
+
+/// A class descriptor for classes of the GNUstep libobjc2 runtime, backed
+/// entirely by reads of the inferior's memory - no code is ever executed in
+/// the inferior.
+///
+/// The layout parsed here is libobjc2's `struct objc_class` (class.h), whose
+/// leading fields have been stable across the gnustep-2.x ABI:
+///
+/// Class isa; // metaclass [index 0]
+/// Class super_class; // [index 1]
+/// const char *name; // [index 2]
+/// long version; // [index 3]
+/// unsigned long info; // flag bits [index 4]
+/// long instance_size; // [index 5]
+///
+/// Note: with the non-fragile ABI the compiler emits a negative
+/// instance_size; the runtime replaces it with the real size when the class
+/// is registered, so debug-time reads of loaded classes see the real value.
+class GNUstepObjCClassDescriptor : public ObjCLanguageRuntime::ClassDescriptor {
+public:
+ GNUstepObjCClassDescriptor(lldb::ProcessSP process_sp,
+ ObjCLanguageRuntime::ObjCISA isa);
+
+ ~GNUstepObjCClassDescriptor() override = default;
+
+ ConstString GetClassName() override { return m_name; }
+
+ ObjCLanguageRuntime::ClassDescriptorSP GetSuperclass() override;
+
+ std::unique_ptr<ObjCLanguageRuntime::ClassDescriptor>
+ GetMetaclass() const override;
+
+ bool IsValid() override { return m_valid; }
+
+ bool GetTaggedPointerInfo(uint64_t *info_bits = nullptr,
+ uint64_t *value_bits = nullptr,
+ uint64_t *payload = nullptr) override {
+ return false;
+ }
+
+ bool GetTaggedPointerInfoSigned(uint64_t *info_bits = nullptr,
+ int64_t *value_bits = nullptr,
+ uint64_t *payload = nullptr) override {
+ return false;
+ }
+
+ uint64_t GetInstanceSize() override { return m_instance_size; }
+
+ ObjCLanguageRuntime::ObjCISA GetISA() override { return m_isa; }
+
+protected:
+ /// Parse `struct objc_class` at m_isa. Called from the constructor;
+ /// sets m_valid on success.
+ void Read();
+
+ lldb::ProcessWP m_process_wp;
+ ObjCLanguageRuntime::ObjCISA m_isa = 0;
+ ConstString m_name;
+ ObjCLanguageRuntime::ObjCISA m_superclass_isa = 0;
+ ObjCLanguageRuntime::ObjCISA m_metaclass_isa = 0;
+ uint64_t m_instance_size = 0;
+ bool m_valid = false;
+};
+
+/// Class descriptor for libobjc2 "small objects" (tagged pointers). The
+/// pointed-to class is the entry of the runtime's `SmallObjectClasses` table
+/// selected by the low tag bits; the descriptor additionally exposes the
+/// payload via GetTaggedPointerInfo.
+class GNUstepObjCTaggedPointerClassDescriptor
+ : public GNUstepObjCClassDescriptor {
+public:
+ GNUstepObjCTaggedPointerClassDescriptor(lldb::ProcessSP process_sp,
+ ObjCLanguageRuntime::ObjCISA isa,
+ lldb::addr_t pointer_value,
+ uint64_t tag, uint32_t payload_shift)
+ : GNUstepObjCClassDescriptor(std::move(process_sp), isa),
+ m_pointer_value(pointer_value), m_tag(tag),
+ m_payload_shift(payload_shift) {}
+
+ bool GetTaggedPointerInfo(uint64_t *info_bits = nullptr,
+ uint64_t *value_bits = nullptr,
+ uint64_t *payload = nullptr) override;
+
+ bool GetTaggedPointerInfoSigned(uint64_t *info_bits = nullptr,
+ int64_t *value_bits = nullptr,
+ uint64_t *payload = nullptr) override;
+
+private:
+ lldb::addr_t m_pointer_value;
+ uint64_t m_tag;
+ uint32_t m_payload_shift;
+};
+
+/// Resolves tagged pointers by mirroring libobjc2's `classForObject()`
+/// (class.h): a pointer with any of the low tag bits set (3 bits on 64-bit
+/// targets, 1 bit on 32-bit targets) is a small object whose class is
+/// `SmallObjectClasses[tag]` (index 0 on 32-bit targets).
+///
+/// `SmallObjectClasses` has hidden visibility, so resolving it requires the
+/// library's .symtab (present in unstripped builds). If it cannot be
+/// resolved, tagged pointers are still detected but their class is unknown.
+class GNUstepTaggedPointerVendor
+ : public ObjCLanguageRuntime::TaggedPointerVendor {
+public:
+ explicit GNUstepTaggedPointerVendor(Process &process) : m_process(process) {}
+
+ ~GNUstepTaggedPointerVendor() override = default;
+
+ bool IsPossibleTaggedPointer(lldb::addr_t ptr) override;
+
+ std::unique_ptr<ObjCLanguageRuntime::ClassDescriptor>
+ GetClassDescriptor(lldb::addr_t ptr) override;
+
+private:
+ /// Load address of libobjc2's `SmallObjectClasses` table, resolved lazily
+ /// and cached. LLDB_INVALID_ADDRESS inside the optional means resolution
+ /// was attempted and failed.
+ std::optional<lldb::addr_t> m_table_addr;
+
+ Process &m_process;
+};
+
+} // namespace lldb_private
+
+#endif // LLDB_SOURCE_PLUGINS_LANGUAGERUNTIME_OBJC_GNUSTEPOBJCRUNTIME_GNUSTEPOBJCCLASSDESCRIPTOR_H
diff --git a/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCRuntime.cpp b/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCRuntime.cpp
index c317f6478fe74..5785e23a71bae 100644
--- a/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCRuntime.cpp
+++ b/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCRuntime.cpp
@@ -7,17 +7,26 @@
//===----------------------------------------------------------------------===//
#include "GNUstepObjCRuntime.h"
+#include "GNUstepObjCClassDescriptor.h"
#include "Plugins/TypeSystem/Clang/TypeSystemClang.h"
#include "lldb/Core/Module.h"
+#include "lldb/Core/ModuleList.h"
#include "lldb/Core/PluginManager.h"
#include "lldb/Expression/UtilityFunction.h"
+#include "lldb/Symbol/DeclVendor.h"
+#include "lldb/Symbol/Symbol.h"
+#include "lldb/Symbol/SymbolContext.h"
#include "lldb/Target/ExecutionContext.h"
#include "lldb/Target/Process.h"
#include "lldb/Target/Target.h"
#include "lldb/Utility/ArchSpec.h"
#include "lldb/Utility/ConstString.h"
+#include "lldb/Utility/LLDBLog.h"
+#include "lldb/Utility/Log.h"
+#include "lldb/Utility/RegularExpression.h"
+#include "lldb/Utility/StructuredData.h"
#include "lldb/ValueObject/ValueObject.h"
using namespace lldb;
@@ -100,7 +109,9 @@ LanguageRuntime *GNUstepObjCRuntime::CreateInstance(Process *process,
GNUstepObjCRuntime::~GNUstepObjCRuntime() = default;
GNUstepObjCRuntime::GNUstepObjCRuntime(Process *process)
- : ObjCLanguageRuntime(process), m_objc_module_sp(nullptr) {
+ : ObjCLanguageRuntime(process), m_objc_module_sp(nullptr),
+ m_tagged_pointer_vendor_up(
+ std::make_unique<GNUstepTaggedPointerVendor>(*process)) {
ReadObjCLibraryIfNeeded(process->GetTarget().GetImages());
}
@@ -128,7 +139,42 @@ bool GNUstepObjCRuntime::GetDynamicTypeAndAddress(
ValueObject &in_value, DynamicValueType use_dynamic,
TypeAndOrName &class_type_or_name, Address &address,
Value::ValueType &value_type, llvm::ArrayRef<uint8_t> &local_buffer) {
- return false;
+ class_type_or_name.Clear();
+ value_type = Value::ValueType::Scalar;
+
+ if (!CouldHaveDynamicValue(in_value))
+ return false;
+
+ ClassDescriptorSP objc_class_sp(GetNonKVOClassDescriptor(in_value));
+ if (!objc_class_sp)
+ return false;
+
+ ConstString class_name(objc_class_sp->GetClassName());
+ if (!class_name)
+ return false;
+
+ const addr_t object_ptr = in_value.GetPointerValue().address;
+ address.SetRawAddress(object_ptr);
+ class_type_or_name.SetName(class_name);
+
+ // Try to upgrade the bare name to a real type: first from the cache of
+ // classes already realized from debug info, then - should a decl vendor
+ // exist one day - from that.
+ TypeSP type_sp(objc_class_sp->GetType());
+ if (!type_sp) {
+ type_sp = LookupInCompleteClassCache(class_name);
+ if (type_sp)
+ objc_class_sp->SetType(type_sp);
+ }
+ if (type_sp)
+ class_type_or_name.SetTypeSP(type_sp);
+ else if (auto *vendor = GetDeclVendor()) {
+ auto types = vendor->FindTypes(class_name, /*max_matches*/ 1);
+ if (!types.empty())
+ class_type_or_name.SetCompilerType(types.front());
+ }
+
+ return !class_type_or_name.IsEmpty();
}
TypeAndOrName
@@ -205,7 +251,76 @@ GNUstepObjCRuntime::GetStepThroughTrampolinePlan(Thread &thread,
}
void GNUstepObjCRuntime::UpdateISAToDescriptorMapIfNeeded() {
- // TODO: Support lazily named and dynamically loaded Objective-C classes
+ if (!m_process)
+ return;
+ const uint32_t stop_id = m_process->GetStopID();
+ if (!m_isa_map_dirty) {
+ m_isa_to_descriptor_stop_id = stop_id;
+ return;
+ }
+
+ // The gnustep-2.x ABI emits every compiled class as a `._OBJC_CLASS_<name>`
+ // data symbol whose address is the class object itself (the ISA of its
+ // instances), so the map can be seeded from symbol tables alone - without
+ // running any code in the inferior. Classes created dynamically at runtime
+ // are handled by the create-on-miss path in GetClassDescriptorFromISA.
+ Target &target = GetTargetRef();
+ const ModuleList &images = target.GetImages();
+
+ SymbolContextList sc_list;
+ RegularExpression regex(llvm::StringRef("^\\._OBJC_CLASS_"));
+ images.FindSymbolsMatchingRegExAndType(regex, eSymbolTypeAny, sc_list);
+
+ static constexpr llvm::StringLiteral g_class_prefix("._OBJC_CLASS_");
+ for (const SymbolContext &sc : sc_list) {
+ if (!sc.symbol)
+ continue;
+ const addr_t isa = sc.symbol->GetAddress().GetLoadAddress(&target);
+ if (isa == 0 || isa == LLDB_INVALID_ADDRESS || ISAIsCached(isa))
+ continue;
+ llvm::StringRef name = sc.symbol->GetName().GetStringRef();
+ name.consume_front(g_class_prefix);
+ auto descriptor_sp = std::make_shared<GNUstepObjCClassDescriptor>(
+ m_process->shared_from_this(), isa);
+ if (descriptor_sp->IsValid())
+ AddClass(isa, descriptor_sp, name.str().c_str());
+ }
+
+ m_isa_map_dirty = false;
+ m_isa_to_descriptor_stop_id = stop_id;
+}
+
+ObjCLanguageRuntime::TaggedPointerVendor *
+GNUstepObjCRuntime::GetTaggedPointerVendor() {
+ return m_tagged_pointer_vendor_up.get();
+}
+
+ObjCLanguageRuntime::ClassDescriptorSP
+GNUstepObjCRuntime::GetClassDescriptor(ValueObject &in_value) {
+ const addr_t ptr = in_value.GetPointerValue().address;
+ if (ptr != LLDB_INVALID_ADDRESS && m_tagged_pointer_vendor_up &&
+ m_tagged_pointer_vendor_up->IsPossibleTaggedPointer(ptr))
+ return m_tagged_pointer_vendor_up->GetClassDescriptor(ptr);
+ return ObjCLanguageRuntime::GetClassDescriptor(in_value);
+}
+
+ObjCLanguageRuntime::ClassDescriptorSP
+GNUstepObjCRuntime::GetClassDescriptorFromISA(ObjCISA isa) {
+ if (ClassDescriptorSP descriptor_sp =
+ ObjCLanguageRuntime::GetClassDescriptorFromISA(isa))
+ return descriptor_sp;
+
+ // The symbol sweep only sees classes with static definitions. Fall back to
+ // parsing the class structure directly so classes registered at runtime
+ // (e.g. via objc_allocateClassPair) resolve as well.
+ if (!m_process || isa == 0 || isa == LLDB_INVALID_ADDRESS)
+ return ClassDescriptorSP();
+ auto descriptor_sp = std::make_shared<GNUstepObjCClassDescriptor>(
+ m_process->shared_from_this(), isa);
+ if (!descriptor_sp->IsValid())
+ return ClassDescriptorSP();
+ AddClass(isa, descriptor_sp, descriptor_sp->GetClassName().GetCString());
+ return descriptor_sp;
}
bool GNUstepObjCRuntime::IsModuleObjCLibrary(const ModuleSP &module_sp) {
@@ -222,6 +337,15 @@ bool GNUstepObjCRuntime::ReadObjCLibrary(const ModuleSP &module_sp) {
return true;
}
+StructuredData::ObjectSP
+GNUstepObjCRuntime::GetLanguageSpecificData(SymbolContext sc) {
+ auto dict_up = std::make_unique<StructuredData::Dictionary>();
+ dict_up->AddItem("Objective-C runtime version",
+ std::make_unique<StructuredData::UnsignedInteger>(2));
+ return dict_up;
+}
+
void GNUstepObjCRuntime::ModulesDidLoad(const ModuleList &module_list) {
ReadObjCLibraryIfNeeded(module_list);
+ m_isa_map_dirty = true;
}
diff --git a/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCRuntime.h b/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCRuntime.h
index 94a5c9e1261a8..0c8f1d0911b72 100644
--- a/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCRuntime.h
+++ b/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCRuntime.h
@@ -17,10 +17,13 @@
#include "llvm/ADT/StringRef.h"
#include "llvm/Support/Error.h"
+#include <memory>
#include <optional>
namespace lldb_private {
+class GNUstepTaggedPointerVendor;
+
class GNUstepObjCRuntime : public lldb_private::ObjCLanguageRuntime {
public:
~GNUstepObjCRuntime() override;
@@ -93,17 +96,33 @@ class GNUstepObjCRuntime : public lldb_private::ObjCLanguageRuntime {
llvm::Expected<std::unique_ptr<UtilityFunction>>
CreateObjectChecker(std::string name, ExecutionContext &exe_ctx) override;
+ /// Reported by `statistics dump` and the SB API, as AppleObjCRuntimeV2
+ /// does. libobjc2 implements the GNUstep Objective-C ABI version 2.
+ StructuredData::ObjectSP GetLanguageSpecificData(SymbolContext sc) override;
+
ObjCRuntimeVersions GetRuntimeVersion() const override {
return ObjCRuntimeVersions::eGNUstep_libobjc2;
}
void UpdateISAToDescriptorMapIfNeeded() override;
+ TaggedPointerVendor *GetTaggedPointerVendor() override;
+
+ ClassDescriptorSP GetClassDescriptor(ValueObject &in_value) override;
+
+ ClassDescriptorSP GetClassDescriptorFromISA(ObjCISA isa) override;
+
protected:
// Call CreateInstance instead.
GNUstepObjCRuntime(Process *process);
lldb::ModuleSP m_objc_module_sp;
+
+ std::unique_ptr<GNUstepTaggedPointerVendor> m_tagged_pointer_vendor_up;
+
+ /// Set when new modules arrive; cleared once the ISA-to-descriptor map has
+ /// been refreshed, so the symbol sweep only reruns after module changes.
+ bool m_isa_map_dirty = true;
};
} // namespace lldb_private
diff --git a/lldb/test/Shell/Expr/objc-gnustep-dynamic-types.m b/lldb/test/Shell/Expr/objc-gnustep-dynamic-types.m
new file mode 100644
index 0000000000000..6c43f2df26223
--- /dev/null
+++ b/lldb/test/Shell/Expr/objc-gnustep-dynamic-types.m
@@ -0,0 +1,55 @@
+// REQUIRES: objc-gnustep
+// XFAIL: system-windows
+//
+// RUN: %build %s --compiler=clang --objc-gnustep --output=%t
+
+#import "objc/runtime.h"
+
+ at protocol NSCoding
+ at end
+
+#ifdef __has_attribute
+#if __has_attribute(objc_root_class)
+__attribute__((objc_root_class))
+#endif
+#endif
+ at interface NSObject <NSCoding> {
+ id isa;
+ int refcount;
+}
+ at end
+ at implementation NSObject
++ (id)new {
+ return class_createInstance(self, 0);
+}
+ at end
+
+ at interface Base : NSObject
+ at end
+ at implementation Base
+ at end
+
+ at interface Derived : Base
+ at end
+ at implementation Derived
+ at end
+
+// The static type of `object` is Base, but the dynamic type is Derived. The
+// GNUstep runtime resolves the dynamic type by reading the class structure
+// from the inferior's memory (no code is run in the inferior).
+//
+// RUN: %lldb -b -o "b objc-gnustep-dynamic-types.m:48" -o "run" \
+// RUN: -o "frame variable -d run-target object" \
+// RUN: -o "frame variable -d no-dynamic-values object" -- %t | FileCheck %s
+//
+int main() {
+ Base *object = [Derived new];
+ (void)object;
+ return 0;
+}
+//
+// CHECK: (lldb) frame variable -d run-target object
+// CHECK: (Derived *) object = 0x
+//
+// CHECK: (lldb) frame variable -d no-dynamic-values object
+// CHECK: (Base *) object = 0x
>From bf485c84eb917b4c2943dde9f97ed6eeee8d0e30 Mon Sep 17 00:00:00 2001
From: Rob <robk at robk.dev>
Date: Tue, 11 Aug 2026 21:01:27 +0100
Subject: [PATCH 02/49] [lldb][GNUstep] Implement object description via
_NSPrintForDebugger
gnustep-base ships the same `const char *_NSPrintForDebugger(id)`
debugger hook that AppleObjCRuntime already uses on Darwin, so `po` can
follow the exact same proven mechanics: resolve the hook by symbol, call
it through a cached FunctionCaller with utility-expression options, and
read back the returned C string.
The call thunk is compiled as plain C with the object passed as void *,
so this works without any Objective-C support in the expression parser.
When gnustep-base is not loaded (bare libobjc2 inferiors), po reports a
clear error instead of failing obscurely.
Assisted-by: Claude Fable 5
---
.../GNUstepObjCRuntime/GNUstepObjCRuntime.cpp | 145 +++++++++++++++++-
.../GNUstepObjCRuntime/GNUstepObjCRuntime.h | 9 ++
lldb/test/Shell/Expr/objc-gnustep-print.m | 15 ++
3 files changed, 165 insertions(+), 4 deletions(-)
diff --git a/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCRuntime.cpp b/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCRuntime.cpp
index 5785e23a71bae..f2e53892f3fe8 100644
--- a/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCRuntime.cpp
+++ b/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCRuntime.cpp
@@ -11,9 +11,13 @@
#include "Plugins/TypeSystem/Clang/TypeSystemClang.h"
+#include "lldb/Core/Address.h"
#include "lldb/Core/Module.h"
#include "lldb/Core/ModuleList.h"
#include "lldb/Core/PluginManager.h"
+#include "lldb/Core/Value.h"
+#include "lldb/Expression/DiagnosticManager.h"
+#include "lldb/Expression/FunctionCaller.h"
#include "lldb/Expression/UtilityFunction.h"
#include "lldb/Symbol/DeclVendor.h"
#include "lldb/Symbol/Symbol.h"
@@ -21,6 +25,7 @@
#include "lldb/Target/ExecutionContext.h"
#include "lldb/Target/Process.h"
#include "lldb/Target/Target.h"
+#include "lldb/Target/Thread.h"
#include "lldb/Utility/ArchSpec.h"
#include "lldb/Utility/ConstString.h"
#include "lldb/Utility/LLDBLog.h"
@@ -115,17 +120,149 @@ GNUstepObjCRuntime::GNUstepObjCRuntime(Process *process)
ReadObjCLibraryIfNeeded(process->GetTarget().GetImages());
}
+Address *GNUstepObjCRuntime::GetPrintForDebuggerAddr() {
+ if (!m_print_for_debugger_addr_up) {
+ SymbolContextList sc_list;
+ GetTargetRef().GetImages().FindSymbolsWithNameAndType(
+ ConstString("_NSPrintForDebugger"), eSymbolTypeCode, sc_list);
+ for (const SymbolContext &sc : sc_list) {
+ if (!sc.symbol)
+ continue;
+ m_print_for_debugger_addr_up =
+ std::make_unique<Address>(sc.symbol->GetAddress());
+ break;
+ }
+ }
+ return m_print_for_debugger_addr_up.get();
+}
+
llvm::Error GNUstepObjCRuntime::GetObjectDescription(Stream &str,
ValueObject &valobj) {
- return llvm::createStringError(
- "LLDB's GNUStep runtime does not support object description");
+ CompilerType compiler_type(valobj.GetCompilerType());
+ bool is_signed;
+ // ObjC objects can only be pointers (or numbers that actually represent
+ // pointers but haven't been typecast).
+ if (!compiler_type.IsIntegerType(is_signed) && !compiler_type.IsPointerType())
+ return llvm::createStringError("not a pointer type");
+
+ Value val;
+ if (!valobj.ResolveValue(val.GetScalar()))
+ return llvm::createStringError("pointer value could not be resolved");
+
+ // Value objects may not have a process in their ExecutionContextRef. But
+ // we need one in the context we pass down to eventually call description.
+ ExecutionContext exe_ctx;
+ if (valobj.GetProcessSP()) {
+ exe_ctx = ExecutionContext(valobj.GetExecutionContextRef());
+ } else {
+ exe_ctx.SetContext(valobj.GetTargetSP(), true);
+ if (!exe_ctx.HasProcessScope())
+ return llvm::createStringError("no process");
+ }
+ return GetObjectDescription(str, val, exe_ctx.GetBestExecutionContextScope());
}
llvm::Error
GNUstepObjCRuntime::GetObjectDescription(Stream &strm, Value &value,
ExecutionContextScope *exe_scope) {
- return llvm::createStringError(
- "LLDB's GNUStep runtime does not support object description");
+ // The libobjc2 runtime alone cannot describe objects; the hook lives in
+ // gnustep-base (Foundation), just like on Darwin.
+ Address *function_address = GetPrintForDebuggerAddr();
+ if (!function_address)
+ return llvm::createStringError(
+ "gnustep-base is not loaded: _NSPrintForDebugger not found");
+
+ ExecutionContext exe_ctx;
+ exe_scope->CalculateExecutionContext(exe_ctx);
+ Process *process = exe_ctx.GetProcessPtr();
+ if (!process)
+ return llvm::createStringError("no process");
+
+ Target *target = exe_ctx.GetTargetPtr();
+ TypeSystemClangSP scratch_ts_sp =
+ ScratchTypeSystemClang::GetForTarget(*target);
+ if (!scratch_ts_sp)
+ return llvm::createStringError("no scratch type system");
+
+ // The call thunk is compiled as plain C (no ObjC machinery needed in the
+ // expression parser), so pass the object as `void *` and read back a
+ // `const char *`.
+ CompilerType void_ptr_type =
+ scratch_ts_sp->GetBasicType(eBasicTypeVoid).GetPointerType();
+ value.SetCompilerType(void_ptr_type);
+
+ ValueList arg_value_list;
+ arg_value_list.PushValue(value);
+
+ CompilerType return_compiler_type = scratch_ts_sp->GetCStringType(true);
+ Value ret;
+ ret.SetCompilerType(return_compiler_type);
+
+ if (!exe_ctx.GetFramePtr()) {
+ Thread *thread = exe_ctx.GetThreadPtr();
+ if (thread == nullptr) {
+ exe_ctx.SetThreadSP(process->GetThreadList().GetSelectedThread());
+ thread = exe_ctx.GetThreadPtr();
+ }
+ if (thread)
+ exe_ctx.SetFrameSP(thread->GetSelectedFrame(DoNoSelectMostRelevantFrame));
+ }
+
+ DiagnosticManager diagnostics;
+ lldb::addr_t wrapper_struct_addr = LLDB_INVALID_ADDRESS;
+
+ if (!m_print_object_caller_up) {
+ Status error;
+ m_print_object_caller_up.reset(
+ exe_scope->CalculateTarget()->GetFunctionCallerForLanguage(
+ eLanguageTypeC, return_compiler_type, *function_address,
+ arg_value_list, "gnustep-object-description", error));
+ if (error.Fail()) {
+ m_print_object_caller_up.reset();
+ return llvm::createStringError(
+ llvm::Twine("could not get function runner to call "
+ "_NSPrintForDebugger: ") +
+ error.AsCString());
+ }
+ m_print_object_caller_up->InsertFunction(exe_ctx, wrapper_struct_addr,
+ diagnostics);
+ } else {
+ m_print_object_caller_up->WriteFunctionArguments(
+ exe_ctx, wrapper_struct_addr, arg_value_list, diagnostics);
+ }
+
+ EvaluateExpressionOptions options;
+ options.SetUnwindOnError(true);
+ options.SetTryAllThreads(true);
+ options.SetStopOthers(true);
+ options.SetIgnoreBreakpoints(true);
+ options.SetTimeout(process->GetUtilityExpressionTimeout());
+ options.SetIsForUtilityExpr(true);
+
+ ExpressionResults results = m_print_object_caller_up->ExecuteFunction(
+ exe_ctx, &wrapper_struct_addr, options, diagnostics, ret);
+ if (results != eExpressionCompleted)
+ return llvm::createStringError(
+ "could not evaluate _NSPrintForDebugger in the inferior");
+
+ addr_t result_ptr = ret.GetScalar().ULongLong(LLDB_INVALID_ADDRESS);
+ if (result_ptr == 0 || result_ptr == LLDB_INVALID_ADDRESS)
+ return llvm::createStringError("object returned no description");
+
+ char buf[512];
+ size_t cstr_len = 0;
+ size_t full_buffer_len = sizeof(buf) - 1;
+ size_t curr_len = full_buffer_len;
+ while (curr_len == full_buffer_len) {
+ Status error;
+ curr_len = process->ReadCStringFromMemory(result_ptr + cstr_len, buf,
+ sizeof(buf), error);
+ strm.Write(buf, curr_len);
+ cstr_len += curr_len;
+ }
+ if (cstr_len > 0)
+ return llvm::Error::success();
+ return llvm::createStringError("empty object description");
}
bool GNUstepObjCRuntime::CouldHaveDynamicValue(ValueObject &in_value) {
diff --git a/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCRuntime.h b/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCRuntime.h
index 0c8f1d0911b72..76737a0d73e11 100644
--- a/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCRuntime.h
+++ b/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCRuntime.h
@@ -116,8 +116,17 @@ class GNUstepObjCRuntime : public lldb_private::ObjCLanguageRuntime {
// Call CreateInstance instead.
GNUstepObjCRuntime(Process *process);
+ /// Address of gnustep-base's `const char *_NSPrintForDebugger(id)`, the
+ /// same debugger hook AppleObjCRuntime uses. Resolved lazily; nullptr when
+ /// gnustep-base is not loaded in the inferior.
+ Address *GetPrintForDebuggerAddr();
+
lldb::ModuleSP m_objc_module_sp;
+ std::unique_ptr<Address> m_print_for_debugger_addr_up;
+
+ std::unique_ptr<FunctionCaller> m_print_object_caller_up;
+
std::unique_ptr<GNUstepTaggedPointerVendor> m_tagged_pointer_vendor_up;
/// Set when new modules arrive; cleared once the ISA-to-descriptor map has
diff --git a/lldb/test/Shell/Expr/objc-gnustep-print.m b/lldb/test/Shell/Expr/objc-gnustep-print.m
index 3f13bf1234cbd..70144a5829854 100644
--- a/lldb/test/Shell/Expr/objc-gnustep-print.m
+++ b/lldb/test/Shell/Expr/objc-gnustep-print.m
@@ -105,3 +105,18 @@ int main() {
[t set_ivars];
return 0;
}
+
+// LLDB resolves `_NSPrintForDebugger` by symbol in any loaded module and
+// calls it to implement `po`. In a full GNUstep environment gnustep-base
+// provides it; this hermetic stand-in exercises the same machinery.
+const char *_NSPrintForDebugger(id object) {
+ if (!object)
+ return 0;
+ return object_getClassName(object);
+}
+
+// RUN: %lldb -b -o "b objc-gnustep-print.m:106" -o "run" -o "po t" \
+// RUN: -- %t | FileCheck %s --check-prefix=PO
+//
+// PO: (lldb) po t
+// PO: TestObj
>From 61a46fe4cd3db6c65cdadd74e988e0517ea6d38e Mon Sep 17 00:00:00 2001
From: Rob <robk at robk.dev>
Date: Tue, 11 Aug 2026 21:43:04 +0100
Subject: [PATCH 03/49] [lldb][GNUstep] Implement step-through for ObjC
dispatch trampolines
When a step lands on the first instruction of a libobjc2 dispatch entry
point (objc_msgSend{,_fpret,_stret}, objc_msg_lookup{,_sender}), read the
receiver and selector from the argument registers, then resolve the IMP
by calling `objc_msg_lookup(receiver, selector)` in the inferior - the
same lookup the trampoline itself is about to perform - from a nested
function-call plan, and run to the returned address. This is the same
plan shape as AppleThreadPlanStepThroughObjCTrampoline, without the
Apple-specific dispatch-table machinery.
Results are stored in ObjCLanguageRuntime's method cache keyed by
(isa, selector), so repeated steps through the same send skip the
inferior call entirely. Messages to nil and unknown entry points fall
back to normal stepping.
Assisted-by: Claude Fable 5
---
.../ObjC/GNUstepObjCRuntime/CMakeLists.txt | 1 +
.../GNUstepObjCRuntime/GNUstepObjCRuntime.cpp | 151 ++++++++++++++++-
.../GNUstepObjCRuntime/GNUstepObjCRuntime.h | 11 ++
...tepThreadPlanStepThroughObjCTrampoline.cpp | 156 ++++++++++++++++++
...UstepThreadPlanStepThroughObjCTrampoline.h | 79 +++++++++
lldb/test/Shell/Expr/objc-gnustep-print.m | 10 ++
6 files changed, 406 insertions(+), 2 deletions(-)
create mode 100644 lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepThreadPlanStepThroughObjCTrampoline.cpp
create mode 100644 lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepThreadPlanStepThroughObjCTrampoline.h
diff --git a/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/CMakeLists.txt b/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/CMakeLists.txt
index 8fd8ffb4bc57f..14364c12dfcde 100644
--- a/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/CMakeLists.txt
+++ b/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/CMakeLists.txt
@@ -1,6 +1,7 @@
add_lldb_library(lldbPluginGNUstepObjCRuntime PLUGIN
GNUstepObjCClassDescriptor.cpp
GNUstepObjCRuntime.cpp
+ GNUstepThreadPlanStepThroughObjCTrampoline.cpp
LINK_COMPONENTS
Support
diff --git a/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCRuntime.cpp b/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCRuntime.cpp
index f2e53892f3fe8..5bd666a9d736b 100644
--- a/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCRuntime.cpp
+++ b/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCRuntime.cpp
@@ -8,6 +8,7 @@
#include "GNUstepObjCRuntime.h"
#include "GNUstepObjCClassDescriptor.h"
+#include "GNUstepThreadPlanStepThroughObjCTrampoline.h"
#include "Plugins/TypeSystem/Clang/TypeSystemClang.h"
@@ -22,10 +23,13 @@
#include "lldb/Symbol/DeclVendor.h"
#include "lldb/Symbol/Symbol.h"
#include "lldb/Symbol/SymbolContext.h"
+#include "lldb/Target/ABI.h"
#include "lldb/Target/ExecutionContext.h"
#include "lldb/Target/Process.h"
+#include "lldb/Target/RegisterContext.h"
#include "lldb/Target/Target.h"
#include "lldb/Target/Thread.h"
+#include "lldb/Target/ThreadPlanRunToAddress.h"
#include "lldb/Utility/ArchSpec.h"
#include "lldb/Utility/ConstString.h"
#include "lldb/Utility/LLDBLog.h"
@@ -383,8 +387,151 @@ GNUstepObjCRuntime::CreateObjectChecker(std::string name,
ThreadPlanSP
GNUstepObjCRuntime::GetStepThroughTrampolinePlan(Thread &thread,
bool stop_others) {
- // TODO: Implement this properly to avoid stepping into things like PLT stubs
- return nullptr;
+ // Only act when stopped at the first instruction of a known libobjc2
+ // dispatch entry point (where the argument registers still hold the
+ // receiver and selector).
+ Process *process = thread.GetProcess().get();
+ if (!process)
+ return {};
+ const addr_t pc = thread.GetRegisterContext()->GetPC();
+ Target &target = GetTargetRef();
+ Address pc_addr;
+ if (!target.ResolveLoadAddress(pc, pc_addr))
+ return {};
+ Symbol *symbol = pc_addr.CalculateSymbolContextSymbol();
+ if (!symbol || symbol->GetAddress().GetLoadAddress(&target) != pc)
+ return {};
+
+ // Dispatch entry points exported by libobjc2 (objc_msgSend.S, sendmsg2.c).
+ // The `_super` variants are omitted: super sends compile to a lookup plus
+ // a direct call, and the direct call steps normally.
+ llvm::StringRef name = symbol->GetName().GetStringRef();
+ bool is_stret = false, is_sender = false;
+ if (name == "objc_msgSend" || name == "objc_msgSend_fpret" ||
+ name == "objc_msg_lookup") {
+ } else if (name == "objc_msgSend_stret") {
+ is_stret = true;
+ } else if (name == "objc_msg_lookup_sender") {
+ is_sender = true;
+ } else {
+ return {};
+ }
+
+ ABISP abi_sp = process->GetABI();
+ if (!abi_sp)
+ return {};
+ TypeSystemClangSP scratch_ts_sp =
+ ScratchTypeSystemClang::GetForTarget(target);
+ if (!scratch_ts_sp)
+ return {};
+ CompilerType void_ptr_type =
+ scratch_ts_sp->GetBasicType(eBasicTypeVoid).GetPointerType();
+ Value void_ptr_value;
+ void_ptr_value.SetValueType(Value::ValueType::Scalar);
+ void_ptr_value.SetCompilerType(void_ptr_type);
+
+ ValueList argument_values;
+ argument_values.PushValue(void_ptr_value);
+ argument_values.PushValue(void_ptr_value);
+ argument_values.PushValue(void_ptr_value);
+ if (!abi_sp->GetArgumentValues(thread, argument_values))
+ return {};
+
+ // With struct return the sret pointer occupies the first argument slot.
+ const uint32_t receiver_idx = is_stret ? 1 : 0;
+ const uint32_t sel_idx = is_stret ? 2 : 1;
+ addr_t receiver =
+ argument_values.GetValueAtIndex(receiver_idx)->GetScalar().ULongLong();
+ const addr_t selector =
+ argument_values.GetValueAtIndex(sel_idx)->GetScalar().ULongLong();
+
+ if (is_sender) {
+ // objc_msg_lookup_sender takes `id *receiver`.
+ Status error;
+ receiver = process->ReadPointerFromMemory(receiver, error);
+ if (error.Fail())
+ return {};
+ }
+
+ // A message to nil does not dispatch anywhere.
+ if (receiver == 0 || receiver == LLDB_INVALID_ADDRESS)
+ return {};
+
+ // Consult the method cache before running anything in the inferior.
+ // Tagged pointers skip the cache: their ISA is not the object's first word.
+ addr_t isa = LLDB_INVALID_ADDRESS;
+ if (!(m_tagged_pointer_vendor_up &&
+ m_tagged_pointer_vendor_up->IsPossibleTaggedPointer(receiver))) {
+ Status error;
+ const addr_t isa_candidate = process->ReadPointerFromMemory(receiver, error);
+ if (error.Success())
+ isa = isa_candidate;
+ }
+ if (isa != LLDB_INVALID_ADDRESS) {
+ const addr_t cached_imp = LookupInMethodCache(isa, selector);
+ if (cached_imp != LLDB_INVALID_ADDRESS) {
+ Address imp_addr;
+ imp_addr.SetOpcodeLoadAddress(cached_imp, &target);
+ return std::make_shared<ThreadPlanRunToAddress>(thread, imp_addr,
+ stop_others);
+ }
+ }
+
+ if (!GetMsgLookupFunctionCaller())
+ return {};
+
+ ValueList lookup_args;
+ Value receiver_value = void_ptr_value;
+ receiver_value.GetScalar() = receiver;
+ lookup_args.PushValue(receiver_value);
+ Value selector_value = void_ptr_value;
+ selector_value.GetScalar() = selector;
+ lookup_args.PushValue(selector_value);
+
+ return std::make_shared<GNUstepThreadPlanStepThroughObjCTrampoline>(
+ thread, *this, lookup_args, isa, selector);
+}
+
+FunctionCaller *GNUstepObjCRuntime::GetMsgLookupFunctionCaller() {
+ if (m_msg_lookup_caller_up)
+ return m_msg_lookup_caller_up.get();
+
+ Target &target = GetTargetRef();
+ SymbolContextList sc_list;
+ target.GetImages().FindSymbolsWithNameAndType(ConstString("objc_msg_lookup"),
+ eSymbolTypeCode, sc_list);
+ Address lookup_addr;
+ for (const SymbolContext &sc : sc_list) {
+ if (sc.symbol) {
+ lookup_addr = sc.symbol->GetAddress();
+ break;
+ }
+ }
+ if (!lookup_addr.IsValid())
+ return nullptr;
+
+ TypeSystemClangSP scratch_ts_sp =
+ ScratchTypeSystemClang::GetForTarget(target);
+ if (!scratch_ts_sp)
+ return nullptr;
+ CompilerType void_ptr_type =
+ scratch_ts_sp->GetBasicType(eBasicTypeVoid).GetPointerType();
+ Value void_ptr_value;
+ void_ptr_value.SetValueType(Value::ValueType::Scalar);
+ void_ptr_value.SetCompilerType(void_ptr_type);
+ ValueList args;
+ args.PushValue(void_ptr_value);
+ args.PushValue(void_ptr_value);
+
+ Status error;
+ m_msg_lookup_caller_up.reset(target.GetFunctionCallerForLanguage(
+ eLanguageTypeC, void_ptr_type, lookup_addr, args, "gnustep-msg-lookup",
+ error));
+ if (error.Fail()) {
+ m_msg_lookup_caller_up.reset();
+ return nullptr;
+ }
+ return m_msg_lookup_caller_up.get();
}
void GNUstepObjCRuntime::UpdateISAToDescriptorMapIfNeeded() {
diff --git a/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCRuntime.h b/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCRuntime.h
index 76737a0d73e11..9f2e1b5cea285 100644
--- a/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCRuntime.h
+++ b/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCRuntime.h
@@ -121,12 +121,23 @@ class GNUstepObjCRuntime : public lldb_private::ObjCLanguageRuntime {
/// gnustep-base is not loaded in the inferior.
Address *GetPrintForDebuggerAddr();
+public:
+ /// Lazily-built FunctionCaller for libobjc2's
+ /// `IMP objc_msg_lookup(id receiver, SEL selector)`, used by the
+ /// step-through-trampoline plan. Returns nullptr if the symbol cannot be
+ /// resolved.
+ FunctionCaller *GetMsgLookupFunctionCaller();
+
+protected:
+
lldb::ModuleSP m_objc_module_sp;
std::unique_ptr<Address> m_print_for_debugger_addr_up;
std::unique_ptr<FunctionCaller> m_print_object_caller_up;
+ std::unique_ptr<FunctionCaller> m_msg_lookup_caller_up;
+
std::unique_ptr<GNUstepTaggedPointerVendor> m_tagged_pointer_vendor_up;
/// Set when new modules arrive; cleared once the ISA-to-descriptor map has
diff --git a/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepThreadPlanStepThroughObjCTrampoline.cpp b/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepThreadPlanStepThroughObjCTrampoline.cpp
new file mode 100644
index 0000000000000..842bc406c68ea
--- /dev/null
+++ b/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepThreadPlanStepThroughObjCTrampoline.cpp
@@ -0,0 +1,156 @@
+//===-- GNUstepThreadPlanStepThroughObjCTrampoline.cpp --------------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+#include "GNUstepThreadPlanStepThroughObjCTrampoline.h"
+#include "GNUstepObjCRuntime.h"
+
+#include "lldb/Expression/DiagnosticManager.h"
+#include "lldb/Expression/FunctionCaller.h"
+#include "lldb/Target/ABI.h"
+#include "lldb/Target/ExecutionContext.h"
+#include "lldb/Target/Process.h"
+#include "lldb/Target/Target.h"
+#include "lldb/Target/Thread.h"
+#include "lldb/Target/ThreadPlanRunToAddress.h"
+#include "lldb/Utility/LLDBLog.h"
+#include "lldb/Utility/Log.h"
+#include "lldb/Utility/Stream.h"
+
+using namespace lldb;
+using namespace lldb_private;
+
+GNUstepThreadPlanStepThroughObjCTrampoline::
+ GNUstepThreadPlanStepThroughObjCTrampoline(Thread &thread,
+ GNUstepObjCRuntime &runtime,
+ ValueList &input_values,
+ lldb::addr_t isa_addr,
+ lldb::addr_t sel_addr)
+ : ThreadPlan(ThreadPlan::eKindGeneric,
+ "GNUstep step through ObjC trampoline", thread, eVoteNoOpinion,
+ eVoteNoOpinion),
+ m_runtime(runtime), m_input_values(input_values), m_isa_addr(isa_addr),
+ m_sel_addr(sel_addr) {}
+
+GNUstepThreadPlanStepThroughObjCTrampoline::
+ ~GNUstepThreadPlanStepThroughObjCTrampoline() = default;
+
+void GNUstepThreadPlanStepThroughObjCTrampoline::DidPush() {
+ // Setting up the called function might require allocations in the
+ // inferior, i.e. a nested function call. This needs to be done as a
+ // PreResumeAction.
+ m_process.AddPreResumeAction(PreResumeInitializeFunctionCaller,
+ (void *)this);
+}
+
+bool GNUstepThreadPlanStepThroughObjCTrampoline::
+ PreResumeInitializeFunctionCaller(void *void_myself) {
+ auto *myself =
+ static_cast<GNUstepThreadPlanStepThroughObjCTrampoline *>(void_myself);
+ return myself->InitializeFunctionCaller();
+}
+
+bool GNUstepThreadPlanStepThroughObjCTrampoline::InitializeFunctionCaller() {
+ if (m_func_sp)
+ return true;
+
+ m_lookup_function = m_runtime.GetMsgLookupFunctionCaller();
+ if (!m_lookup_function)
+ return false;
+
+ ExecutionContext exe_ctx;
+ GetThread().CalculateExecutionContext(exe_ctx);
+
+ DiagnosticManager diagnostics;
+ if (!m_lookup_function->InsertFunction(exe_ctx, m_args_addr, diagnostics))
+ return false;
+ if (!m_lookup_function->WriteFunctionArguments(exe_ctx, m_args_addr,
+ m_input_values, diagnostics))
+ return false;
+
+ EvaluateExpressionOptions options;
+ options.SetUnwindOnError(true);
+ options.SetIgnoreBreakpoints(true);
+ options.SetStopOthers(false);
+
+ m_func_sp = m_lookup_function->GetThreadPlanToCallFunction(
+ exe_ctx, m_args_addr, options, diagnostics);
+ if (!m_func_sp)
+ return false;
+ m_func_sp->SetOkayToDiscard(true);
+ PushPlan(m_func_sp);
+ return true;
+}
+
+void GNUstepThreadPlanStepThroughObjCTrampoline::GetDescription(
+ Stream *s, lldb::DescriptionLevel level) {
+ if (level == lldb::eDescriptionLevelBrief) {
+ s->Printf("Step through GNUstep ObjC trampoline");
+ return;
+ }
+ s->Printf("Stepping to implementation of ObjC method - obj: 0x%" PRIx64
+ ", isa: 0x%" PRIx64 ", sel: 0x%" PRIx64,
+ static_cast<uint64_t>(
+ m_input_values.GetValueAtIndex(0)->GetScalar().ULongLong()),
+ m_isa_addr, m_sel_addr);
+}
+
+bool GNUstepThreadPlanStepThroughObjCTrampoline::ShouldStop(Event *event_ptr) {
+ // First stage: the nested "call objc_msg_lookup" plan is still running.
+ if (m_func_sp) {
+ if (!m_func_sp->IsPlanComplete())
+ return false;
+ if (!m_func_sp->PlanSucceeded()) {
+ SetPlanComplete(false);
+ return true;
+ }
+ m_func_sp.reset();
+ }
+
+ Log *log = GetLog(LLDBLog::Step);
+
+ // Second stage: fetch the IMP the lookup returned and run to it.
+ if (!m_run_to_sp) {
+ Value target_addr_value;
+ ExecutionContext exe_ctx;
+ GetThread().CalculateExecutionContext(exe_ctx);
+ m_lookup_function->FetchFunctionResults(exe_ctx, m_args_addr,
+ target_addr_value);
+ m_lookup_function->DeallocateFunctionResults(exe_ctx, m_args_addr);
+ lldb::addr_t target_addr = target_addr_value.GetScalar().ULongLong();
+
+ if (ABISP abi_sp = GetThread().GetProcess()->GetABI())
+ target_addr = abi_sp->FixCodeAddress(target_addr);
+
+ if (target_addr == 0 || target_addr == LLDB_INVALID_ADDRESS) {
+ LLDB_LOG(log, "objc_msg_lookup returned {0:x}, stopping.", target_addr);
+ SetPlanComplete();
+ return true;
+ }
+
+ LLDB_LOG(log, "Running to GNUstep ObjC method implementation: {0:x}",
+ target_addr);
+
+ if (m_isa_addr != LLDB_INVALID_ADDRESS &&
+ m_sel_addr != LLDB_INVALID_ADDRESS)
+ m_runtime.AddToMethodCache(m_isa_addr, m_sel_addr, target_addr);
+
+ Address target_so_addr;
+ target_so_addr.SetOpcodeLoadAddress(target_addr, exe_ctx.GetTargetPtr());
+ m_run_to_sp = std::make_shared<ThreadPlanRunToAddress>(
+ GetThread(), target_so_addr, false);
+ PushPlan(m_run_to_sp);
+ return false;
+ }
+
+ // Third stage: wait for the run-to-implementation plan.
+ if (GetThread().IsThreadPlanDone(m_run_to_sp.get())) {
+ SetPlanComplete();
+ return true;
+ }
+ return false;
+}
diff --git a/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepThreadPlanStepThroughObjCTrampoline.h b/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepThreadPlanStepThroughObjCTrampoline.h
new file mode 100644
index 0000000000000..8b61a4d8c26d6
--- /dev/null
+++ b/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepThreadPlanStepThroughObjCTrampoline.h
@@ -0,0 +1,79 @@
+//===-- GNUstepThreadPlanStepThroughObjCTrampoline.h ------------*- C++ -*-===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+#ifndef LLDB_SOURCE_PLUGINS_LANGUAGERUNTIME_OBJC_GNUSTEPOBJCRUNTIME_GNUSTEPTHREADPLANSTEPTHROUGHOBJCTRAMPOLINE_H
+#define LLDB_SOURCE_PLUGINS_LANGUAGERUNTIME_OBJC_GNUSTEPOBJCRUNTIME_GNUSTEPTHREADPLANSTEPTHROUGHOBJCTRAMPOLINE_H
+
+#include "lldb/Core/Value.h"
+#include "lldb/Target/ThreadPlan.h"
+#include "lldb/lldb-types.h"
+
+namespace lldb_private {
+
+class GNUstepObjCRuntime;
+
+/// Steps from a libobjc2 dispatch entry point (objc_msgSend,
+/// objc_msg_lookup, ...) to the method implementation it is about to
+/// dispatch to. The IMP is resolved by calling `objc_msg_lookup(receiver,
+/// selector)` in the inferior - the same lookup the trampoline itself
+/// performs - from a nested function-call plan, then running to the
+/// returned address. This is the same shape as
+/// AppleThreadPlanStepThroughObjCTrampoline.
+class GNUstepThreadPlanStepThroughObjCTrampoline : public ThreadPlan {
+public:
+ GNUstepThreadPlanStepThroughObjCTrampoline(Thread &thread,
+ GNUstepObjCRuntime &runtime,
+ ValueList &input_values,
+ lldb::addr_t isa_addr,
+ lldb::addr_t sel_addr);
+
+ ~GNUstepThreadPlanStepThroughObjCTrampoline() override;
+
+ static bool PreResumeInitializeFunctionCaller(void *myself);
+
+ void GetDescription(Stream *s, lldb::DescriptionLevel level) override;
+
+ bool ValidatePlan(Stream *error) override { return true; }
+
+ lldb::StateType GetPlanRunState() override { return lldb::eStateRunning; }
+
+ bool ShouldStop(Event *event_ptr) override;
+
+ // The lookup might have to fill dispatch caches, so it is not safe to run
+ // only one thread.
+ bool StopOthers() override { return false; }
+
+ bool MischiefManaged() override { return IsPlanComplete(); }
+
+ void DidPush() override;
+
+ bool WillStop() override { return true; }
+
+protected:
+ bool DoPlanExplainsStop(Event *event_ptr) override { return true; }
+
+private:
+ bool InitializeFunctionCaller();
+
+ GNUstepObjCRuntime &m_runtime;
+ /// Address of the argument struct of the msg-lookup function call.
+ lldb::addr_t m_args_addr = LLDB_INVALID_ADDRESS;
+ ValueList m_input_values;
+ /// Keys for the method cache filled in when the lookup completes.
+ lldb::addr_t m_isa_addr;
+ lldb::addr_t m_sel_addr;
+ /// The nested function-call plan; reset once it completes.
+ lldb::ThreadPlanSP m_func_sp;
+ /// The run-to-implementation plan queued after the lookup.
+ lldb::ThreadPlanSP m_run_to_sp;
+ FunctionCaller *m_lookup_function = nullptr;
+};
+
+} // namespace lldb_private
+
+#endif // LLDB_SOURCE_PLUGINS_LANGUAGERUNTIME_OBJC_GNUSTEPOBJCRUNTIME_GNUSTEPTHREADPLANSTEPTHROUGHOBJCTRAMPOLINE_H
diff --git a/lldb/test/Shell/Expr/objc-gnustep-print.m b/lldb/test/Shell/Expr/objc-gnustep-print.m
index 70144a5829854..6e119cefc459b 100644
--- a/lldb/test/Shell/Expr/objc-gnustep-print.m
+++ b/lldb/test/Shell/Expr/objc-gnustep-print.m
@@ -120,3 +120,13 @@ int main() {
//
// PO: (lldb) po t
// PO: TestObj
+
+// Stepping at a message send goes through the objc_msgSend trampoline into
+// the method implementation.
+//
+// RUN: %lldb -b -o "b objc-gnustep-print.m:104" -o "run" -o "step" \
+// RUN: -- %t | FileCheck %s --check-prefix=STEP
+//
+// STEP: (lldb) step
+// STEP: stop reason = step in
+// STEP: check_ivars_zeroed
>From 2776a3b523c09d5dcf5aeaa5509073a7a1ba47c4 Mon Sep 17 00:00:00 2001
From: Rob <robk at robk.dev>
Date: Tue, 11 Aug 2026 23:45:07 +0100
Subject: [PATCH 04/49] [lldb][GNUstep] Enable Objective-C expression
evaluation
The expression parser already compiles in native gnustep-2.x mode when
this runtime reports eGNUstep_libobjc2, and the ABI lowers message
sends, class references, and constant strings to ordinary symbols that
resolve from the target's symbol tables. The one missing piece is
selector registration: JIT'd expression modules never run __objc_load,
so their `.objc_selector_*` structures reach objc_msgSend unregistered
and every send fails in the runtime's forwarding path.
Provide the missing piece as a module IR pass via the existing
LanguageRuntime::GetIRPasses hook: rewrite each use of a selector
global in the __objc_selectors section into a call to
sel_registerTypedName_np() (or sel_registerName() for untyped
selectors), reusing the name and type-encoding string constants from
the selector's own initializer. No shared expression-parser code is
touched.
Also override CalculateHasNewLiteralsAndIndexing: gnustep-base
implements the container-literal and boxed-expression protocol, so
@[...], @{...} and @(...) work out of the box.
Assisted-by: Claude Fable 5
---
.../GNUstepObjCRuntime/GNUstepObjCRuntime.cpp | 104 ++++++++++++++++++
.../GNUstepObjCRuntime/GNUstepObjCRuntime.h | 10 ++
lldb/test/Shell/Expr/objc-gnustep-expr.m | 54 +++++++++
3 files changed, 168 insertions(+)
create mode 100644 lldb/test/Shell/Expr/objc-gnustep-expr.m
diff --git a/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCRuntime.cpp b/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCRuntime.cpp
index 5bd666a9d736b..61d32db706e6d 100644
--- a/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCRuntime.cpp
+++ b/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCRuntime.cpp
@@ -38,11 +38,108 @@
#include "lldb/Utility/StructuredData.h"
#include "lldb/ValueObject/ValueObject.h"
+#include "llvm/IR/IRBuilder.h"
+#include "llvm/IR/LegacyPassManager.h"
+#include "llvm/IR/Module.h"
+#include "llvm/Pass.h"
+
using namespace lldb;
using namespace lldb_private;
LLDB_PLUGIN_DEFINE(GNUstepObjCRuntime)
+namespace {
+/// Registers the Objective-C selectors of a JIT'd expression module with the
+/// libobjc2 runtime.
+///
+/// clang emits each selector as a `.objc_selector_<name>_<types>` global in
+/// the `__objc_selectors` section: a {name, types} string pair that the
+/// runtime's __objc_load rewrites into a registered selector when a module
+/// is loaded. Expression modules are never loaded that way, so passing the
+/// raw structure to objc_msgSend dispatches an unregistered selector (which
+/// gnustep-base reports as e.g. "-[NSSmallInt ]"). Replace every use with
+/// the result of sel_registerTypedName_np()/sel_registerName(), which
+/// resolve against libobjc2 at expression link time.
+class GNUstepObjCSelectorRegistrationPass : public llvm::ModulePass {
+public:
+ static char ID;
+
+ GNUstepObjCSelectorRegistrationPass() : llvm::ModulePass(ID) {}
+
+ llvm::StringRef getPassName() const override {
+ return "GNUstep ObjC selector registration";
+ }
+
+ bool runOnModule(llvm::Module &module) override {
+ llvm::SmallVector<llvm::GlobalVariable *, 8> sel_globals;
+ for (llvm::GlobalVariable &gv : module.globals())
+ if (gv.hasSection() &&
+ llvm::StringRef(gv.getSection()).starts_with("__objc_selectors"))
+ sel_globals.push_back(&gv);
+ if (sel_globals.empty())
+ return false;
+
+ llvm::LLVMContext &ctx = module.getContext();
+ llvm::PointerType *ptr_ty = llvm::PointerType::get(ctx, 0);
+ llvm::FunctionCallee typed_reg;
+ llvm::FunctionCallee untyped_reg;
+
+ bool changed = false;
+ for (llvm::GlobalVariable *gv : sel_globals) {
+ if (!gv->hasInitializer())
+ continue;
+ auto *init = llvm::dyn_cast<llvm::ConstantStruct>(gv->getInitializer());
+ if (!init || init->getNumOperands() < 1)
+ continue;
+ llvm::Constant *name_ptr = init->getOperand(0);
+ llvm::Constant *types_ptr =
+ init->getNumOperands() > 1 ? init->getOperand(1) : nullptr;
+ const bool has_types = types_ptr && !types_ptr->isNullValue();
+
+ // One registration call per function; the entry block dominates all
+ // uses, including PHI incoming edges.
+ llvm::SmallDenseMap<llvm::Function *, llvm::Value *, 4> call_per_fn;
+ llvm::SmallVector<llvm::Use *, 8> uses;
+ for (llvm::Use &use : gv->uses())
+ uses.push_back(&use);
+ for (llvm::Use *use : uses) {
+ auto *inst = llvm::dyn_cast<llvm::Instruction>(use->getUser());
+ if (!inst)
+ continue;
+ llvm::Function *func = inst->getFunction();
+ llvm::Value *®_call = call_per_fn[func];
+ if (!reg_call) {
+ llvm::IRBuilder<> builder(
+ &*func->getEntryBlock().getFirstInsertionPt());
+ if (has_types) {
+ if (!typed_reg)
+ typed_reg = module.getOrInsertFunction(
+ "sel_registerTypedName_np",
+ llvm::FunctionType::get(ptr_ty, {ptr_ty, ptr_ty},
+ /*isVarArg=*/false));
+ reg_call = builder.CreateCall(typed_reg, {name_ptr, types_ptr},
+ "lldb.objc.sel");
+ } else {
+ if (!untyped_reg)
+ untyped_reg = module.getOrInsertFunction(
+ "sel_registerName",
+ llvm::FunctionType::get(ptr_ty, {ptr_ty},
+ /*isVarArg=*/false));
+ reg_call =
+ builder.CreateCall(untyped_reg, {name_ptr}, "lldb.objc.sel");
+ }
+ }
+ use->set(reg_call);
+ changed = true;
+ }
+ }
+ return changed;
+ }
+};
+
+char GNUstepObjCSelectorRegistrationPass::ID = 0;
+} // namespace
+
char GNUstepObjCRuntime::ID = 0;
void GNUstepObjCRuntime::Initialize() {
@@ -574,6 +671,13 @@ void GNUstepObjCRuntime::UpdateISAToDescriptorMapIfNeeded() {
m_isa_to_descriptor_stop_id = stop_id;
}
+bool GNUstepObjCRuntime::GetIRPasses(
+ LLVMUserExpression::IRPasses &custom_passes) {
+ custom_passes.EarlyPasses = std::make_shared<llvm::legacy::PassManager>();
+ custom_passes.EarlyPasses->add(new GNUstepObjCSelectorRegistrationPass());
+ return true;
+}
+
ObjCLanguageRuntime::TaggedPointerVendor *
GNUstepObjCRuntime::GetTaggedPointerVendor() {
return m_tagged_pointer_vendor_up.get();
diff --git a/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCRuntime.h b/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCRuntime.h
index 9f2e1b5cea285..88128bdd2ac98 100644
--- a/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCRuntime.h
+++ b/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCRuntime.h
@@ -106,6 +106,16 @@ class GNUstepObjCRuntime : public lldb_private::ObjCLanguageRuntime {
void UpdateISAToDescriptorMapIfNeeded() override;
+ /// Provides an IR pass that registers the expression module's Objective-C
+ /// selectors with the runtime. JIT'd expression modules never run
+ /// __objc_load, so their selector structures would otherwise reach
+ /// objc_msgSend unregistered.
+ bool GetIRPasses(LLVMUserExpression::IRPasses &custom_passes) override;
+
+ /// gnustep-base implements the container-literal and boxed-expression
+ /// protocol methods, so @[...], @{...} and @(...) are available.
+ bool CalculateHasNewLiteralsAndIndexing() override { return true; }
+
TaggedPointerVendor *GetTaggedPointerVendor() override;
ClassDescriptorSP GetClassDescriptor(ValueObject &in_value) override;
diff --git a/lldb/test/Shell/Expr/objc-gnustep-expr.m b/lldb/test/Shell/Expr/objc-gnustep-expr.m
new file mode 100644
index 0000000000000..c58b13e1238fb
--- /dev/null
+++ b/lldb/test/Shell/Expr/objc-gnustep-expr.m
@@ -0,0 +1,54 @@
+// REQUIRES: objc-gnustep
+// XFAIL: system-windows
+//
+// RUN: %build %s --compiler=clang --objc-gnustep --output=%t
+
+#import "objc/runtime.h"
+
+ at protocol NSCoding
+ at end
+
+#ifdef __has_attribute
+#if __has_attribute(objc_root_class)
+__attribute__((objc_root_class))
+#endif
+#endif
+ at interface NSObject <NSCoding> {
+ id isa;
+ int refcount;
+}
+ at end
+ at implementation NSObject
++ (id)new {
+ return class_createInstance(self, 0);
+}
+ at end
+
+ at interface Calc : NSObject
+- (int)addFourtyTwoTo:(int)value;
+ at end
+ at implementation Calc
+- (int)addFourtyTwoTo:(int)value {
+ return value + 42;
+}
+ at end
+
+// Message sends in expressions require the JIT'd module's selectors to be
+// registered with the runtime (the GNUstep plugin's IR pass does this);
+// without it the dispatch reaches the runtime with an unregistered selector.
+//
+// RUN: %lldb -b -o "b objc-gnustep-expr.m:47" -o "run" \
+// RUN: -o "expr [c addFourtyTwoTo:100]" \
+// RUN: -o "expr (int)[[Calc new] addFourtyTwoTo:1]" -- %t | FileCheck %s
+//
+int main() {
+ Calc *c = [Calc new];
+ (void)[c addFourtyTwoTo:0];
+ return 0;
+}
+//
+// CHECK: (lldb) expr [c addFourtyTwoTo:100]
+// CHECK: (int) {{.*}} = 142
+//
+// CHECK: (lldb) expr (int)[[Calc new] addFourtyTwoTo:1]
+// CHECK: (int) {{.*}} = 43
>From 3113a74a6313bdd8a56401b3c479d776931d1931 Mon Sep 17 00:00:00 2001
From: Rob <robk at robk.dev>
Date: Wed, 12 Aug 2026 00:13:22 +0100
Subject: [PATCH 05/49] [lldb][GNUstep] Match the expression result variable by
pattern
An expression's result is named $0, $1 and so on depending on how many
have been evaluated before it, so match it with a pattern rather than a
literal, and avoid an unbalanced bracket in a check line.
Assisted-by: Claude Opus 5
---
lldb/test/Shell/Expr/objc-gnustep-expr.m | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/lldb/test/Shell/Expr/objc-gnustep-expr.m b/lldb/test/Shell/Expr/objc-gnustep-expr.m
index c58b13e1238fb..bc20422d817bc 100644
--- a/lldb/test/Shell/Expr/objc-gnustep-expr.m
+++ b/lldb/test/Shell/Expr/objc-gnustep-expr.m
@@ -48,7 +48,7 @@ int main() {
}
//
// CHECK: (lldb) expr [c addFourtyTwoTo:100]
-// CHECK: (int) {{.*}} = 142
+// CHECK: (int) {{\$[0-9]+}} = 142
//
-// CHECK: (lldb) expr (int)[[Calc new] addFourtyTwoTo:1]
-// CHECK: (int) {{.*}} = 43
+// CHECK: addFourtyTwoTo:1]
+// CHECK: (int) {{\$[0-9]+}} = 43
>From 5cc6b73688b7f9c4e6d2b4048fbbbc4b688fb9cf Mon Sep 17 00:00:00 2001
From: Rob <robk at robk.dev>
Date: Wed, 12 Aug 2026 00:13:22 +0100
Subject: [PATCH 06/49] [lldb][GNUstep] Assert the dynamic type through the
command interpreter
`frame variable -d run-target` is what a user actually sees, so check
its output rather than only the value the API returns, and check that
the static type is still reported when dynamic values are turned off.
Assisted-by: Claude Opus 5
---
lldb/test/Shell/Expr/objc-gnustep-dynamic-types.m | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/lldb/test/Shell/Expr/objc-gnustep-dynamic-types.m b/lldb/test/Shell/Expr/objc-gnustep-dynamic-types.m
index 6c43f2df26223..2d0caf1c69426 100644
--- a/lldb/test/Shell/Expr/objc-gnustep-dynamic-types.m
+++ b/lldb/test/Shell/Expr/objc-gnustep-dynamic-types.m
@@ -36,9 +36,9 @@ @implementation Derived
// The static type of `object` is Base, but the dynamic type is Derived. The
// GNUstep runtime resolves the dynamic type by reading the class structure
-// from the inferior's memory (no code is run in the inferior).
+// from the inferior's memory and attaching the matching type from debug info.
//
-// RUN: %lldb -b -o "b objc-gnustep-dynamic-types.m:48" -o "run" \
+// RUN: %lldb -b -o "b objc-gnustep-dynamic-types.m:47" -o "run" \
// RUN: -o "frame variable -d run-target object" \
// RUN: -o "frame variable -d no-dynamic-values object" -- %t | FileCheck %s
//
>From 21f60a5a3f163d11b3fc8d7258ef03946c0d93b3 Mon Sep 17 00:00:00 2001
From: Rob <robk at robk.dev>
Date: Wed, 12 Aug 2026 00:16:39 +0100
Subject: [PATCH 07/49] [lldb][GNUstep] Attach a type from debug info to a
dynamic value
Reporting only a class name leaves LLDB without a type to display, so it
falls back to the static type. The base class's cache is keyed on a
symbol named after the class, which the Apple ABI emits but the
gnustep-2.x ABI does not - its class symbol is `._OBJC_CLASS_<name>` -
so on a miss, query the debug info directly for the interface type.
Assisted-by: Claude Opus 5
---
.../GNUstepObjCRuntime/GNUstepObjCRuntime.cpp | 28 +++++++++++++------
.../GNUstepObjCRuntime/GNUstepObjCRuntime.h | 6 ++++
2 files changed, 26 insertions(+), 8 deletions(-)
diff --git a/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCRuntime.cpp b/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCRuntime.cpp
index 61d32db706e6d..4dc05bcb46eeb 100644
--- a/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCRuntime.cpp
+++ b/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCRuntime.cpp
@@ -395,26 +395,38 @@ bool GNUstepObjCRuntime::GetDynamicTypeAndAddress(
address.SetRawAddress(object_ptr);
class_type_or_name.SetName(class_name);
- // Try to upgrade the bare name to a real type: first from the cache of
- // classes already realized from debug info, then - should a decl vendor
- // exist one day - from that.
+ // Upgrade the bare class name to a real type when the inferior's debug
+ // info defines the class. LookupInCompleteClassCache keys on an
+ // eSymbolTypeObjCClass symbol named exactly after the class, which the
+ // Apple ABI emits but the gnustep-2.x ABI does not (its class symbol is
+ // "._OBJC_CLASS_<name>"), so on a cache miss query the debug info directly.
TypeSP type_sp(objc_class_sp->GetType());
if (!type_sp) {
type_sp = LookupInCompleteClassCache(class_name);
+ if (!type_sp)
+ type_sp = LookupClassTypeInDebugInfo(class_name);
if (type_sp)
objc_class_sp->SetType(type_sp);
}
if (type_sp)
class_type_or_name.SetTypeSP(type_sp);
- else if (auto *vendor = GetDeclVendor()) {
- auto types = vendor->FindTypes(class_name, /*max_matches*/ 1);
- if (!types.empty())
- class_type_or_name.SetCompilerType(types.front());
- }
return !class_type_or_name.IsEmpty();
}
+lldb::TypeSP
+GNUstepObjCRuntime::LookupClassTypeInDebugInfo(ConstString class_name) {
+ TypeQuery query(class_name.GetStringRef(), TypeQueryOptions::e_exact_match);
+ TypeResults results;
+ GetTargetRef().GetImages().FindTypes(nullptr, query, results);
+ for (const TypeSP &type_sp : results.GetTypeMap().Types()) {
+ if (type_sp && TypeSystemClang::IsObjCObjectOrInterfaceType(
+ type_sp->GetForwardCompilerType()))
+ return type_sp;
+ }
+ return TypeSP();
+}
+
TypeAndOrName
GNUstepObjCRuntime::FixUpDynamicType(const TypeAndOrName &type_and_or_name,
ValueObject &static_value) {
diff --git a/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCRuntime.h b/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCRuntime.h
index 88128bdd2ac98..6b5f0b5c5c88c 100644
--- a/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCRuntime.h
+++ b/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCRuntime.h
@@ -126,6 +126,12 @@ class GNUstepObjCRuntime : public lldb_private::ObjCLanguageRuntime {
// Call CreateInstance instead.
GNUstepObjCRuntime(Process *process);
+ /// Finds a complete Objective-C interface type named \p class_name in the
+ /// target's debug info. Used to attach a real type to a dynamic value when
+ /// the base class's symbol-name-keyed cache misses (the gnustep-2.x class
+ /// symbol is not named after the class).
+ lldb::TypeSP LookupClassTypeInDebugInfo(ConstString class_name);
+
/// Address of gnustep-base's `const char *_NSPrintForDebugger(id)`, the
/// same debugger hook AppleObjCRuntime uses. Resolved lazily; nullptr when
/// gnustep-base is not loaded in the inferior.
>From 117c610893b875a00d2a4bfaa24f44bc6e033050 Mon Sep 17 00:00:00 2001
From: Rob <robk at robk.dev>
Date: Wed, 12 Aug 2026 00:16:39 +0100
Subject: [PATCH 08/49] [lldb][GNUstep] Identify dispatch entry points by name,
not by address
libobjc2's hand-written assembly places local labels at the same address
as objc_msgSend, so the symbol found at an address is not reliably the
dispatch symbol. Resolve each entry point's address from its name
instead, and add the struct-return and lookup variants.
Build the call that resolves a method implementation as a utility
function, the way AppleObjCTrampolineHandler does, rather than from a
bare function address: the latter cannot be compiled from inside a
step's pre-resume action.
Assisted-by: Claude Opus 5
---
.../GNUstepObjCRuntime/GNUstepObjCRuntime.cpp | 125 ++++++++++++------
.../GNUstepObjCRuntime/GNUstepObjCRuntime.h | 31 ++++-
...tepThreadPlanStepThroughObjCTrampoline.cpp | 8 +-
3 files changed, 115 insertions(+), 49 deletions(-)
diff --git a/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCRuntime.cpp b/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCRuntime.cpp
index 4dc05bcb46eeb..52834e5f9eae5 100644
--- a/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCRuntime.cpp
+++ b/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCRuntime.cpp
@@ -498,33 +498,22 @@ GNUstepObjCRuntime::GetStepThroughTrampolinePlan(Thread &thread,
bool stop_others) {
// Only act when stopped at the first instruction of a known libobjc2
// dispatch entry point (where the argument registers still hold the
- // receiver and selector).
+ // receiver and selector). Match by resolving each entry point's address by
+ // name rather than by the symbol at the PC: libobjc2's hand-written
+ // assembly places local labels (e.g. __objc_block_trampoline_end_sret) at
+ // the same address as objc_msgSend, so the symbol found at an address is
+ // not reliably the dispatch symbol.
Process *process = thread.GetProcess().get();
if (!process)
return {};
const addr_t pc = thread.GetRegisterContext()->GetPC();
Target &target = GetTargetRef();
- Address pc_addr;
- if (!target.ResolveLoadAddress(pc, pc_addr))
- return {};
- Symbol *symbol = pc_addr.CalculateSymbolContextSymbol();
- if (!symbol || symbol->GetAddress().GetLoadAddress(&target) != pc)
- return {};
- // Dispatch entry points exported by libobjc2 (objc_msgSend.S, sendmsg2.c).
- // The `_super` variants are omitted: super sends compile to a lookup plus
- // a direct call, and the direct call steps normally.
- llvm::StringRef name = symbol->GetName().GetStringRef();
- bool is_stret = false, is_sender = false;
- if (name == "objc_msgSend" || name == "objc_msgSend_fpret" ||
- name == "objc_msg_lookup") {
- } else if (name == "objc_msgSend_stret") {
- is_stret = true;
- } else if (name == "objc_msg_lookup_sender") {
- is_sender = true;
- } else {
+ const DispatchEntryPoint *entry = FindDispatchEntryPoint(pc);
+ if (!entry)
return {};
- }
+ const bool is_stret = entry->is_stret;
+ const bool is_sender = entry->is_sender;
ABISP abi_sp = process->GetABI();
if (!abi_sp)
@@ -586,7 +575,7 @@ GNUstepObjCRuntime::GetStepThroughTrampolinePlan(Thread &thread,
}
}
- if (!GetMsgLookupFunctionCaller())
+ if (!GetMsgLookupFunctionCaller(thread))
return {};
ValueList lookup_args;
@@ -601,30 +590,82 @@ GNUstepObjCRuntime::GetStepThroughTrampolinePlan(Thread &thread,
thread, *this, lookup_args, isa, selector);
}
-FunctionCaller *GNUstepObjCRuntime::GetMsgLookupFunctionCaller() {
- if (m_msg_lookup_caller_up)
- return m_msg_lookup_caller_up.get();
-
- Target &target = GetTargetRef();
- SymbolContextList sc_list;
- target.GetImages().FindSymbolsWithNameAndType(ConstString("objc_msg_lookup"),
- eSymbolTypeCode, sc_list);
- Address lookup_addr;
- for (const SymbolContext &sc : sc_list) {
- if (sc.symbol) {
- lookup_addr = sc.symbol->GetAddress();
- break;
+const GNUstepObjCRuntime::DispatchEntryPoint *
+GNUstepObjCRuntime::FindDispatchEntryPoint(lldb::addr_t pc) {
+ if (!m_dispatch_entry_points_resolved) {
+ m_dispatch_entry_points_resolved = true;
+ // Dispatch entry points exported by libobjc2 (objc_msgSend.S,
+ // sendmsg2.c). The `_super` variants are omitted: super sends compile to
+ // a lookup plus a direct call, and the direct call steps normally.
+ static const struct {
+ const char *name;
+ bool is_stret;
+ bool is_sender;
+ } kEntryPoints[] = {
+ {"objc_msgSend", false, false},
+ {"objc_msgSend_fpret", false, false},
+ {"objc_msgSend_stret", true, false},
+ {"objc_msg_lookup", false, false},
+ {"objc_msg_lookup_sender", false, true},
+ };
+ Target &target = GetTargetRef();
+ for (const auto &ep : kEntryPoints) {
+ SymbolContextList sc_list;
+ target.GetImages().FindSymbolsWithNameAndType(ConstString(ep.name),
+ eSymbolTypeCode, sc_list);
+ for (const SymbolContext &sc : sc_list) {
+ if (!sc.symbol)
+ continue;
+ const addr_t addr = sc.symbol->GetLoadAddress(&target);
+ if (addr != LLDB_INVALID_ADDRESS) {
+ m_dispatch_entry_points.push_back({addr, ep.is_stret, ep.is_sender});
+ break;
+ }
+ }
}
}
- if (!lookup_addr.IsValid())
+
+ for (const DispatchEntryPoint &ep : m_dispatch_entry_points)
+ if (ep.address == pc)
+ return &ep;
+ return nullptr;
+}
+
+FunctionCaller *GNUstepObjCRuntime::GetMsgLookupFunctionCaller(Thread &thread) {
+ // Build (once) a utility function that resolves a method implementation by
+ // calling libobjc2's objc_msg_lookup, and a FunctionCaller to invoke it.
+ // This mirrors AppleObjCTrampolineHandler's dispatch-lookup utility and is
+ // the JIT path that works from inside a step's PreResume action.
+ static const char *g_lookup_name = "$__lldb_gnustep_objc_msg_lookup";
+ static const char *g_lookup_code =
+ "void *objc_msg_lookup(void *receiver, void *selector);\n"
+ "void *$__lldb_gnustep_objc_msg_lookup(void *receiver, void *selector) {\n"
+ " return objc_msg_lookup(receiver, selector);\n"
+ "}\n";
+
+ if (m_msg_lookup_caller)
+ return m_msg_lookup_caller;
+
+ ThreadSP thread_sp(thread.shared_from_this());
+ ExecutionContext exe_ctx(thread_sp);
+ Log *log = GetLog(LLDBLog::Step);
+
+ auto utility_fn_or_error = exe_ctx.GetTargetRef().CreateUtilityFunction(
+ g_lookup_code, g_lookup_name, eLanguageTypeC, exe_ctx);
+ if (!utility_fn_or_error) {
+ LLDB_LOG_ERROR(log, utility_fn_or_error.takeError(),
+ "[GNUstep] failed to build objc_msg_lookup utility: {0}");
return nullptr;
+ }
+ m_msg_lookup_utility_up = std::move(*utility_fn_or_error);
TypeSystemClangSP scratch_ts_sp =
- ScratchTypeSystemClang::GetForTarget(target);
+ ScratchTypeSystemClang::GetForTarget(GetTargetRef());
if (!scratch_ts_sp)
return nullptr;
CompilerType void_ptr_type =
scratch_ts_sp->GetBasicType(eBasicTypeVoid).GetPointerType();
+
Value void_ptr_value;
void_ptr_value.SetValueType(Value::ValueType::Scalar);
void_ptr_value.SetCompilerType(void_ptr_type);
@@ -633,14 +674,16 @@ FunctionCaller *GNUstepObjCRuntime::GetMsgLookupFunctionCaller() {
args.PushValue(void_ptr_value);
Status error;
- m_msg_lookup_caller_up.reset(target.GetFunctionCallerForLanguage(
- eLanguageTypeC, void_ptr_type, lookup_addr, args, "gnustep-msg-lookup",
- error));
+ m_msg_lookup_caller =
+ m_msg_lookup_utility_up->MakeFunctionCaller(void_ptr_type, args,
+ thread_sp, error);
if (error.Fail()) {
- m_msg_lookup_caller_up.reset();
+ LLDB_LOG(log, "[GNUstep] failed to make objc_msg_lookup caller: {0}",
+ error.AsCString());
+ m_msg_lookup_caller = nullptr;
return nullptr;
}
- return m_msg_lookup_caller_up.get();
+ return m_msg_lookup_caller;
}
void GNUstepObjCRuntime::UpdateISAToDescriptorMapIfNeeded() {
diff --git a/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCRuntime.h b/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCRuntime.h
index 6b5f0b5c5c88c..a7042c6f25ccd 100644
--- a/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCRuntime.h
+++ b/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCRuntime.h
@@ -14,6 +14,7 @@
#include "Plugins/LanguageRuntime/ObjC/ObjCLanguageRuntime.h"
+#include "llvm/ADT/SmallVector.h"
#include "llvm/ADT/StringRef.h"
#include "llvm/Support/Error.h"
@@ -126,6 +127,20 @@ class GNUstepObjCRuntime : public lldb_private::ObjCLanguageRuntime {
// Call CreateInstance instead.
GNUstepObjCRuntime(Process *process);
+ /// A libobjc2 message dispatch entry point, identified by the load address
+ /// of its first instruction (resolved by symbol name, so it is robust to
+ /// local labels sharing the address).
+ struct DispatchEntryPoint {
+ lldb::addr_t address;
+ bool is_stret;
+ bool is_sender;
+ };
+
+ /// Returns the dispatch entry point whose first instruction is at \p pc, or
+ /// nullptr. The entry-point address table is resolved and cached on first
+ /// use.
+ const DispatchEntryPoint *FindDispatchEntryPoint(lldb::addr_t pc);
+
/// Finds a complete Objective-C interface type named \p class_name in the
/// target's debug info. Used to attach a real type to a dynamic value when
/// the base class's symbol-name-keyed cache misses (the gnustep-2.x class
@@ -138,11 +153,12 @@ class GNUstepObjCRuntime : public lldb_private::ObjCLanguageRuntime {
Address *GetPrintForDebuggerAddr();
public:
- /// Lazily-built FunctionCaller for libobjc2's
+ /// Lazily-built FunctionCaller for a utility function that resolves a
+ /// method implementation via libobjc2's
/// `IMP objc_msg_lookup(id receiver, SEL selector)`, used by the
- /// step-through-trampoline plan. Returns nullptr if the symbol cannot be
- /// resolved.
- FunctionCaller *GetMsgLookupFunctionCaller();
+ /// step-through-trampoline plan. Returns nullptr on failure. The caller is
+ /// owned by the utility function and stays valid for the runtime's life.
+ FunctionCaller *GetMsgLookupFunctionCaller(Thread &thread);
protected:
@@ -152,7 +168,12 @@ class GNUstepObjCRuntime : public lldb_private::ObjCLanguageRuntime {
std::unique_ptr<FunctionCaller> m_print_object_caller_up;
- std::unique_ptr<FunctionCaller> m_msg_lookup_caller_up;
+ /// Utility function wrapping objc_msg_lookup; owns m_msg_lookup_caller.
+ std::unique_ptr<UtilityFunction> m_msg_lookup_utility_up;
+ FunctionCaller *m_msg_lookup_caller = nullptr;
+
+ llvm::SmallVector<DispatchEntryPoint, 5> m_dispatch_entry_points;
+ bool m_dispatch_entry_points_resolved = false;
std::unique_ptr<GNUstepTaggedPointerVendor> m_tagged_pointer_vendor_up;
diff --git a/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepThreadPlanStepThroughObjCTrampoline.cpp b/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepThreadPlanStepThroughObjCTrampoline.cpp
index 842bc406c68ea..8b5e28cd99072 100644
--- a/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepThreadPlanStepThroughObjCTrampoline.cpp
+++ b/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepThreadPlanStepThroughObjCTrampoline.cpp
@@ -58,16 +58,18 @@ bool GNUstepThreadPlanStepThroughObjCTrampoline::InitializeFunctionCaller() {
if (m_func_sp)
return true;
- m_lookup_function = m_runtime.GetMsgLookupFunctionCaller();
+ m_lookup_function = m_runtime.GetMsgLookupFunctionCaller(GetThread());
if (!m_lookup_function)
return false;
ExecutionContext exe_ctx;
GetThread().CalculateExecutionContext(exe_ctx);
+ // The wrapper was already compiled into the inferior when the caller was
+ // built (GetMsgLookupFunctionCaller); only write a fresh argument struct
+ // here. m_args_addr starts invalid so WriteFunctionArguments allocates one.
DiagnosticManager diagnostics;
- if (!m_lookup_function->InsertFunction(exe_ctx, m_args_addr, diagnostics))
- return false;
+ m_args_addr = LLDB_INVALID_ADDRESS;
if (!m_lookup_function->WriteFunctionArguments(exe_ctx, m_args_addr,
m_input_values, diagnostics))
return false;
>From c1e36972e3d0952b841913ae13e2859ca052109b Mon Sep 17 00:00:00 2001
From: Rob <robk at robk.dev>
Date: Wed, 12 Aug 2026 10:24:42 +0100
Subject: [PATCH 09/49] [lldb][GNUstep] Harden runtime for all libobjc2
configurations
Correctness and robustness work across the plugin:
Detection: identify the runtime by a defined __objc_load rather than by
library file name, so statically linked (BUILD_STATIC_LIBOBJC) and
renamed (LIBOBJC_NAME) builds are recognized too. Undefined references
are ignored, since every module compiled against libobjc2 carries one.
Data model: compute struct objc_class field offsets from the target's
data model instead of assuming pointer stride. The trailing fields are
`long`, which is 32 bits on Windows, so instance_size lives at a
different offset there. Likewise use the COFF symbol prefix ($_) and
section names (.objcrt$SEL) on Windows, and recognize
objc_msgSend_stret2.
Memory safety: bound the class-name read, and validate a candidate class
by cross-checking the meta flag against its metaclass. Only report a
superclass and instance size for resolved classes, since before the
runtime resolves a class those fields hold a name pointer and the
negated size of the class's own ivars.
Correct 32-bit signed tagged payloads by sign-extending from the
target's pointer width.
Feature probing: only claim literal/subscripting support when the
Foundation classes it lowers to are present, so a bare libobjc2 process
reports a clean error at compile time instead of failing inside the
inferior.
Caching: invalidate the dispatch entry points, tagged pointer table and
negative type lookups when modules load, scan only newly loaded modules
for classes rather than re-walking every symbol table, and cache classes
that have no debug info.
Step-through: guard against a lookup that was never set up, stop instead
of stepping into the runtime's forwarding machinery, clear the
pre-resume action when the plan is popped, and defer building the call
wrapper to the pre-resume action. Serialize its construction and latch
failures.
Assisted-by: Claude Opus 5
---
.../GNUstepObjCClassDescriptor.cpp | 138 ++++++++---
.../GNUstepObjCClassDescriptor.h | 41 +++-
.../GNUstepObjCRuntime/GNUstepObjCRuntime.cpp | 231 +++++++++++++-----
.../GNUstepObjCRuntime/GNUstepObjCRuntime.h | 60 +++--
...tepThreadPlanStepThroughObjCTrampoline.cpp | 26 ++
...UstepThreadPlanStepThroughObjCTrampoline.h | 2 +
6 files changed, 361 insertions(+), 137 deletions(-)
diff --git a/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCClassDescriptor.cpp b/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCClassDescriptor.cpp
index 816b2cae1ade8..f310bff13aa51 100644
--- a/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCClassDescriptor.cpp
+++ b/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCClassDescriptor.cpp
@@ -14,6 +14,7 @@
#include "lldb/Symbol/SymbolContext.h"
#include "lldb/Target/Process.h"
#include "lldb/Target/Target.h"
+#include "lldb/Utility/ArchSpec.h"
#include "lldb/Utility/ConstString.h"
#include "lldb/Utility/LLDBLog.h"
#include "lldb/Utility/Log.h"
@@ -22,16 +23,44 @@
using namespace lldb;
using namespace lldb_private;
-// Field indices into libobjc2's `struct objc_class` (see class documentation
-// in the header).
-static constexpr uint64_t kClassFieldIsa = 0;
-static constexpr uint64_t kClassFieldSuperclass = 1;
-static constexpr uint64_t kClassFieldName = 2;
-static constexpr uint64_t kClassFieldInstanceSize = 5;
-
-// An upper bound for plausible class names; longer strings indicate that the
-// name pointer does not actually point at a class name.
-static constexpr size_t kMaxClassNameLength = 512;
+// Flags from libobjc2's `enum objc_class_flags` (class.h).
+static constexpr uint64_t g_class_flag_meta = 1ULL << 0;
+static constexpr uint64_t g_class_flag_resolved = 1ULL << 9;
+
+// An upper bound for plausible class names. A string that does not terminate
+// within this many bytes is not a class name, and stopping there keeps a
+// stray pointer from dragging in arbitrary amounts of inferior memory.
+static constexpr size_t g_max_class_name_length = 256;
+
+namespace {
+/// Offsets of the `struct objc_class` fields this descriptor reads. The first
+/// three fields are pointers; the rest are `long`, which is not always the
+/// same width (see the class documentation).
+struct ClassLayout {
+ uint32_t pointer_size;
+ uint32_t long_size;
+ uint64_t superclass_offset;
+ uint64_t name_offset;
+ uint64_t info_offset;
+ uint64_t instance_size_offset;
+};
+
+ClassLayout GetClassLayout(Process &process) {
+ ClassLayout layout;
+ layout.pointer_size = process.GetAddressByteSize();
+ // Windows is LLP64, so `long` stays 32 bits there while pointers are 64.
+ const llvm::Triple &triple =
+ process.GetTarget().GetArchitecture().GetTriple();
+ layout.long_size = (triple.isOSWindows() && layout.pointer_size == 8)
+ ? 4
+ : layout.pointer_size;
+ layout.superclass_offset = layout.pointer_size;
+ layout.name_offset = 2 * layout.pointer_size;
+ layout.info_offset = 3 * layout.pointer_size + layout.long_size;
+ layout.instance_size_offset = 3 * layout.pointer_size + 2 * layout.long_size;
+ return layout;
+}
+} // namespace
GNUstepObjCClassDescriptor::GNUstepObjCClassDescriptor(
ProcessSP process_sp, ObjCLanguageRuntime::ObjCISA isa)
@@ -44,46 +73,66 @@ void GNUstepObjCClassDescriptor::Read() {
if (!process_sp || m_isa == 0 || m_isa == LLDB_INVALID_ADDRESS)
return;
- const uint32_t addr_size = process_sp->GetAddressByteSize();
+ const ClassLayout layout = GetClassLayout(*process_sp);
// Class objects are at least pointer-aligned.
- if (m_isa % addr_size != 0)
+ if (m_isa % layout.pointer_size != 0)
return;
Status error;
- auto read_field = [&](uint64_t index) -> addr_t {
- addr_t value = process_sp->ReadPointerFromMemory(
- m_isa + index * addr_size, error);
+ auto read_pointer = [&](uint64_t offset) -> addr_t {
+ addr_t value = process_sp->ReadPointerFromMemory(m_isa + offset, error);
return error.Fail() ? LLDB_INVALID_ADDRESS : value;
};
- const addr_t metaclass = read_field(kClassFieldIsa);
- if (metaclass == LLDB_INVALID_ADDRESS)
+ const addr_t metaclass = read_pointer(0);
+ if (metaclass == 0 || metaclass == LLDB_INVALID_ADDRESS)
return;
- const addr_t superclass = read_field(kClassFieldSuperclass);
+ const addr_t superclass = read_pointer(layout.superclass_offset);
if (superclass == LLDB_INVALID_ADDRESS)
return;
- const addr_t name_ptr = read_field(kClassFieldName);
- if (name_ptr == LLDB_INVALID_ADDRESS || name_ptr == 0)
+ const addr_t name_ptr = read_pointer(layout.name_offset);
+ if (name_ptr == 0 || name_ptr == LLDB_INVALID_ADDRESS)
return;
- std::string name;
- process_sp->ReadCStringFromMemory(name_ptr, name, error);
- if (error.Fail() || name.empty() || name.size() >= kMaxClassNameLength)
+ char name_buffer[g_max_class_name_length];
+ const size_t name_length = process_sp->ReadCStringFromMemory(
+ name_ptr, name_buffer, sizeof(name_buffer), error);
+ // A string that fills the buffer was truncated, so it is not a class name.
+ if (error.Fail() || name_length == 0 ||
+ name_length >= sizeof(name_buffer) - 1)
return;
- // `instance_size` is a signed `long`. With the non-fragile ABI it is
- // negative until the runtime registers the class; take the magnitude so a
- // not-yet-registered class still yields a usable size.
- const int64_t instance_size = process_sp->ReadSignedIntegerFromMemory(
- m_isa + kClassFieldInstanceSize * addr_size, addr_size, 0, error);
+ const uint64_t info = process_sp->ReadUnsignedIntegerFromMemory(
+ m_isa + layout.info_offset, layout.long_size, 0, error);
if (error.Fail())
return;
+ // A class and its metaclass must disagree about the meta flag. Checking
+ // both directions is what keeps an arbitrary readable address from being
+ // accepted as a class.
+ const bool is_meta = (info & g_class_flag_meta) != 0;
+ if (!is_meta) {
+ const uint64_t metaclass_info = process_sp->ReadUnsignedIntegerFromMemory(
+ metaclass + layout.info_offset, layout.long_size, 0, error);
+ if (error.Fail() || (metaclass_info & g_class_flag_meta) == 0)
+ return;
+ }
+
+ // Only a resolved class has a real superclass pointer and instance size;
+ // see the class documentation.
+ const bool resolved = (info & g_class_flag_resolved) != 0;
+ if (resolved) {
+ const int64_t instance_size = process_sp->ReadSignedIntegerFromMemory(
+ m_isa + layout.instance_size_offset, layout.long_size, 0, error);
+ if (error.Fail())
+ return;
+ m_instance_size = static_cast<uint64_t>(
+ instance_size < 0 ? -instance_size : instance_size);
+ m_superclass_isa = superclass;
+ }
+
m_metaclass_isa = metaclass;
- m_superclass_isa = superclass;
- m_name = ConstString(name);
- m_instance_size = static_cast<uint64_t>(
- instance_size < 0 ? -instance_size : instance_size);
+ m_name = ConstString(name_buffer);
m_valid = true;
}
@@ -130,9 +179,15 @@ bool GNUstepObjCTaggedPointerClassDescriptor::GetTaggedPointerInfoSigned(
uint64_t *info_bits, int64_t *value_bits, uint64_t *payload) {
if (info_bits)
*info_bits = m_tag;
- if (value_bits)
- *value_bits =
- static_cast<int64_t>(m_pointer_value) >> m_payload_shift;
+ if (value_bits) {
+ // Sign-extend from the target's pointer width before shifting, so that a
+ // negative payload in a 32-bit pointer is not read as a large positive.
+ const uint32_t pointer_bits = m_pointer_size * 8;
+ int64_t signed_value = static_cast<int64_t>(m_pointer_value)
+ << (64 - pointer_bits);
+ signed_value >>= (64 - pointer_bits);
+ *value_bits = signed_value >> m_payload_shift;
+ }
if (payload)
*payload = m_pointer_value;
return true;
@@ -145,7 +200,8 @@ bool GNUstepTaggedPointerVendor::IsPossibleTaggedPointer(lldb::addr_t ptr) {
std::unique_ptr<ObjCLanguageRuntime::ClassDescriptor>
GNUstepTaggedPointerVendor::GetClassDescriptor(lldb::addr_t ptr) {
- const bool is_64_bit = m_process.GetAddressByteSize() == 8;
+ const uint32_t pointer_size = m_process.GetAddressByteSize();
+ const bool is_64_bit = pointer_size == 8;
const uint64_t mask = is_64_bit ? 7 : 1;
const uint32_t payload_shift = is_64_bit ? 3 : 1;
const uint64_t tag = ptr & mask;
@@ -153,8 +209,9 @@ GNUstepTaggedPointerVendor::GetClassDescriptor(lldb::addr_t ptr) {
return nullptr;
// Mirror libobjc2's classForObject(): 32-bit targets have a single small
- // object class at index 0; 64-bit targets index the table by the tag. The
- // table has 7 entries, so reject out-of-range tags.
+ // object class at index 0; 64-bit targets index the table by the tag.
+ // `SmallObjectClasses` has 7 entries (class_table.c), so a tag of 7 has no
+ // corresponding class.
const uint64_t index = is_64_bit ? tag : 0;
if (index > 6)
return nullptr;
@@ -184,13 +241,14 @@ GNUstepTaggedPointerVendor::GetClassDescriptor(lldb::addr_t ptr) {
Status error;
const addr_t isa = m_process.ReadPointerFromMemory(
- *m_table_addr + index * m_process.GetAddressByteSize(), error);
+ *m_table_addr + index * pointer_size, error);
if (error.Fail() || isa == 0 || isa == LLDB_INVALID_ADDRESS)
return nullptr;
auto descriptor_up =
std::make_unique<GNUstepObjCTaggedPointerClassDescriptor>(
- m_process.shared_from_this(), isa, ptr, tag, payload_shift);
+ m_process.shared_from_this(), isa, ptr, tag, payload_shift,
+ pointer_size);
if (!descriptor_up->IsValid())
return nullptr;
return descriptor_up;
diff --git a/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCClassDescriptor.h b/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCClassDescriptor.h
index 949b9f98ca9d6..e1b2ee240d931 100644
--- a/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCClassDescriptor.h
+++ b/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCClassDescriptor.h
@@ -25,16 +25,23 @@ namespace lldb_private {
/// The layout parsed here is libobjc2's `struct objc_class` (class.h), whose
/// leading fields have been stable across the gnustep-2.x ABI:
///
-/// Class isa; // metaclass [index 0]
-/// Class super_class; // [index 1]
-/// const char *name; // [index 2]
-/// long version; // [index 3]
-/// unsigned long info; // flag bits [index 4]
-/// long instance_size; // [index 5]
+/// Class isa; // metaclass
+/// Class super_class;
+/// const char *name;
+/// long version;
+/// unsigned long info; // enum objc_class_flags
+/// long instance_size;
///
-/// Note: with the non-fragile ABI the compiler emits a negative
-/// instance_size; the runtime replaces it with the real size when the class
-/// is registered, so debug-time reads of loaded classes see the real value.
+/// Note that the last three fields are `long`, which is 32 bits on Windows
+/// (LLP64) and pointer-sized on the LP64 and ILP32 targets libobjc2 supports,
+/// so field offsets are computed from the target's data model rather than from
+/// the pointer size alone.
+///
+/// Classes emitted by the compiler are only fully formed once the runtime has
+/// resolved them (`objc_class_flag_resolved`): before that, `super_class` may
+/// still hold the superclass *name* rather than a Class, and `instance_size`
+/// holds the negated size of just this class's own ivars. Both are therefore
+/// only reported for resolved classes.
class GNUstepObjCClassDescriptor : public ObjCLanguageRuntime::ClassDescriptor {
public:
GNUstepObjCClassDescriptor(lldb::ProcessSP process_sp,
@@ -68,8 +75,9 @@ class GNUstepObjCClassDescriptor : public ObjCLanguageRuntime::ClassDescriptor {
ObjCLanguageRuntime::ObjCISA GetISA() override { return m_isa; }
protected:
- /// Parse `struct objc_class` at m_isa. Called from the constructor;
- /// sets m_valid on success.
+ /// Parse `struct objc_class` at m_isa. Called from the constructor; sets
+ /// m_valid only if the structure passes the consistency checks that keep a
+ /// stray pointer into readable memory from being reported as a class.
void Read();
lldb::ProcessWP m_process_wp;
@@ -91,10 +99,11 @@ class GNUstepObjCTaggedPointerClassDescriptor
GNUstepObjCTaggedPointerClassDescriptor(lldb::ProcessSP process_sp,
ObjCLanguageRuntime::ObjCISA isa,
lldb::addr_t pointer_value,
- uint64_t tag, uint32_t payload_shift)
+ uint64_t tag, uint32_t payload_shift,
+ uint32_t pointer_size)
: GNUstepObjCClassDescriptor(std::move(process_sp), isa),
m_pointer_value(pointer_value), m_tag(tag),
- m_payload_shift(payload_shift) {}
+ m_payload_shift(payload_shift), m_pointer_size(pointer_size) {}
bool GetTaggedPointerInfo(uint64_t *info_bits = nullptr,
uint64_t *value_bits = nullptr,
@@ -108,6 +117,7 @@ class GNUstepObjCTaggedPointerClassDescriptor
lldb::addr_t m_pointer_value;
uint64_t m_tag;
uint32_t m_payload_shift;
+ uint32_t m_pointer_size;
};
/// Resolves tagged pointers by mirroring libobjc2's `classForObject()`
@@ -130,6 +140,11 @@ class GNUstepTaggedPointerVendor
std::unique_ptr<ObjCLanguageRuntime::ClassDescriptor>
GetClassDescriptor(lldb::addr_t ptr) override;
+ /// Forget where (or whether) the small object class table was found, so a
+ /// newly loaded runtime is picked up and a negative result is not cached
+ /// for the lifetime of the process.
+ void ModulesDidLoad() { m_table_addr.reset(); }
+
private:
/// Load address of libobjc2's `SmallObjectClasses` table, resolved lazily
/// and cached. LLDB_INVALID_ADDRESS inside the optional means resolution
diff --git a/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCRuntime.cpp b/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCRuntime.cpp
index 52834e5f9eae5..4d6078589ccab 100644
--- a/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCRuntime.cpp
+++ b/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCRuntime.cpp
@@ -42,6 +42,7 @@
#include "llvm/IR/LegacyPassManager.h"
#include "llvm/IR/Module.h"
#include "llvm/Pass.h"
+#include "llvm/Support/Regex.h"
using namespace lldb;
using namespace lldb_private;
@@ -71,11 +72,18 @@ class GNUstepObjCSelectorRegistrationPass : public llvm::ModulePass {
}
bool runOnModule(llvm::Module &module) override {
+ // Section names differ by object format: "__objc_selectors" everywhere
+ // except COFF, which sorts the runtime metadata into ".objcrt$SEL"
+ // subsections (CGObjCGNU.cpp).
llvm::SmallVector<llvm::GlobalVariable *, 8> sel_globals;
- for (llvm::GlobalVariable &gv : module.globals())
- if (gv.hasSection() &&
- llvm::StringRef(gv.getSection()).starts_with("__objc_selectors"))
+ for (llvm::GlobalVariable &gv : module.globals()) {
+ if (!gv.hasSection())
+ continue;
+ llvm::StringRef section(gv.getSection());
+ if (section.starts_with("__objc_selectors") ||
+ section.starts_with(".objcrt$SEL"))
sel_globals.push_back(&gv);
+ }
if (sel_globals.empty())
return false;
@@ -104,8 +112,14 @@ class GNUstepObjCSelectorRegistrationPass : public llvm::ModulePass {
uses.push_back(&use);
for (llvm::Use *use : uses) {
auto *inst = llvm::dyn_cast<llvm::Instruction>(use->getUser());
- if (!inst)
+ if (!inst) {
+ // A constant expression has no instruction to anchor the call to;
+ // such a selector stays unregistered.
+ LLDB_LOG(GetLog(LLDBLog::Expressions),
+ "not registering selector used by a constant expression: {0}",
+ gv->getName());
continue;
+ }
llvm::Function *func = inst->getFunction();
llvm::Value *®_call = call_per_fn[func];
if (!reg_call) {
@@ -152,31 +166,40 @@ void GNUstepObjCRuntime::Terminate() {
PluginManager::UnregisterPlugin(CreateInstance);
}
-static bool CanModuleBeGNUstepObjCLibrary(const ModuleSP &module_sp,
- const llvm::Triple &TT) {
+/// Returns true if \p module_sp defines (rather than merely references) a
+/// function named \p name.
+static bool ModuleDefinesFunction(const ModuleSP &module_sp,
+ llvm::StringRef name) {
if (!module_sp)
return false;
- const FileSpec &module_file_spec = module_sp->GetFileSpec();
- if (!module_file_spec)
- return false;
- llvm::StringRef filename = module_file_spec.GetFilename();
- if (TT.isOSBinFormatELF())
- return filename.starts_with("libobjc.so");
- if (TT.isOSWindows())
- return filename == "objc.dll";
+ SymbolContextList sc_list;
+ module_sp->FindSymbolsWithNameAndType(ConstString(name), eSymbolTypeCode,
+ sc_list);
+ for (const SymbolContext &sc : sc_list) {
+ // Every module compiled against libobjc2 carries an undefined reference
+ // to __objc_load from its .objc_init constructor, so only a definition
+ // identifies the runtime itself.
+ if (sc.symbol && sc.symbol->GetAddress().IsValid())
+ return true;
+ }
return false;
}
-static bool ScanForGNUstepObjCLibraryCandidate(const ModuleList &modules,
- const llvm::Triple &TT) {
+/// Finds the module implementing the libobjc2 runtime, identified by its
+/// loader entry point. __objc_load is exported by every libobjc2 build on
+/// every platform and does not exist in GCC's Objective-C runtime, so this
+/// both avoids activating for an unrelated runtime and recognizes builds the
+/// file name does not identify: a renamed library (LIBOBJC_NAME) or a static
+/// libobjc2, whose symbols land in the executable itself.
+static ModuleSP FindGNUstepObjCRuntimeModule(const ModuleList &modules) {
std::lock_guard<std::recursive_mutex> guard(modules.GetMutex());
- size_t num_modules = modules.GetSize();
+ const size_t num_modules = modules.GetSize();
for (size_t i = 0; i < num_modules; i++) {
- auto mod = modules.GetModuleAtIndex(i);
- if (CanModuleBeGNUstepObjCLibrary(mod, TT))
- return true;
+ ModuleSP module_sp = modules.GetModuleAtIndex(i);
+ if (ModuleDefinesFunction(module_sp, "__objc_load"))
+ return module_sp;
}
- return false;
+ return ModuleSP();
}
LanguageRuntime *GNUstepObjCRuntime::CreateInstance(Process *process,
@@ -191,24 +214,9 @@ LanguageRuntime *GNUstepObjCRuntime::CreateInstance(Process *process,
if (TT.getVendor() == llvm::Triple::VendorType::Apple)
return nullptr;
- const ModuleList &images = target.GetImages();
- if (!ScanForGNUstepObjCLibraryCandidate(images, TT))
+ if (!FindGNUstepObjCRuntimeModule(target.GetImages()))
return nullptr;
- if (TT.isOSBinFormatELF()) {
- SymbolContextList eh_pers;
- RegularExpression regex("__gnustep_objc[x]*_personality_v[0-9]+");
- images.FindSymbolsMatchingRegExAndType(regex, eSymbolTypeCode, eh_pers);
- if (eh_pers.GetSize() == 0)
- return nullptr;
- } else if (TT.isOSWindows()) {
- SymbolContextList objc_mandatory;
- images.FindSymbolsWithNameAndType(ConstString("__objc_load"),
- eSymbolTypeCode, objc_mandatory);
- if (objc_mandatory.GetSize() == 0)
- return nullptr;
- }
-
return new GNUstepObjCRuntime(process);
}
@@ -416,6 +424,12 @@ bool GNUstepObjCRuntime::GetDynamicTypeAndAddress(
lldb::TypeSP
GNUstepObjCRuntime::LookupClassTypeInDebugInfo(ConstString class_name) {
+ // Searching every module's debug info is expensive and happens for each
+ // value on each stop, so remember the classes that have no debug info. The
+ // cache is dropped whenever new modules arrive.
+ if (m_negative_type_cache.count(class_name))
+ return TypeSP();
+
TypeQuery query(class_name.GetStringRef(), TypeQueryOptions::e_exact_match);
TypeResults results;
GetTargetRef().GetImages().FindTypes(nullptr, query, results);
@@ -424,9 +438,30 @@ GNUstepObjCRuntime::LookupClassTypeInDebugInfo(ConstString class_name) {
type_sp->GetForwardCompilerType()))
return type_sp;
}
+ m_negative_type_cache.insert(class_name);
return TypeSP();
}
+bool GNUstepObjCRuntime::CalculateHasNewLiteralsAndIndexing() {
+ // The literal and subscripting syntax lowers to calls on Foundation
+ // classes, which live in gnustep-base rather than in the runtime itself.
+ // Claiming support without them makes such expressions compile and then
+ // fail inside the inferior, so require the classes to be present.
+ static constexpr llvm::StringLiteral g_required_classes[] = {
+ "NSArray", "NSDictionary", "NSNumber", "NSString"};
+
+ const llvm::StringRef prefix = GetClassSymbolPrefix();
+ const ModuleList &images = GetTargetRef().GetImages();
+ for (llvm::StringRef class_name : g_required_classes) {
+ SymbolContextList sc_list;
+ images.FindSymbolsWithNameAndType(ConstString(prefix.str() + class_name.str()),
+ eSymbolTypeAny, sc_list);
+ if (sc_list.GetSize() == 0)
+ return false;
+ }
+ return true;
+}
+
TypeAndOrName
GNUstepObjCRuntime::FixUpDynamicType(const TypeAndOrName &type_and_or_name,
ValueObject &static_value) {
@@ -509,7 +544,7 @@ GNUstepObjCRuntime::GetStepThroughTrampolinePlan(Thread &thread,
const addr_t pc = thread.GetRegisterContext()->GetPC();
Target &target = GetTargetRef();
- const DispatchEntryPoint *entry = FindDispatchEntryPoint(pc);
+ std::optional<DispatchEntryPoint> entry = FindDispatchEntryPoint(pc);
if (!entry)
return {};
const bool is_stret = entry->is_stret;
@@ -575,7 +610,10 @@ GNUstepObjCRuntime::GetStepThroughTrampolinePlan(Thread &thread,
}
}
- if (!GetMsgLookupFunctionCaller(thread))
+ // Only claim the step if the runtime actually exports the lookup function.
+ // Building the call wrapper is deliberately left to the plan's pre-resume
+ // action, which is where running code in the inferior is safe.
+ if (!ModuleDefinesFunction(m_objc_module_sp, "objc_msg_lookup"))
return {};
ValueList lookup_args;
@@ -590,7 +628,7 @@ GNUstepObjCRuntime::GetStepThroughTrampolinePlan(Thread &thread,
thread, *this, lookup_args, isa, selector);
}
-const GNUstepObjCRuntime::DispatchEntryPoint *
+std::optional<GNUstepObjCRuntime::DispatchEntryPoint>
GNUstepObjCRuntime::FindDispatchEntryPoint(lldb::addr_t pc) {
if (!m_dispatch_entry_points_resolved) {
m_dispatch_entry_points_resolved = true;
@@ -601,41 +639,43 @@ GNUstepObjCRuntime::FindDispatchEntryPoint(lldb::addr_t pc) {
const char *name;
bool is_stret;
bool is_sender;
- } kEntryPoints[] = {
+ } g_entry_points[] = {
{"objc_msgSend", false, false},
{"objc_msgSend_fpret", false, false},
{"objc_msgSend_stret", true, false},
+ // Windows on ARM64 dispatches struct returns through this variant.
+ {"objc_msgSend_stret2", true, false},
{"objc_msg_lookup", false, false},
{"objc_msg_lookup_sender", false, true},
};
Target &target = GetTargetRef();
- for (const auto &ep : kEntryPoints) {
+ for (const auto &ep : g_entry_points) {
SymbolContextList sc_list;
target.GetImages().FindSymbolsWithNameAndType(ConstString(ep.name),
eSymbolTypeCode, sc_list);
for (const SymbolContext &sc : sc_list) {
if (!sc.symbol)
continue;
- const addr_t addr = sc.symbol->GetLoadAddress(&target);
- if (addr != LLDB_INVALID_ADDRESS) {
+ // Use the opcode address so the comparison against the PC is correct
+ // on targets where the symbol address carries an ISA bit (Thumb).
+ const addr_t addr =
+ sc.symbol->GetAddress().GetOpcodeLoadAddress(&target);
+ if (addr != LLDB_INVALID_ADDRESS)
m_dispatch_entry_points.push_back({addr, ep.is_stret, ep.is_sender});
- break;
- }
}
}
}
for (const DispatchEntryPoint &ep : m_dispatch_entry_points)
if (ep.address == pc)
- return &ep;
- return nullptr;
+ return ep;
+ return std::nullopt;
}
FunctionCaller *GNUstepObjCRuntime::GetMsgLookupFunctionCaller(Thread &thread) {
// Build (once) a utility function that resolves a method implementation by
// calling libobjc2's objc_msg_lookup, and a FunctionCaller to invoke it.
- // This mirrors AppleObjCTrampolineHandler's dispatch-lookup utility and is
- // the JIT path that works from inside a step's PreResume action.
+ // This mirrors AppleObjCTrampolineHandler's dispatch-lookup utility.
static const char *g_lookup_name = "$__lldb_gnustep_objc_msg_lookup";
static const char *g_lookup_code =
"void *objc_msg_lookup(void *receiver, void *selector);\n"
@@ -643,8 +683,13 @@ FunctionCaller *GNUstepObjCRuntime::GetMsgLookupFunctionCaller(Thread &thread) {
" return objc_msg_lookup(receiver, selector);\n"
"}\n";
+ std::lock_guard<std::mutex> guard(m_msg_lookup_mutex);
if (m_msg_lookup_caller)
return m_msg_lookup_caller;
+ // Don't pay for compiling the wrapper again on every step once it is known
+ // not to work in this process.
+ if (m_msg_lookup_failed)
+ return nullptr;
ThreadSP thread_sp(thread.shared_from_this());
ExecutionContext exe_ctx(thread_sp);
@@ -654,15 +699,18 @@ FunctionCaller *GNUstepObjCRuntime::GetMsgLookupFunctionCaller(Thread &thread) {
g_lookup_code, g_lookup_name, eLanguageTypeC, exe_ctx);
if (!utility_fn_or_error) {
LLDB_LOG_ERROR(log, utility_fn_or_error.takeError(),
- "[GNUstep] failed to build objc_msg_lookup utility: {0}");
+ "failed to build objc_msg_lookup utility: {0}");
+ m_msg_lookup_failed = true;
return nullptr;
}
m_msg_lookup_utility_up = std::move(*utility_fn_or_error);
TypeSystemClangSP scratch_ts_sp =
ScratchTypeSystemClang::GetForTarget(GetTargetRef());
- if (!scratch_ts_sp)
+ if (!scratch_ts_sp) {
+ m_msg_lookup_failed = true;
return nullptr;
+ }
CompilerType void_ptr_type =
scratch_ts_sp->GetBasicType(eBasicTypeVoid).GetPointerType();
@@ -678,9 +726,10 @@ FunctionCaller *GNUstepObjCRuntime::GetMsgLookupFunctionCaller(Thread &thread) {
m_msg_lookup_utility_up->MakeFunctionCaller(void_ptr_type, args,
thread_sp, error);
if (error.Fail()) {
- LLDB_LOG(log, "[GNUstep] failed to make objc_msg_lookup caller: {0}",
+ LLDB_LOG(log, "failed to make objc_msg_lookup caller: {0}",
error.AsCString());
m_msg_lookup_caller = nullptr;
+ m_msg_lookup_failed = true;
return nullptr;
}
return m_msg_lookup_caller;
@@ -695,19 +744,58 @@ void GNUstepObjCRuntime::UpdateISAToDescriptorMapIfNeeded() {
return;
}
- // The gnustep-2.x ABI emits every compiled class as a `._OBJC_CLASS_<name>`
+ // The first update has to look at everything already loaded; afterwards only
+ // the modules that arrived since need scanning, so a dlopen does not re-walk
+ // every symbol table in the process.
+ if (m_swept_all_modules) {
+ for (const ModuleSP &module_sp : m_pending_modules)
+ AddClassesFromModule(module_sp);
+ } else {
+ const ModuleList &images = GetTargetRef().GetImages();
+ std::lock_guard<std::recursive_mutex> guard(images.GetMutex());
+ const size_t num_modules = images.GetSize();
+ for (size_t i = 0; i < num_modules; i++)
+ AddClassesFromModule(images.GetModuleAtIndex(i));
+ m_swept_all_modules = true;
+ }
+
+ m_pending_modules.clear();
+ m_isa_map_dirty = false;
+ m_isa_to_descriptor_stop_id = stop_id;
+}
+
+bool GNUstepObjCRuntime::IsRuntimeInternalAddress(lldb::addr_t addr) {
+ if (!m_objc_module_sp || addr == 0 || addr == LLDB_INVALID_ADDRESS)
+ return false;
+ Address resolved;
+ if (!GetTargetRef().ResolveLoadAddress(addr, resolved))
+ return false;
+ return resolved.GetModule() == m_objc_module_sp;
+}
+
+llvm::StringRef GNUstepObjCRuntime::GetClassSymbolPrefix() {
+ // clang mangles the public runtime symbols with a leading "._" on every
+ // object format except COFF, which uses "$_" (CGObjCGNU.cpp).
+ return GetTargetRef().GetArchitecture().GetTriple().isOSBinFormatCOFF()
+ ? "$_OBJC_CLASS_"
+ : "._OBJC_CLASS_";
+}
+
+void GNUstepObjCRuntime::AddClassesFromModule(const ModuleSP &module_sp) {
+ if (!module_sp || !m_process)
+ return;
+
+ // The gnustep-2.x ABI emits every compiled class as a `<prefix>OBJC_CLASS_`
// data symbol whose address is the class object itself (the ISA of its
// instances), so the map can be seeded from symbol tables alone - without
// running any code in the inferior. Classes created dynamically at runtime
// are handled by the create-on-miss path in GetClassDescriptorFromISA.
- Target &target = GetTargetRef();
- const ModuleList &images = target.GetImages();
-
+ const llvm::StringRef prefix = GetClassSymbolPrefix();
+ RegularExpression regex("^" + llvm::Regex::escape(prefix));
SymbolContextList sc_list;
- RegularExpression regex(llvm::StringRef("^\\._OBJC_CLASS_"));
- images.FindSymbolsMatchingRegExAndType(regex, eSymbolTypeAny, sc_list);
+ module_sp->FindSymbolsMatchingRegExAndType(regex, eSymbolTypeAny, sc_list);
- static constexpr llvm::StringLiteral g_class_prefix("._OBJC_CLASS_");
+ Target &target = GetTargetRef();
for (const SymbolContext &sc : sc_list) {
if (!sc.symbol)
continue;
@@ -715,15 +803,12 @@ void GNUstepObjCRuntime::UpdateISAToDescriptorMapIfNeeded() {
if (isa == 0 || isa == LLDB_INVALID_ADDRESS || ISAIsCached(isa))
continue;
llvm::StringRef name = sc.symbol->GetName().GetStringRef();
- name.consume_front(g_class_prefix);
+ name.consume_front(prefix);
auto descriptor_sp = std::make_shared<GNUstepObjCClassDescriptor>(
m_process->shared_from_this(), isa);
if (descriptor_sp->IsValid())
AddClass(isa, descriptor_sp, name.str().c_str());
}
-
- m_isa_map_dirty = false;
- m_isa_to_descriptor_stop_id = stop_id;
}
bool GNUstepObjCRuntime::GetIRPasses(
@@ -767,8 +852,7 @@ GNUstepObjCRuntime::GetClassDescriptorFromISA(ObjCISA isa) {
}
bool GNUstepObjCRuntime::IsModuleObjCLibrary(const ModuleSP &module_sp) {
- const llvm::Triple &TT = GetTargetRef().GetArchitecture().GetTriple();
- return CanModuleBeGNUstepObjCLibrary(module_sp, TT);
+ return ModuleDefinesFunction(module_sp, "__objc_load");
}
bool GNUstepObjCRuntime::ReadObjCLibrary(const ModuleSP &module_sp) {
@@ -790,5 +874,20 @@ GNUstepObjCRuntime::GetLanguageSpecificData(SymbolContext sc) {
void GNUstepObjCRuntime::ModulesDidLoad(const ModuleList &module_list) {
ReadObjCLibraryIfNeeded(module_list);
+
+ // Everything cached from a symbol lookup can be invalidated by new modules:
+ // classes to add to the map, dispatch entry points that may only now exist,
+ // and negative results that may now resolve.
+ {
+ std::lock_guard<std::recursive_mutex> guard(module_list.GetMutex());
+ const size_t num_modules = module_list.GetSize();
+ for (size_t i = 0; i < num_modules; i++)
+ m_pending_modules.push_back(module_list.GetModuleAtIndex(i));
+ }
m_isa_map_dirty = true;
+ m_dispatch_entry_points.clear();
+ m_dispatch_entry_points_resolved = false;
+ m_negative_type_cache.clear();
+ if (m_tagged_pointer_vendor_up)
+ m_tagged_pointer_vendor_up->ModulesDidLoad();
}
diff --git a/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCRuntime.h b/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCRuntime.h
index a7042c6f25ccd..dd64a8f0d9044 100644
--- a/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCRuntime.h
+++ b/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCRuntime.h
@@ -19,7 +19,10 @@
#include "llvm/Support/Error.h"
#include <memory>
+#include <mutex>
#include <optional>
+#include <set>
+#include <vector>
namespace lldb_private {
@@ -113,9 +116,7 @@ class GNUstepObjCRuntime : public lldb_private::ObjCLanguageRuntime {
/// objc_msgSend unregistered.
bool GetIRPasses(LLVMUserExpression::IRPasses &custom_passes) override;
- /// gnustep-base implements the container-literal and boxed-expression
- /// protocol methods, so @[...], @{...} and @(...) are available.
- bool CalculateHasNewLiteralsAndIndexing() override { return true; }
+ bool CalculateHasNewLiteralsAndIndexing() override;
TaggedPointerVendor *GetTaggedPointerVendor() override;
@@ -123,6 +124,19 @@ class GNUstepObjCRuntime : public lldb_private::ObjCLanguageRuntime {
ClassDescriptorSP GetClassDescriptorFromISA(ObjCISA isa) override;
+ /// Lazily-built FunctionCaller for a utility function that resolves a
+ /// method implementation via libobjc2's
+ /// `IMP objc_msg_lookup(id receiver, SEL selector)`, used by the
+ /// step-through-trampoline plan. Returns nullptr on failure. The caller is
+ /// owned by the utility function and stays valid for the runtime's life.
+ FunctionCaller *GetMsgLookupFunctionCaller(Thread &thread);
+
+ /// Returns true if \p addr belongs to the module implementing the ObjC
+ /// runtime. Method lookups that resolve there reached either the forwarding
+ /// machinery or one of the runtime's own methods, neither of which has user
+ /// source to step into.
+ bool IsRuntimeInternalAddress(lldb::addr_t addr);
+
protected:
// Call CreateInstance instead.
GNUstepObjCRuntime(Process *process);
@@ -136,10 +150,18 @@ class GNUstepObjCRuntime : public lldb_private::ObjCLanguageRuntime {
bool is_sender;
};
- /// Returns the dispatch entry point whose first instruction is at \p pc, or
- /// nullptr. The entry-point address table is resolved and cached on first
- /// use.
- const DispatchEntryPoint *FindDispatchEntryPoint(lldb::addr_t pc);
+ /// Returns the dispatch entry point whose first instruction is at \p pc, if
+ /// any. The entry-point address table is resolved on first use and dropped
+ /// when modules are loaded.
+ std::optional<DispatchEntryPoint> FindDispatchEntryPoint(lldb::addr_t pc);
+
+ /// The prefix clang gives the runtime's public class symbols, which differs
+ /// between object formats.
+ llvm::StringRef GetClassSymbolPrefix();
+
+ /// Adds every class statically defined by \p module_sp to the
+ /// ISA-to-descriptor map.
+ void AddClassesFromModule(const lldb::ModuleSP &module_sp);
/// Finds a complete Objective-C interface type named \p class_name in the
/// target's debug info. Used to attach a real type to a dynamic value when
@@ -152,16 +174,6 @@ class GNUstepObjCRuntime : public lldb_private::ObjCLanguageRuntime {
/// gnustep-base is not loaded in the inferior.
Address *GetPrintForDebuggerAddr();
-public:
- /// Lazily-built FunctionCaller for a utility function that resolves a
- /// method implementation via libobjc2's
- /// `IMP objc_msg_lookup(id receiver, SEL selector)`, used by the
- /// step-through-trampoline plan. Returns nullptr on failure. The caller is
- /// owned by the utility function and stays valid for the runtime's life.
- FunctionCaller *GetMsgLookupFunctionCaller(Thread &thread);
-
-protected:
-
lldb::ModuleSP m_objc_module_sp;
std::unique_ptr<Address> m_print_for_debugger_addr_up;
@@ -169,14 +181,26 @@ class GNUstepObjCRuntime : public lldb_private::ObjCLanguageRuntime {
std::unique_ptr<FunctionCaller> m_print_object_caller_up;
/// Utility function wrapping objc_msg_lookup; owns m_msg_lookup_caller.
+ /// Guarded by m_msg_lookup_mutex, which also latches a failed build so it
+ /// is not retried on every step.
+ std::mutex m_msg_lookup_mutex;
std::unique_ptr<UtilityFunction> m_msg_lookup_utility_up;
FunctionCaller *m_msg_lookup_caller = nullptr;
+ bool m_msg_lookup_failed = false;
- llvm::SmallVector<DispatchEntryPoint, 5> m_dispatch_entry_points;
+ llvm::SmallVector<DispatchEntryPoint, 6> m_dispatch_entry_points;
bool m_dispatch_entry_points_resolved = false;
std::unique_ptr<GNUstepTaggedPointerVendor> m_tagged_pointer_vendor_up;
+ /// Classes named here have no debug info, so the search is not repeated.
+ std::set<ConstString> m_negative_type_cache;
+
+ /// Modules seen since the last ISA-to-descriptor map update, so only new
+ /// symbol tables have to be scanned.
+ std::vector<lldb::ModuleSP> m_pending_modules;
+ bool m_swept_all_modules = false;
+
/// Set when new modules arrive; cleared once the ISA-to-descriptor map has
/// been refreshed, so the symbol sweep only reruns after module changes.
bool m_isa_map_dirty = true;
diff --git a/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepThreadPlanStepThroughObjCTrampoline.cpp b/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepThreadPlanStepThroughObjCTrampoline.cpp
index 8b5e28cd99072..19a0ca8217437 100644
--- a/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepThreadPlanStepThroughObjCTrampoline.cpp
+++ b/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepThreadPlanStepThroughObjCTrampoline.cpp
@@ -47,6 +47,13 @@ void GNUstepThreadPlanStepThroughObjCTrampoline::DidPush() {
(void *)this);
}
+void GNUstepThreadPlanStepThroughObjCTrampoline::DidPop() {
+ // The action holds a bare pointer to this plan, so it must not outlive it -
+ // the plan can be discarded before the process ever resumes.
+ m_process.ClearPreResumeAction(PreResumeInitializeFunctionCaller,
+ (void *)this);
+}
+
bool GNUstepThreadPlanStepThroughObjCTrampoline::
PreResumeInitializeFunctionCaller(void *void_myself) {
auto *myself =
@@ -115,6 +122,14 @@ bool GNUstepThreadPlanStepThroughObjCTrampoline::ShouldStop(Event *event_ptr) {
Log *log = GetLog(LLDBLog::Step);
+ // Setting up the call can fail after the plan is already on the stack, in
+ // which case there is nothing to collect a result from.
+ if (!m_lookup_function || m_args_addr == LLDB_INVALID_ADDRESS) {
+ LLDB_LOG(log, "objc_msg_lookup call was never set up, stopping.");
+ SetPlanComplete(false);
+ return true;
+ }
+
// Second stage: fetch the IMP the lookup returned and run to it.
if (!m_run_to_sp) {
Value target_addr_value;
@@ -134,6 +149,17 @@ bool GNUstepThreadPlanStepThroughObjCTrampoline::ShouldStop(Event *event_ptr) {
return true;
}
+ // A selector the class does not implement resolves to the runtime's
+ // forwarding machinery, which lives inside libobjc itself - as do the
+ // runtime's own internal method implementations. There is no user code to
+ // step into in either case, so stop here instead.
+ if (m_runtime.IsRuntimeInternalAddress(target_addr)) {
+ LLDB_LOG(log, "objc_msg_lookup resolved into the runtime itself "
+ "(forwarding or an internal method), stopping.");
+ SetPlanComplete();
+ return true;
+ }
+
LLDB_LOG(log, "Running to GNUstep ObjC method implementation: {0:x}",
target_addr);
diff --git a/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepThreadPlanStepThroughObjCTrampoline.h b/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepThreadPlanStepThroughObjCTrampoline.h
index 8b61a4d8c26d6..a477050271ff4 100644
--- a/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepThreadPlanStepThroughObjCTrampoline.h
+++ b/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepThreadPlanStepThroughObjCTrampoline.h
@@ -52,6 +52,8 @@ class GNUstepThreadPlanStepThroughObjCTrampoline : public ThreadPlan {
void DidPush() override;
+ void DidPop() override;
+
bool WillStop() override { return true; }
protected:
>From 0e252bd17ac277b1100540880b5b35b7813c5ca1 Mon Sep 17 00:00:00 2001
From: Rob <robk at robk.dev>
Date: Wed, 12 Aug 2026 10:28:59 +0100
Subject: [PATCH 10/49] [lldb][GNUstep] Add unit tests for class structure
parsing
Cover the memory parsing that backs class descriptors with a fake
process serving byte buffers, so the tests need no Objective-C runtime
and run everywhere - which matters because the Shell tests are only
enabled when a GNUstep installation is configured.
The tests are parameterized over the three data models libobjc2
supports, which pins down the field offsets: on Windows `long` is 32
bits while pointers are 64, so every field after the class name sits at
a different offset than on the LP64 and ILP32 targets.
Also cover tagged pointer payload decoding, including sign extension
from the target's pointer width, and the rejection of memory that is
not a class: unmapped or misaligned addresses, missing metaclasses,
absent or unterminated names, and a metaclass whose meta flag disagrees
with its class.
Assisted-by: Claude Opus 5
---
lldb/unittests/CMakeLists.txt | 1 +
lldb/unittests/LanguageRuntime/CMakeLists.txt | 1 +
.../LanguageRuntime/ObjC/CMakeLists.txt | 1 +
.../ObjC/GNUstepObjCRuntime/CMakeLists.txt | 15 +
.../GNUstepObjCClassDescriptorTest.cpp | 335 ++++++++++++++++++
5 files changed, 353 insertions(+)
create mode 100644 lldb/unittests/LanguageRuntime/CMakeLists.txt
create mode 100644 lldb/unittests/LanguageRuntime/ObjC/CMakeLists.txt
create mode 100644 lldb/unittests/LanguageRuntime/ObjC/GNUstepObjCRuntime/CMakeLists.txt
create mode 100644 lldb/unittests/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCClassDescriptorTest.cpp
diff --git a/lldb/unittests/CMakeLists.txt b/lldb/unittests/CMakeLists.txt
index b0b7f68a7dcd6..76f2fa8fca198 100644
--- a/lldb/unittests/CMakeLists.txt
+++ b/lldb/unittests/CMakeLists.txt
@@ -90,6 +90,7 @@ add_subdirectory(Host)
add_subdirectory(Instruction)
add_subdirectory(Interpreter)
add_subdirectory(Language)
+add_subdirectory(LanguageRuntime)
add_subdirectory(ObjectContainer)
add_subdirectory(ObjectFile)
add_subdirectory(Platform)
diff --git a/lldb/unittests/LanguageRuntime/CMakeLists.txt b/lldb/unittests/LanguageRuntime/CMakeLists.txt
new file mode 100644
index 0000000000000..7115e09686740
--- /dev/null
+++ b/lldb/unittests/LanguageRuntime/CMakeLists.txt
@@ -0,0 +1 @@
+add_subdirectory(ObjC)
diff --git a/lldb/unittests/LanguageRuntime/ObjC/CMakeLists.txt b/lldb/unittests/LanguageRuntime/ObjC/CMakeLists.txt
new file mode 100644
index 0000000000000..7cda9782a9278
--- /dev/null
+++ b/lldb/unittests/LanguageRuntime/ObjC/CMakeLists.txt
@@ -0,0 +1 @@
+add_subdirectory(GNUstepObjCRuntime)
diff --git a/lldb/unittests/LanguageRuntime/ObjC/GNUstepObjCRuntime/CMakeLists.txt b/lldb/unittests/LanguageRuntime/ObjC/GNUstepObjCRuntime/CMakeLists.txt
new file mode 100644
index 0000000000000..39b22ebd2816e
--- /dev/null
+++ b/lldb/unittests/LanguageRuntime/ObjC/GNUstepObjCRuntime/CMakeLists.txt
@@ -0,0 +1,15 @@
+add_lldb_unittest(LanguageRuntimeObjCGNUstepTests
+ GNUstepObjCClassDescriptorTest.cpp
+
+ LINK_COMPONENTS
+ Support
+ LINK_LIBS
+ lldbCore
+ lldbHost
+ lldbSymbol
+ lldbTarget
+ lldbUtility
+ lldbPluginGNUstepObjCRuntime
+ lldbPluginPlatformLinux
+ lldbPluginPlatformWindows
+ )
diff --git a/lldb/unittests/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCClassDescriptorTest.cpp b/lldb/unittests/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCClassDescriptorTest.cpp
new file mode 100644
index 0000000000000..5d06a61a9acb2
--- /dev/null
+++ b/lldb/unittests/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCClassDescriptorTest.cpp
@@ -0,0 +1,335 @@
+//===-- GNUstepObjCClassDescriptorTest.cpp --------------------------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+#include "Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCClassDescriptor.h"
+#include "Plugins/Platform/Linux/PlatformLinux.h"
+#include "Plugins/Platform/Windows/PlatformWindows.h"
+#include "TestingSupport/SubsystemRAII.h"
+#include "lldb/Core/Debugger.h"
+#include "lldb/Core/PluginManager.h"
+#include "lldb/Host/FileSystem.h"
+#include "lldb/Host/HostInfo.h"
+#include "lldb/Target/Process.h"
+#include "lldb/Target/Target.h"
+#include "lldb/Utility/ArchSpec.h"
+#include "lldb/Utility/Listener.h"
+
+#include "gtest/gtest.h"
+
+#include <cstring>
+
+using namespace lldb;
+using namespace lldb_private;
+
+namespace {
+
+/// Serves memory reads out of one contiguous block of fake inferior memory, so
+/// that class structures can be laid out byte by byte and handed to the
+/// descriptor without a live process.
+class FakeProcess : public Process {
+public:
+ static constexpr addr_t g_base_addr = 0x100000;
+ static constexpr size_t g_size = 0x2000;
+
+ FakeProcess(TargetSP target_sp, ListenerSP listener_sp)
+ : Process(target_sp, listener_sp), m_memory(g_size, 0) {}
+
+ bool CanDebug(TargetSP, bool) override { return true; }
+ Status DoDestroy() override { return {}; }
+ void RefreshStateAfterStop() override {}
+ bool IsAlive() override { return true; }
+ bool DoUpdateThreadList(ThreadList &, ThreadList &) override { return false; }
+ llvm::StringRef GetPluginName() override { return "fake"; }
+
+ size_t DoReadMemory(const ProcessAddress &process_addr, void *buf,
+ size_t size, Status &error) override {
+ const addr_t vm_addr = process_addr.GetValue();
+ if (vm_addr < g_base_addr || vm_addr >= g_base_addr + m_memory.size()) {
+ error = Status::FromErrorString("address is not mapped");
+ return 0;
+ }
+ const size_t offset = vm_addr - g_base_addr;
+ const size_t bytes = std::min(size, m_memory.size() - offset);
+ std::memcpy(buf, m_memory.data() + offset, bytes);
+ return bytes;
+ }
+
+ // The targets libobjc2 supports are all little-endian.
+ void WriteInteger(addr_t addr, uint64_t value, uint32_t byte_size) {
+ const size_t offset = addr - g_base_addr;
+ for (uint32_t i = 0; i < byte_size; i++)
+ m_memory[offset + i] = (value >> (8 * i)) & 0xff;
+ }
+
+ void WriteCString(addr_t addr, llvm::StringRef str) {
+ const size_t offset = addr - g_base_addr;
+ std::memcpy(m_memory.data() + offset, str.data(), str.size());
+ m_memory[offset + str.size()] = '\0';
+ }
+
+ void Fill(addr_t addr, uint8_t byte, size_t count) {
+ const size_t offset = addr - g_base_addr;
+ std::memset(m_memory.data() + offset, byte, count);
+ }
+
+ std::vector<uint8_t> m_memory;
+};
+
+/// The parts of libobjc2's `struct objc_class` this test lays out. The first
+/// three fields are pointers and the rest are `long`, which is why the two
+/// sizes are tracked separately.
+struct DataModel {
+ const char *triple;
+ uint32_t pointer_size;
+ uint32_t long_size;
+};
+
+class GNUstepClassDescriptorTest : public ::testing::TestWithParam<DataModel> {
+public:
+ void SetUp() override {
+ ArchSpec arch(GetParam().triple);
+ PlatformSP platform_sp =
+ arch.GetTriple().isOSWindows()
+ ? PlatformWindows::CreateInstance(true, &arch)
+ : platform_linux::PlatformLinux::CreateInstance(true, &arch);
+ Platform::SetHostPlatform(platform_sp);
+
+ m_debugger_sp = Debugger::CreateInstance();
+ m_debugger_sp->GetTargetList().CreateTarget(
+ *m_debugger_sp, "", arch, eLoadDependentsNo, platform_sp, m_target_sp);
+ ASSERT_TRUE(m_target_sp);
+
+ ListenerSP listener_sp(Listener::MakeListener("fake"));
+ m_process_sp = std::make_shared<FakeProcess>(m_target_sp, listener_sp);
+ struct TargetHack : public Target {
+ void SetProcess(ProcessSP process) { m_process_sp = process; }
+ };
+ static_cast<TargetHack *>(m_target_sp.get())->SetProcess(m_process_sp);
+ }
+
+ void TearDown() override {
+ m_process_sp.reset();
+ m_target_sp.reset();
+ m_debugger_sp.reset();
+ }
+
+ FakeProcess &GetProcess() {
+ return *static_cast<FakeProcess *>(m_process_sp.get());
+ }
+
+ uint32_t PointerSize() const { return GetParam().pointer_size; }
+ uint32_t LongSize() const { return GetParam().long_size; }
+
+ addr_t InfoOffset() const { return 3 * PointerSize() + LongSize(); }
+ addr_t InstanceSizeOffset() const {
+ return 3 * PointerSize() + 2 * LongSize();
+ }
+ addr_t ClassSize() const { return 3 * PointerSize() + 6 * LongSize(); }
+
+ /// Lays out a class structure, returning its address.
+ addr_t WriteClass(addr_t addr, addr_t metaclass, addr_t superclass,
+ addr_t name_addr, uint64_t info, int64_t instance_size) {
+ FakeProcess &process = GetProcess();
+ process.WriteInteger(addr, metaclass, PointerSize());
+ process.WriteInteger(addr + PointerSize(), superclass, PointerSize());
+ process.WriteInteger(addr + 2 * PointerSize(), name_addr, PointerSize());
+ process.WriteInteger(addr + InfoOffset(), info, LongSize());
+ process.WriteInteger(addr + InstanceSizeOffset(),
+ static_cast<uint64_t>(instance_size), LongSize());
+ return addr;
+ }
+
+ // Flags from libobjc2's enum objc_class_flags.
+ static constexpr uint64_t g_flag_meta = 1ULL << 0;
+ static constexpr uint64_t g_flag_resolved = 1ULL << 9;
+
+ SubsystemRAII<FileSystem, HostInfo, platform_linux::PlatformLinux,
+ PlatformWindows>
+ m_subsystems;
+ DebuggerSP m_debugger_sp;
+ TargetSP m_target_sp;
+ ProcessSP m_process_sp;
+};
+
+// Addresses inside the fake memory block. Kept far from its edges so a cache
+// read around a structure stays mapped.
+constexpr addr_t g_class_addr = FakeProcess::g_base_addr + 0x100;
+constexpr addr_t g_metaclass_addr = FakeProcess::g_base_addr + 0x200;
+constexpr addr_t g_superclass_addr = FakeProcess::g_base_addr + 0x300;
+constexpr addr_t g_superclass_meta_addr = FakeProcess::g_base_addr + 0x380;
+constexpr addr_t g_name_addr = FakeProcess::g_base_addr + 0x400;
+constexpr addr_t g_super_name_addr = FakeProcess::g_base_addr + 0x480;
+
+/// A well-formed class parses on every data model. This is what proves the
+/// field offsets track the target's `long` size rather than its pointer size:
+/// on Windows the instance size sits four bytes earlier than on Linux.
+TEST_P(GNUstepClassDescriptorTest, ParsesWellFormedClass) {
+ FakeProcess &process = GetProcess();
+ process.WriteCString(g_name_addr, "Derived");
+ WriteClass(g_class_addr, g_metaclass_addr, g_superclass_addr, g_name_addr,
+ g_flag_resolved, 42);
+ WriteClass(g_metaclass_addr, g_metaclass_addr, 0, g_name_addr,
+ g_flag_meta | g_flag_resolved, 0);
+
+ GNUstepObjCClassDescriptor descriptor(m_process_sp, g_class_addr);
+ ASSERT_TRUE(descriptor.IsValid());
+ EXPECT_EQ(descriptor.GetClassName(), ConstString("Derived"));
+ EXPECT_EQ(descriptor.GetInstanceSize(), 42u);
+ EXPECT_EQ(descriptor.GetISA(), g_class_addr);
+}
+
+TEST_P(GNUstepClassDescriptorTest, WalksSuperclassChain) {
+ FakeProcess &process = GetProcess();
+ process.WriteCString(g_name_addr, "Derived");
+ process.WriteCString(g_super_name_addr, "Base");
+ WriteClass(g_class_addr, g_metaclass_addr, g_superclass_addr, g_name_addr,
+ g_flag_resolved, 42);
+ WriteClass(g_metaclass_addr, g_metaclass_addr, 0, g_name_addr,
+ g_flag_meta | g_flag_resolved, 0);
+ WriteClass(g_superclass_addr, g_superclass_meta_addr, 0, g_super_name_addr,
+ g_flag_resolved, 16);
+ WriteClass(g_superclass_meta_addr, g_superclass_meta_addr, 0,
+ g_super_name_addr, g_flag_meta | g_flag_resolved, 0);
+
+ GNUstepObjCClassDescriptor descriptor(m_process_sp, g_class_addr);
+ ASSERT_TRUE(descriptor.IsValid());
+ auto superclass_sp = descriptor.GetSuperclass();
+ ASSERT_TRUE(superclass_sp);
+ EXPECT_EQ(superclass_sp->GetClassName(), ConstString("Base"));
+ EXPECT_EQ(superclass_sp->GetInstanceSize(), 16u);
+}
+
+/// Before the runtime resolves a class, `super_class` still holds a name
+/// pointer and `instance_size` the negated size of only this class's ivars,
+/// so neither may be reported.
+TEST_P(GNUstepClassDescriptorTest, UnresolvedClassHidesSuperclassAndSize) {
+ FakeProcess &process = GetProcess();
+ process.WriteCString(g_name_addr, "Derived");
+ WriteClass(g_class_addr, g_metaclass_addr, g_superclass_addr, g_name_addr,
+ /*info=*/0, -8);
+ WriteClass(g_metaclass_addr, g_metaclass_addr, 0, g_name_addr, g_flag_meta,
+ 0);
+
+ GNUstepObjCClassDescriptor descriptor(m_process_sp, g_class_addr);
+ ASSERT_TRUE(descriptor.IsValid());
+ EXPECT_EQ(descriptor.GetClassName(), ConstString("Derived"));
+ EXPECT_EQ(descriptor.GetInstanceSize(), 0u);
+ EXPECT_FALSE(descriptor.GetSuperclass());
+}
+
+TEST_P(GNUstepClassDescriptorTest, RejectsUnmappedAddress) {
+ GNUstepObjCClassDescriptor descriptor(m_process_sp, 0xdead0000);
+ EXPECT_FALSE(descriptor.IsValid());
+}
+
+TEST_P(GNUstepClassDescriptorTest, RejectsMisalignedAddress) {
+ GNUstepObjCClassDescriptor descriptor(m_process_sp, g_class_addr + 1);
+ EXPECT_FALSE(descriptor.IsValid());
+}
+
+TEST_P(GNUstepClassDescriptorTest, RejectsNullNamePointer) {
+ WriteClass(g_class_addr, g_metaclass_addr, g_superclass_addr, /*name=*/0,
+ g_flag_resolved, 42);
+ GNUstepObjCClassDescriptor descriptor(m_process_sp, g_class_addr);
+ EXPECT_FALSE(descriptor.IsValid());
+}
+
+/// A name that never terminates is not a class name, and must not drag in
+/// unbounded amounts of inferior memory.
+TEST_P(GNUstepClassDescriptorTest, RejectsUnterminatedName) {
+ FakeProcess &process = GetProcess();
+ process.Fill(g_name_addr, 'A', 0x800);
+ WriteClass(g_class_addr, g_metaclass_addr, g_superclass_addr, g_name_addr,
+ g_flag_resolved, 42);
+ WriteClass(g_metaclass_addr, g_metaclass_addr, 0, g_name_addr,
+ g_flag_meta | g_flag_resolved, 0);
+
+ GNUstepObjCClassDescriptor descriptor(m_process_sp, g_class_addr);
+ EXPECT_FALSE(descriptor.IsValid());
+}
+
+/// Arbitrary readable memory must not be accepted as a class: a class and its
+/// metaclass have to disagree about the meta flag.
+TEST_P(GNUstepClassDescriptorTest, RejectsClassWhoseMetaclassIsNotMeta) {
+ FakeProcess &process = GetProcess();
+ process.WriteCString(g_name_addr, "NotAClass");
+ WriteClass(g_class_addr, g_metaclass_addr, g_superclass_addr, g_name_addr,
+ g_flag_resolved, 42);
+ // The "metaclass" is missing the meta flag.
+ WriteClass(g_metaclass_addr, g_metaclass_addr, 0, g_name_addr,
+ g_flag_resolved, 0);
+
+ GNUstepObjCClassDescriptor descriptor(m_process_sp, g_class_addr);
+ EXPECT_FALSE(descriptor.IsValid());
+}
+
+TEST_P(GNUstepClassDescriptorTest, RejectsNullMetaclass) {
+ FakeProcess &process = GetProcess();
+ process.WriteCString(g_name_addr, "Derived");
+ WriteClass(g_class_addr, /*metaclass=*/0, g_superclass_addr, g_name_addr,
+ g_flag_resolved, 42);
+ GNUstepObjCClassDescriptor descriptor(m_process_sp, g_class_addr);
+ EXPECT_FALSE(descriptor.IsValid());
+}
+
+/// Tagged pointer payloads are shifted by the tag width, and a signed payload
+/// has to be sign-extended from the target's pointer width.
+TEST_P(GNUstepClassDescriptorTest, DecodesTaggedPointerPayload) {
+ FakeProcess &process = GetProcess();
+ process.WriteCString(g_name_addr, "NSSmallInt");
+ WriteClass(g_class_addr, g_metaclass_addr, 0, g_name_addr, g_flag_resolved,
+ 0);
+ WriteClass(g_metaclass_addr, g_metaclass_addr, 0, g_name_addr,
+ g_flag_meta | g_flag_resolved, 0);
+
+ const bool is_64_bit = PointerSize() == 8;
+ const uint64_t tag = is_64_bit ? 3 : 1;
+ const uint32_t shift = is_64_bit ? 3 : 1;
+ const uint64_t pointer_mask =
+ is_64_bit ? UINT64_MAX : std::numeric_limits<uint32_t>::max();
+
+ // A positive payload of 42.
+ const addr_t positive = ((42ULL << shift) | tag) & pointer_mask;
+ GNUstepObjCTaggedPointerClassDescriptor positive_descriptor(
+ m_process_sp, g_class_addr, positive, tag, shift, PointerSize());
+ ASSERT_TRUE(positive_descriptor.IsValid());
+ uint64_t info_bits = 0;
+ uint64_t value_bits = 0;
+ ASSERT_TRUE(
+ positive_descriptor.GetTaggedPointerInfo(&info_bits, &value_bits));
+ EXPECT_EQ(info_bits, tag);
+ EXPECT_EQ(value_bits, 42u);
+
+ // A negative payload of -42 encoded in the target's pointer width.
+ const addr_t negative =
+ ((static_cast<uint64_t>(-42LL) << shift) | tag) & pointer_mask;
+ GNUstepObjCTaggedPointerClassDescriptor negative_descriptor(
+ m_process_sp, g_class_addr, negative, tag, shift, PointerSize());
+ int64_t signed_value = 0;
+ ASSERT_TRUE(negative_descriptor.GetTaggedPointerInfoSigned(&info_bits,
+ &signed_value));
+ EXPECT_EQ(info_bits, tag);
+ EXPECT_EQ(signed_value, -42);
+}
+
+INSTANTIATE_TEST_SUITE_P(
+ DataModels, GNUstepClassDescriptorTest,
+ ::testing::Values(DataModel{"x86_64-pc-linux", 8, 8},
+ // Windows is LLP64: pointers are 64 bits but long stays
+ // 32, moving every field after the class name.
+ DataModel{"x86_64-pc-windows-msvc", 8, 4},
+ DataModel{"i386-pc-linux", 4, 4}),
+ [](const ::testing::TestParamInfo<DataModel> &info) {
+ std::string name = info.param.triple;
+ for (char &c : name)
+ if (!std::isalnum(static_cast<unsigned char>(c)))
+ c = '_';
+ return name;
+ });
+
+} // namespace
>From be96c66073d9400e6ac19f521475034521fc2f6e Mon Sep 17 00:00:00 2001
From: Rob <robk at robk.dev>
Date: Wed, 12 Aug 2026 10:30:22 +0100
Subject: [PATCH 11/49] [lldb][GNUstep] Formatting and include cleanup
Apply clang-format, drop the DeclVendor include left behind when the
decl vendor fallback was removed, include Symbol/Type.h for the type
query it uses rather than relying on it arriving transitively, and fix
a misspelled selector in the expression test.
Assisted-by: Claude Opus 5
---
.../GNUstepObjCClassDescriptor.cpp | 4 ++--
.../GNUstepObjCRuntime/GNUstepObjCRuntime.cpp | 24 ++++++++++---------
...tepThreadPlanStepThroughObjCTrampoline.cpp | 3 +--
lldb/test/Shell/Expr/objc-gnustep-expr.m | 14 +++++------
4 files changed, 23 insertions(+), 22 deletions(-)
diff --git a/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCClassDescriptor.cpp b/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCClassDescriptor.cpp
index f310bff13aa51..3a00f1abb3c4a 100644
--- a/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCClassDescriptor.cpp
+++ b/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCClassDescriptor.cpp
@@ -126,8 +126,8 @@ void GNUstepObjCClassDescriptor::Read() {
m_isa + layout.instance_size_offset, layout.long_size, 0, error);
if (error.Fail())
return;
- m_instance_size = static_cast<uint64_t>(
- instance_size < 0 ? -instance_size : instance_size);
+ m_instance_size = static_cast<uint64_t>(instance_size < 0 ? -instance_size
+ : instance_size);
m_superclass_isa = superclass;
}
diff --git a/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCRuntime.cpp b/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCRuntime.cpp
index 4d6078589ccab..2ed3c807bf8f6 100644
--- a/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCRuntime.cpp
+++ b/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCRuntime.cpp
@@ -20,9 +20,9 @@
#include "lldb/Expression/DiagnosticManager.h"
#include "lldb/Expression/FunctionCaller.h"
#include "lldb/Expression/UtilityFunction.h"
-#include "lldb/Symbol/DeclVendor.h"
#include "lldb/Symbol/Symbol.h"
#include "lldb/Symbol/SymbolContext.h"
+#include "lldb/Symbol/Type.h"
#include "lldb/Target/ABI.h"
#include "lldb/Target/ExecutionContext.h"
#include "lldb/Target/Process.h"
@@ -115,9 +115,10 @@ class GNUstepObjCSelectorRegistrationPass : public llvm::ModulePass {
if (!inst) {
// A constant expression has no instruction to anchor the call to;
// such a selector stays unregistered.
- LLDB_LOG(GetLog(LLDBLog::Expressions),
- "not registering selector used by a constant expression: {0}",
- gv->getName());
+ LLDB_LOG(
+ GetLog(LLDBLog::Expressions),
+ "not registering selector used by a constant expression: {0}",
+ gv->getName());
continue;
}
llvm::Function *func = inst->getFunction();
@@ -454,8 +455,8 @@ bool GNUstepObjCRuntime::CalculateHasNewLiteralsAndIndexing() {
const ModuleList &images = GetTargetRef().GetImages();
for (llvm::StringRef class_name : g_required_classes) {
SymbolContextList sc_list;
- images.FindSymbolsWithNameAndType(ConstString(prefix.str() + class_name.str()),
- eSymbolTypeAny, sc_list);
+ images.FindSymbolsWithNameAndType(
+ ConstString(prefix.str() + class_name.str()), eSymbolTypeAny, sc_list);
if (sc_list.GetSize() == 0)
return false;
}
@@ -596,7 +597,8 @@ GNUstepObjCRuntime::GetStepThroughTrampolinePlan(Thread &thread,
if (!(m_tagged_pointer_vendor_up &&
m_tagged_pointer_vendor_up->IsPossibleTaggedPointer(receiver))) {
Status error;
- const addr_t isa_candidate = process->ReadPointerFromMemory(receiver, error);
+ const addr_t isa_candidate =
+ process->ReadPointerFromMemory(receiver, error);
if (error.Success())
isa = isa_candidate;
}
@@ -679,7 +681,8 @@ FunctionCaller *GNUstepObjCRuntime::GetMsgLookupFunctionCaller(Thread &thread) {
static const char *g_lookup_name = "$__lldb_gnustep_objc_msg_lookup";
static const char *g_lookup_code =
"void *objc_msg_lookup(void *receiver, void *selector);\n"
- "void *$__lldb_gnustep_objc_msg_lookup(void *receiver, void *selector) {\n"
+ "void *$__lldb_gnustep_objc_msg_lookup(void *receiver, void *selector) "
+ "{\n"
" return objc_msg_lookup(receiver, selector);\n"
"}\n";
@@ -722,9 +725,8 @@ FunctionCaller *GNUstepObjCRuntime::GetMsgLookupFunctionCaller(Thread &thread) {
args.PushValue(void_ptr_value);
Status error;
- m_msg_lookup_caller =
- m_msg_lookup_utility_up->MakeFunctionCaller(void_ptr_type, args,
- thread_sp, error);
+ m_msg_lookup_caller = m_msg_lookup_utility_up->MakeFunctionCaller(
+ void_ptr_type, args, thread_sp, error);
if (error.Fail()) {
LLDB_LOG(log, "failed to make objc_msg_lookup caller: {0}",
error.AsCString());
diff --git a/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepThreadPlanStepThroughObjCTrampoline.cpp b/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepThreadPlanStepThroughObjCTrampoline.cpp
index 19a0ca8217437..0aa5df0e40825 100644
--- a/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepThreadPlanStepThroughObjCTrampoline.cpp
+++ b/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepThreadPlanStepThroughObjCTrampoline.cpp
@@ -43,8 +43,7 @@ void GNUstepThreadPlanStepThroughObjCTrampoline::DidPush() {
// Setting up the called function might require allocations in the
// inferior, i.e. a nested function call. This needs to be done as a
// PreResumeAction.
- m_process.AddPreResumeAction(PreResumeInitializeFunctionCaller,
- (void *)this);
+ m_process.AddPreResumeAction(PreResumeInitializeFunctionCaller, (void *)this);
}
void GNUstepThreadPlanStepThroughObjCTrampoline::DidPop() {
diff --git a/lldb/test/Shell/Expr/objc-gnustep-expr.m b/lldb/test/Shell/Expr/objc-gnustep-expr.m
index bc20422d817bc..8a511d57395e1 100644
--- a/lldb/test/Shell/Expr/objc-gnustep-expr.m
+++ b/lldb/test/Shell/Expr/objc-gnustep-expr.m
@@ -25,10 +25,10 @@ + (id)new {
@end
@interface Calc : NSObject
-- (int)addFourtyTwoTo:(int)value;
+- (int)addFortyTwoTo:(int)value;
@end
@implementation Calc
-- (int)addFourtyTwoTo:(int)value {
+- (int)addFortyTwoTo:(int)value {
return value + 42;
}
@end
@@ -38,17 +38,17 @@ - (int)addFourtyTwoTo:(int)value {
// without it the dispatch reaches the runtime with an unregistered selector.
//
// RUN: %lldb -b -o "b objc-gnustep-expr.m:47" -o "run" \
-// RUN: -o "expr [c addFourtyTwoTo:100]" \
-// RUN: -o "expr (int)[[Calc new] addFourtyTwoTo:1]" -- %t | FileCheck %s
+// RUN: -o "expr [c addFortyTwoTo:100]" \
+// RUN: -o "expr (int)[[Calc new] addFortyTwoTo:1]" -- %t | FileCheck %s
//
int main() {
Calc *c = [Calc new];
- (void)[c addFourtyTwoTo:0];
+ (void)[c addFortyTwoTo:0];
return 0;
}
//
-// CHECK: (lldb) expr [c addFourtyTwoTo:100]
+// CHECK: (lldb) expr [c addFortyTwoTo:100]
// CHECK: (int) {{\$[0-9]+}} = 142
//
-// CHECK: addFourtyTwoTo:1]
+// CHECK: addFortyTwoTo:1]
// CHECK: (int) {{\$[0-9]+}} = 43
>From 8d2bb2733f488c37beee4fa4a391cabea63518b1 Mon Sep 17 00:00:00 2001
From: Rob <robk at robk.dev>
Date: Wed, 12 Aug 2026 10:34:16 +0100
Subject: [PATCH 12/49] [lldb][test] Allow API tests to build against a GNUstep
libobjc2 runtime
The Shell tests can already be pointed at a GNUstep libobjc2
installation through LLDB_TEST_OBJC_GNUSTEP, but the API test suite had
no equivalent, so Objective-C behaviour could only be tested there on
Darwin.
Thread the configured directory through to dotest the same way a custom
libc++ is handled, and have Makefile.rules build Objective-C sources
against it, using the same flags as the Shell test helper. Tests opt in
with a new "objc-gnustep" category, which is skipped unless a runtime
directory was given, so nothing changes for builds that do not configure
one. The existing Darwin-only "objc" category is untouched.
Add a first test under lang/objc-gnustep covering dynamic type
resolution through both the SB API and the command interpreter.
Assisted-by: Claude Opus 5
---
.../Python/lldbsuite/test/builders/builder.py | 6 +++
.../Python/lldbsuite/test/configuration.py | 4 ++
lldb/packages/Python/lldbsuite/test/dotest.py | 14 ++++++
.../Python/lldbsuite/test/dotest_args.py | 8 ++++
.../Python/lldbsuite/test/make/Makefile.rules | 16 +++++++
.../Python/lldbsuite/test/test_categories.py | 1 +
lldb/test/API/lang/objc-gnustep/categories | 1 +
.../lang/objc-gnustep/dynamic-value/Makefile | 3 ++
.../dynamic-value/TestGNUstepDynamicValue.py | 47 +++++++++++++++++++
.../lang/objc-gnustep/dynamic-value/main.m | 36 ++++++++++++++
lldb/test/API/lit.cfg.py | 5 ++
lldb/test/API/lit.site.cfg.py.in | 1 +
12 files changed, 142 insertions(+)
create mode 100644 lldb/test/API/lang/objc-gnustep/categories
create mode 100644 lldb/test/API/lang/objc-gnustep/dynamic-value/Makefile
create mode 100644 lldb/test/API/lang/objc-gnustep/dynamic-value/TestGNUstepDynamicValue.py
create mode 100644 lldb/test/API/lang/objc-gnustep/dynamic-value/main.m
diff --git a/lldb/packages/Python/lldbsuite/test/builders/builder.py b/lldb/packages/Python/lldbsuite/test/builders/builder.py
index 47ef61030fa16..3ed463ae76049 100644
--- a/lldb/packages/Python/lldbsuite/test/builders/builder.py
+++ b/lldb/packages/Python/lldbsuite/test/builders/builder.py
@@ -243,6 +243,11 @@ def getLibCxxArgs(self):
return libcpp_args
return []
+ def getObjcGnustepArgs(self):
+ if configuration.objc_gnustep_dir:
+ return ["OBJC_GNUSTEP_DIR={}".format(configuration.objc_gnustep_dir)]
+ return []
+
def getLLDBObjRoot(self):
if configuration.lldb_obj_root:
return [f"LLDB_OBJ_ROOT={configuration.lldb_obj_root}"]
@@ -303,6 +308,7 @@ def getBuildCommand(
self.getExtraMakeArgs(),
self.getModuleCacheSpec(),
self.getLibCxxArgs(),
+ self.getObjcGnustepArgs(),
self.getLLDBObjRoot(),
self.getResourceDirArgs(),
self.getCmdLine(dictionary),
diff --git a/lldb/packages/Python/lldbsuite/test/configuration.py b/lldb/packages/Python/lldbsuite/test/configuration.py
index af069adf9c69e..2c7a4f5f12afe 100644
--- a/lldb/packages/Python/lldbsuite/test/configuration.py
+++ b/lldb/packages/Python/lldbsuite/test/configuration.py
@@ -148,6 +148,10 @@
libcxx_include_target_dir = None
libcxx_library_dir = None
+# GNUstep libobjc2 installation directory used to build Objective-C tests on
+# non-Apple platforms.
+objc_gnustep_dir = None
+
# A plugin whose tests will be enabled, like intel-pt.
enabled_plugins = []
diff --git a/lldb/packages/Python/lldbsuite/test/dotest.py b/lldb/packages/Python/lldbsuite/test/dotest.py
index dec46c7715e40..3e4279dc9d2ac 100644
--- a/lldb/packages/Python/lldbsuite/test/dotest.py
+++ b/lldb/packages/Python/lldbsuite/test/dotest.py
@@ -291,6 +291,9 @@ def parseOptionsAndInitTestdirs():
logging.warning("No valid FileCheck executable; some tests may fail...")
logging.warning("(Double-check the --llvm-tools-dir argument to dotest.py)")
+ if args.objc_gnustep_dir:
+ configuration.objc_gnustep_dir = args.objc_gnustep_dir
+
if args.libcxx_include_dir or args.libcxx_library_dir:
if args.lldb_platform_name:
logging.warning(
@@ -1018,6 +1021,16 @@ def checkObjcSupport():
configuration.skip_categories.append("objc")
+def checkObjcGnustepSupport():
+ """The GNUstep libobjc2 runtime is not part of any platform's SDK, so its
+ tests only run when a build of it has been pointed at."""
+ if not configuration.objc_gnustep_dir:
+ if configuration.verbose:
+ print("objc-gnustep tests will be skipped because no GNUstep")
+ print("libobjc2 installation was specified")
+ configuration.skip_categories.append("objc-gnustep")
+
+
def checkExpressionSupport():
from lldbsuite.test import lldbplatformutil
@@ -1217,6 +1230,7 @@ def run_suite():
checkDebugInfoSupport()
checkDebugServerSupport()
checkObjcSupport()
+ checkObjcGnustepSupport()
checkExpressionSupport()
checkForkVForkSupport()
checkPexpectSupport()
diff --git a/lldb/packages/Python/lldbsuite/test/dotest_args.py b/lldb/packages/Python/lldbsuite/test/dotest_args.py
index 516559fb6268d..b3dbd1c378bb2 100644
--- a/lldb/packages/Python/lldbsuite/test/dotest_args.py
+++ b/lldb/packages/Python/lldbsuite/test/dotest_args.py
@@ -85,6 +85,14 @@ def create_parser():
"Specify the path to a custom libc++ library directory. Must be used in conjunction with --libcxx-include-dir."
),
)
+ group.add_argument(
+ "--objc-gnustep-dir",
+ metavar="dir",
+ dest="objc_gnustep_dir",
+ help=textwrap.dedent(
+ "Specify the path to a GNUstep libobjc2 installation to build Objective-C tests against on non-Apple platforms."
+ ),
+ )
# FIXME? This won't work for different extra flags according to each triple.
group.add_argument(
"-E",
diff --git a/lldb/packages/Python/lldbsuite/test/make/Makefile.rules b/lldb/packages/Python/lldbsuite/test/make/Makefile.rules
index feb0f3aa36856..387ce0d21cda9 100644
--- a/lldb/packages/Python/lldbsuite/test/make/Makefile.rules
+++ b/lldb/packages/Python/lldbsuite/test/make/Makefile.rules
@@ -546,6 +546,22 @@ ifneq "$(strip $(OBJCXX_SOURCES))" ""
endif
endif
+#----------------------------------------------------------------------
+# Build Objective-C sources against a GNUstep libobjc2 installation when one
+# has been configured, which is how Objective-C tests run on platforms with
+# no system runtime. Mirrors the flags the Shell test helper uses.
+#----------------------------------------------------------------------
+ifneq "$(strip $(OBJC_GNUSTEP_DIR))" ""
+ ifneq "$(strip $(OBJC_SOURCES)$(OBJCXX_SOURCES))" ""
+ OBJCFLAGS +=-fobjc-runtime=gnustep-2.0 -I$(OBJC_GNUSTEP_DIR)/include
+ CFLAGS +=-fobjc-runtime=gnustep-2.0 -I$(OBJC_GNUSTEP_DIR)/include
+ LDFLAGS +=-L$(OBJC_GNUSTEP_DIR)/lib
+ ifeq "$(OS)" "Linux"
+ LDFLAGS +=-Wl,-rpath,$(OBJC_GNUSTEP_DIR)/lib
+ endif
+ endif
+endif
+
ifeq ($(CC_TYPE), clang)
CXXFLAGS += --driver-mode=g++
endif
diff --git a/lldb/packages/Python/lldbsuite/test/test_categories.py b/lldb/packages/Python/lldbsuite/test/test_categories.py
index b8a764fb3349a..efc55d4284e24 100644
--- a/lldb/packages/Python/lldbsuite/test/test_categories.py
+++ b/lldb/packages/Python/lldbsuite/test/test_categories.py
@@ -43,6 +43,7 @@
"pdb": "Tests that can be run with PDB debug information",
"pexpect": "Tests requiring the pexpect library to be available",
"objc": "Tests related to the Objective-C programming language support",
+ "objc-gnustep": "Tests requiring the GNUstep libobjc2 Objective-C runtime",
"pyapi": "Tests related to the Python API",
"std-module": "Tests related to importing the std module",
"stresstest": "Tests related to stressing lldb limits",
diff --git a/lldb/test/API/lang/objc-gnustep/categories b/lldb/test/API/lang/objc-gnustep/categories
new file mode 100644
index 0000000000000..3ef065a2dd25d
--- /dev/null
+++ b/lldb/test/API/lang/objc-gnustep/categories
@@ -0,0 +1 @@
+objc-gnustep
diff --git a/lldb/test/API/lang/objc-gnustep/dynamic-value/Makefile b/lldb/test/API/lang/objc-gnustep/dynamic-value/Makefile
new file mode 100644
index 0000000000000..845553d5e3f2f
--- /dev/null
+++ b/lldb/test/API/lang/objc-gnustep/dynamic-value/Makefile
@@ -0,0 +1,3 @@
+OBJC_SOURCES := main.m
+
+include Makefile.rules
diff --git a/lldb/test/API/lang/objc-gnustep/dynamic-value/TestGNUstepDynamicValue.py b/lldb/test/API/lang/objc-gnustep/dynamic-value/TestGNUstepDynamicValue.py
new file mode 100644
index 0000000000000..ae06f0c661ad0
--- /dev/null
+++ b/lldb/test/API/lang/objc-gnustep/dynamic-value/TestGNUstepDynamicValue.py
@@ -0,0 +1,47 @@
+"""
+Test the dynamic type of an Objective-C object with the GNUstep runtime.
+"""
+
+import lldb
+from lldbsuite.test.decorators import *
+from lldbsuite.test.lldbtest import *
+from lldbsuite.test import lldbutil
+
+
+class TestGNUstepDynamicValue(TestBase):
+ def test_dynamic_value_from_api(self):
+ """The dynamic type is read out of the runtime's class structures."""
+ self.build()
+ lldbutil.run_to_source_breakpoint(
+ self, "// break here", lldb.SBFileSpec("main.m")
+ )
+
+ frame = self.frame()
+ static_value = frame.FindVariable("object", lldb.eNoDynamicValues)
+ self.assertTrue(static_value.IsValid(), "found the variable")
+ self.assertEqual(static_value.GetTypeName(), "Base *")
+
+ dynamic_value = static_value.GetDynamicValue(lldb.eDynamicCanRunTarget)
+ self.assertTrue(dynamic_value.IsValid(), "resolved a dynamic value")
+ self.assertEqual(dynamic_value.GetTypeName(), "Derived *")
+
+ # A variable whose dynamic and static types agree stays unchanged.
+ base = frame.FindVariable("base", lldb.eNoDynamicValues).GetDynamicValue(
+ lldb.eDynamicCanRunTarget
+ )
+ self.assertEqual(base.GetTypeName(), "Base *")
+
+ def test_dynamic_value_from_command(self):
+ """`frame variable` reports the same dynamic type as the API."""
+ self.build()
+ lldbutil.run_to_source_breakpoint(
+ self, "// break here", lldb.SBFileSpec("main.m")
+ )
+
+ self.expect(
+ "frame variable -d run-target object", substrs=["(Derived *) object"]
+ )
+ self.expect(
+ "frame variable -d no-dynamic-values object",
+ substrs=["(Base *) object"],
+ )
diff --git a/lldb/test/API/lang/objc-gnustep/dynamic-value/main.m b/lldb/test/API/lang/objc-gnustep/dynamic-value/main.m
new file mode 100644
index 0000000000000..4b2b7bce0722c
--- /dev/null
+++ b/lldb/test/API/lang/objc-gnustep/dynamic-value/main.m
@@ -0,0 +1,36 @@
+#import "objc/runtime.h"
+
+ at protocol NSCoding
+ at end
+
+#ifdef __has_attribute
+#if __has_attribute(objc_root_class)
+__attribute__((objc_root_class))
+#endif
+#endif
+ at interface NSObject <NSCoding> {
+ id isa;
+ int refcount;
+}
+ at end
+ at implementation NSObject
++ (id)new {
+ return class_createInstance(self, 0);
+}
+ at end
+
+ at interface Base : NSObject
+ at end
+ at implementation Base
+ at end
+
+ at interface Derived : Base
+ at end
+ at implementation Derived
+ at end
+
+int main() {
+ Base *object = [Derived new];
+ Base *base = [Base new];
+ return object != base; // break here
+}
diff --git a/lldb/test/API/lit.cfg.py b/lldb/test/API/lit.cfg.py
index 41603a1e6f5af..3f32433a3beec 100644
--- a/lldb/test/API/lit.cfg.py
+++ b/lldb/test/API/lit.cfg.py
@@ -237,6 +237,11 @@ def delete_module_cache(path):
]
dotest_cmd += ["--libcxx-library-dir", config.libcxx_libs_dir]
+# If a GNUstep libobjc2 installation is available, build Objective-C tests
+# against it on non-Apple platforms.
+if is_configured("objc_gnustep_dir"):
+ dotest_cmd += ["--objc-gnustep-dir", config.objc_gnustep_dir]
+
# Forward ASan-specific environment variables to tests, as a test may load an
# ASan-ified dylib.
for env_var in ("ASAN_OPTIONS", "DYLD_INSERT_LIBRARIES"):
diff --git a/lldb/test/API/lit.site.cfg.py.in b/lldb/test/API/lit.site.cfg.py.in
index 44c62414f8bdd..30bb3733121c3 100644
--- a/lldb/test/API/lit.site.cfg.py.in
+++ b/lldb/test/API/lit.site.cfg.py.in
@@ -42,6 +42,7 @@ config.has_libcxx = @LLDB_HAS_LIBCXX@
config.libcxx_libs_dir = "@LIBCXX_LIBRARY_DIR@"
config.libcxx_include_dir = "@LIBCXX_GENERATED_INCLUDE_DIR@"
config.libcxx_include_target_dir = "@LIBCXX_GENERATED_INCLUDE_TARGET_DIR@"
+config.objc_gnustep_dir = "@LLDB_TEST_OBJC_GNUSTEP_DIR@"
config.lldb_launcher = "@LLDB_LAUNCHER@"
config.test_resource_dir = "@LLDB_TEST_RESOURCE_DIR@"
config.lldb_enable_mte = @LLDB_ENABLE_MTE@
>From 45573a39dd98170c3bedaa0d057a926a50e0d8e3 Mon Sep 17 00:00:00 2001
From: Rob <robk at robk.dev>
Date: Wed, 12 Aug 2026 10:50:42 +0100
Subject: [PATCH 13/49] [lldb][GNUstep] Add tests for tagged pointers and
stepping
Cover two runtime features that had no test: resolving the class of a
tagged pointer, and stepping through the runtime's dispatch function
into a method implementation.
The tagged pointer test registers its own small object class rather than
relying on Foundation, so it runs against a bare libobjc2, and also
checks that an ordinary pointer is still resolved by reading its class
structure. The stepping test covers both landing in the implementation
and a message to nil, which dispatches nowhere and must simply carry on.
Assisted-by: Claude Opus 5
---
lldb/test/Shell/Expr/objc-gnustep-stepping.m | 62 +++++++++++++++++++
.../Shell/Expr/objc-gnustep-tagged-pointers.m | 60 ++++++++++++++++++
2 files changed, 122 insertions(+)
create mode 100644 lldb/test/Shell/Expr/objc-gnustep-stepping.m
create mode 100644 lldb/test/Shell/Expr/objc-gnustep-tagged-pointers.m
diff --git a/lldb/test/Shell/Expr/objc-gnustep-stepping.m b/lldb/test/Shell/Expr/objc-gnustep-stepping.m
new file mode 100644
index 0000000000000..0a35f1945ded5
--- /dev/null
+++ b/lldb/test/Shell/Expr/objc-gnustep-stepping.m
@@ -0,0 +1,62 @@
+// REQUIRES: objc-gnustep
+// XFAIL: system-windows
+//
+// RUN: %build %s --compiler=clang --objc-gnustep --output=%t
+
+#import "objc/runtime.h"
+
+ at protocol NSCoding
+ at end
+
+#ifdef __has_attribute
+#if __has_attribute(objc_root_class)
+__attribute__((objc_root_class))
+#endif
+#endif
+ at interface NSObject <NSCoding> {
+ id isa;
+ int refcount;
+}
+ at end
+ at implementation NSObject
++ (id)new {
+ return class_createInstance(self, 0);
+}
+ at end
+
+ at interface Doubler : NSObject
+- (int)twice:(int)value;
+ at end
+ at implementation Doubler
+- (int)twice:(int)value {
+ return value * 2;
+}
+ at end
+
+// Stepping at a message send has to run through the runtime's dispatch
+// function and land in the method implementation.
+//
+// RUN: %lldb -b -o "b objc-gnustep-stepping.m:50" -o "run" -o "step" \
+// RUN: -- %t | FileCheck %s --check-prefix=STEP_IN
+//
+// A message to nil dispatches nowhere, so the step must simply move on
+// instead of trying to run to an implementation.
+//
+// RUN: %lldb -b -o "b objc-gnustep-stepping.m:52" -o "run" -o "step" \
+// RUN: -- %t | FileCheck %s --check-prefix=STEP_OVER_NIL
+//
+int main() {
+ Doubler *doubler = [Doubler new];
+ int value = [doubler twice:21];
+ Doubler *nothing = (Doubler *)0;
+ int none = [nothing twice:1];
+ return value + none;
+}
+//
+// STEP_IN: (lldb) step
+// STEP_IN: stop reason = step in
+// STEP_IN: -[Doubler twice:]
+//
+// STEP_OVER_NIL: (lldb) step
+// STEP_OVER_NIL: stop reason = step in
+// STEP_OVER_NIL: main at objc-gnustep-stepping.m:53
diff --git a/lldb/test/Shell/Expr/objc-gnustep-tagged-pointers.m b/lldb/test/Shell/Expr/objc-gnustep-tagged-pointers.m
new file mode 100644
index 0000000000000..fc7d1103ff310
--- /dev/null
+++ b/lldb/test/Shell/Expr/objc-gnustep-tagged-pointers.m
@@ -0,0 +1,60 @@
+// REQUIRES: objc-gnustep
+// XFAIL: system-windows
+//
+// RUN: %build %s --compiler=clang --objc-gnustep --output=%t
+
+#import "objc/runtime.h"
+
+ at protocol NSCoding
+ at end
+
+#ifdef __has_attribute
+#if __has_attribute(objc_root_class)
+__attribute__((objc_root_class))
+#endif
+#endif
+ at interface NSObject <NSCoding> {
+ id isa;
+ int refcount;
+}
+ at end
+ at implementation NSObject
++ (id)new {
+ return class_createInstance(self, 0);
+}
+ at end
+
+// libobjc2 calls these "small objects": a pointer with any of its low bits set
+// holds a value inline rather than pointing at an object, and its class comes
+// from the runtime's table of registered small object classes.
+ at interface TinyNumber : NSObject
+ at end
+ at implementation TinyNumber
+ at end
+
+ at interface Ordinary : NSObject
+ at end
+ at implementation Ordinary
+ at end
+
+// RUN: %lldb -b -o "b objc-gnustep-tagged-pointers.m:50" -o "run" \
+// RUN: -o "frame variable -d run-target tagged" \
+// RUN: -o "frame variable -d run-target ordinary" -- %t | FileCheck %s
+//
+int main() {
+ objc_registerSmallObjectClass_np(objc_getClass("TinyNumber"), 1);
+
+ // A payload of 3 in the slot registered above.
+ id tagged = (id)(uintptr_t)((3 << 3) | 1);
+ id ordinary = [Ordinary new];
+ return tagged == ordinary;
+}
+//
+// The tagged value's class comes from the runtime's small object table, while
+// an ordinary pointer is still resolved by reading its class structure.
+//
+// CHECK: (lldb) frame variable -d run-target tagged
+// CHECK: (TinyNumber *) tagged = 0x{{0*}}19
+//
+// CHECK: (lldb) frame variable -d run-target ordinary
+// CHECK: (Ordinary *) ordinary = 0x
>From 1b0f51449c719422ca232c6b6e07239ae4e268c4 Mon Sep 17 00:00:00 2001
From: Rob <robk at robk.dev>
Date: Wed, 12 Aug 2026 10:56:59 +0100
Subject: [PATCH 14/49] [lldb][docs] Document testing Objective-C against a
GNUstep runtime
LLDB_TEST_OBJC_GNUSTEP and LLDB_TEST_OBJC_GNUSTEP_DIR were not mentioned
anywhere, so there was no way to discover how to run the Objective-C
tests on a platform without a system runtime. Describe them alongside
the other test-related CMake options, note the new API test category,
and add a release note for the GNUstep runtime support.
Assisted-by: Claude Opus 5
---
lldb/docs/resources/build.md | 17 +++++++++++++++++
lldb/docs/resources/test.md | 7 +++++++
llvm/docs/ReleaseNotes.md | 5 +++++
3 files changed, 29 insertions(+)
diff --git a/lldb/docs/resources/build.md b/lldb/docs/resources/build.md
index e3c3250006051..d01586a0f18d7 100644
--- a/lldb/docs/resources/build.md
+++ b/lldb/docs/resources/build.md
@@ -287,6 +287,23 @@ When both of these options are enabled, LLDB can use, and be used from, a
different version of Python (3.8 or later) than it was built against. Note that
on Windows, `LLDB_ENABLE_DYNAMIC_SCRIPTINTERPRETERS` is not required.
+#### Testing Objective-C without a system runtime
+
+Linux and Windows have no Objective-C runtime of their own, so the tests for
+Objective-C language support are only run when LLDB is pointed at a build of
+the GNUstep [libobjc2](https://github.com/gnustep/libobjc2) runtime:
+
+```
+-DLLDB_TEST_OBJC_GNUSTEP=On
+-DLLDB_TEST_OBJC_GNUSTEP_DIR=/path/to/libobjc2/install
+```
+
+The directory is the install prefix of libobjc2, containing `lib` and
+`include`. With these set, the Shell tests that require the `objc-gnustep`
+feature and the API tests in the `objc-gnustep` category are enabled; without
+them those tests are skipped. Foundation is not required: the tests run
+against libobjc2 alone.
+
#### Windows
On Windows the LLDB test suite requires lld. Either add `lld` to
diff --git a/lldb/docs/resources/test.md b/lldb/docs/resources/test.md
index 5097b972db9f6..c38f474d187e0 100644
--- a/lldb/docs/resources/test.md
+++ b/lldb/docs/resources/test.md
@@ -166,6 +166,13 @@ Reach for `require*` when the test is tied to a platform-specific file format,
API, or OS feature. If the test is merely untested or broken somewhere, keep
`skipIf*` so nobody mistakes a bug for a design decision.
+Some tests instead depend on something the build was pointed at rather than on
+the platform. Objective-C tests are an example: on Linux and Windows there is
+no system runtime, so tests that need one go in the `objc-gnustep` category
+(by adding a `categories` file next to them) and only run when the build was
+configured with `LLDB_TEST_OBJC_GNUSTEP_DIR`. The Darwin-only `objc` category
+is unaffected.
+
In addition to providing a lot more flexibility when it comes to writing the
test, the API test also allow for much more complex scenarios when it comes to
building inferiors. Every test has its own `Makefile`, most of them only a
diff --git a/llvm/docs/ReleaseNotes.md b/llvm/docs/ReleaseNotes.md
index 2468c7ed01000..fdf03987f81f5 100644
--- a/llvm/docs/ReleaseNotes.md
+++ b/llvm/docs/ReleaseNotes.md
@@ -144,6 +144,11 @@ Makes programs 10x faster by doing Special New Thing.
### Changes to LLDB
+* Debugging Objective-C with the GNUstep libobjc2 runtime is now supported on
+ Linux and Windows: dynamic types, tagged pointers, printing objects with
+ `po`, stepping through message dispatch, evaluating expressions that send
+ messages, and data formatters for the gnustep-base Foundation classes.
+
#### SBAPI
* A [bug](https://github.com/llvm/llvm-project/issues/211787) involving SBValues
>From 15e6b607d07e16d4d8a52ca89ef2033a810a39a8 Mon Sep 17 00:00:00 2001
From: Rob <robk at robk.dev>
Date: Wed, 12 Aug 2026 23:37:16 +0100
Subject: [PATCH 15/49] [lldb] Guard MS inheritance model against
non-CXXRecordDecl DWARF types
CompleteRecordType() computes the MSInheritanceAttr for the Microsoft
C++ ABI by calling calculateInheritanceModel() on the record's
CXXRecordDecl. Objective-C interface types complete through the same
path but are ObjCInterfaceDecls, so GetAsCXXRecordDecl() returns null
and the Microsoft-ABI block crashed on every Objective-C type
completion for *-windows-msvc targets. Guard it the same way as the
SetRecordLayout call above.
Found debugging GNUstep Objective-C programs on Windows, where any
`frame variable` touching an object type crashed LLDB.
Assisted-by: Claude Fable 5
---
.../Plugins/SymbolFile/DWARF/DWARFASTParserClang.cpp | 10 ++++++++--
1 file changed, 8 insertions(+), 2 deletions(-)
diff --git a/lldb/source/Plugins/SymbolFile/DWARF/DWARFASTParserClang.cpp b/lldb/source/Plugins/SymbolFile/DWARF/DWARFASTParserClang.cpp
index b60f1d9e41958..8609e0b2b0388 100644
--- a/lldb/source/Plugins/SymbolFile/DWARF/DWARFASTParserClang.cpp
+++ b/lldb/source/Plugins/SymbolFile/DWARF/DWARFASTParserClang.cpp
@@ -2239,8 +2239,14 @@ bool DWARFASTParserClang::CompleteRecordType(const DWARFDIE &die,
clang::CXXRecordDecl *record_decl =
m_ast.GetAsCXXRecordDecl(clang_type.GetOpaqueQualType());
- if (record_decl)
- GetClangASTImporter().SetRecordLayout(record_decl, layout_info);
+ // Objective-C interfaces are completed through this path as well, but are
+ // not CXXRecordDecls. Nothing that follows applies to them: they have no
+ // record layout to hand to the importer, no pointer-to-member
+ // representation to infer, and no nested types to resolve.
+ if (!record_decl)
+ return clang_type.IsValid();
+
+ GetClangASTImporter().SetRecordLayout(record_decl, layout_info);
// DWARF doesn't have the attribute, but we can infer the value the same way
// as Clang Sema does. It's required to calculate the size of pointers to
>From 92fb85972b323bb2de94798cf726a7fed469e622 Mon Sep 17 00:00:00 2001
From: Rob <robk at robk.dev>
Date: Wed, 12 Aug 2026 23:37:31 +0100
Subject: [PATCH 16/49] [lldb][GNUstep] Ignore PE import thunks when locating
the runtime
ModuleDefinesFunction() treats any __objc_load symbol with a valid
address as a definition. On PE/COFF every module linked against
libobjc2 contains an import thunk for __objc_load that carries the
plain symbol name and a valid code address, so the executable itself
was identified as the runtime module. The step-through gate then looked
for objc_msg_lookup in the executable and refused to create a plan,
which broke stepping through message sends on Windows, and
IsRuntimeInternalAddress() classified every executable address as
runtime-internal.
Only the importing module also has the IAT pointer symbol
`__imp_<name>`; the module that implements the function does not. Use
that to reject importers.
Assisted-by: Claude Fable 5
---
.../GNUstepObjCRuntime/GNUstepObjCRuntime.cpp | 21 ++++++++++++++++---
1 file changed, 18 insertions(+), 3 deletions(-)
diff --git a/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCRuntime.cpp b/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCRuntime.cpp
index 2ed3c807bf8f6..0fe4d82cb29d2 100644
--- a/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCRuntime.cpp
+++ b/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCRuntime.cpp
@@ -176,14 +176,29 @@ static bool ModuleDefinesFunction(const ModuleSP &module_sp,
SymbolContextList sc_list;
module_sp->FindSymbolsWithNameAndType(ConstString(name), eSymbolTypeCode,
sc_list);
+ bool defines_function = false;
for (const SymbolContext &sc : sc_list) {
// Every module compiled against libobjc2 carries an undefined reference
// to __objc_load from its .objc_init constructor, so only a definition
// identifies the runtime itself.
- if (sc.symbol && sc.symbol->GetAddress().IsValid())
- return true;
+ if (sc.symbol && sc.symbol->GetAddress().IsValid()) {
+ defines_function = true;
+ break;
+ }
}
- return false;
+ if (!defines_function)
+ return false;
+ // On PE/COFF an importing module contains an import thunk that carries the
+ // imported function's plain name and a valid code address, which the check
+ // above cannot tell apart from a definition. Only the importer also has the
+ // IAT pointer symbol `__imp_<name>`; the implementing module does not.
+ SymbolContextList imp_list;
+ module_sp->FindSymbolsWithNameAndType(ConstString(("__imp_" + name).str()),
+ eSymbolTypeAny, imp_list);
+ for (const SymbolContext &sc : imp_list)
+ if (sc.symbol && sc.symbol->GetAddress().IsValid())
+ return false;
+ return true;
}
/// Finds the module implementing the libobjc2 runtime, identified by its
>From d5747082ccaed4b643d240186de351d77dff914f Mon Sep 17 00:00:00 2001
From: Rob <robk at robk.dev>
Date: Wed, 12 Aug 2026 23:37:32 +0100
Subject: [PATCH 17/49] [lldb][GNUstep] Build Windows GNUstep tests with DWARF
and un-XFAIL them
CodeView cannot represent Objective-C types, so test binaries built
with -gcodeview present plain C++ record types to the debugger: dynamic
type resolution silently falls back to the static type, ivar values
read from wrong offsets, and expression evaluation rejects message
sends. Compile the Shell and API test binaries with -gdwarf instead and
link with lld-link /debug:dwarf, which keeps the DWARF sections and
also writes a COFF symbol table - required for LLDB to see the runtime
metadata symbols ($_OBJC_CLASS_..., selector references) that the
executable image otherwise loses.
The API harness additionally copies objc.dll next to the test binary,
because lldbtest launches inferiors with the shared-library search path
scrubbed (it clears PATH on Windows).
With this, all five objc-gnustep Shell tests and the dynamic-value API
test pass on Windows against libobjc2 v2.3, so drop the XFAILs. The
in-file breakpoint line numbers shift by one for the removed XFAIL
lines.
Assisted-by: Claude Fable 5
---
.../Python/lldbsuite/test/make/Makefile.rules | 18 ++++++++++++++++++
.../Shell/Expr/objc-gnustep-dynamic-types.m | 3 +--
lldb/test/Shell/Expr/objc-gnustep-expr.m | 3 +--
lldb/test/Shell/Expr/objc-gnustep-print.m | 11 +++++------
lldb/test/Shell/Expr/objc-gnustep-stepping.m | 7 +++----
.../Shell/Expr/objc-gnustep-tagged-pointers.m | 3 +--
lldb/test/Shell/helper/build.py | 14 ++++++++------
7 files changed, 37 insertions(+), 22 deletions(-)
diff --git a/lldb/packages/Python/lldbsuite/test/make/Makefile.rules b/lldb/packages/Python/lldbsuite/test/make/Makefile.rules
index 387ce0d21cda9..33c2b0788f87d 100644
--- a/lldb/packages/Python/lldbsuite/test/make/Makefile.rules
+++ b/lldb/packages/Python/lldbsuite/test/make/Makefile.rules
@@ -559,6 +559,12 @@ ifneq "$(strip $(OBJC_GNUSTEP_DIR))" ""
ifeq "$(OS)" "Linux"
LDFLAGS +=-Wl,-rpath,$(OBJC_GNUSTEP_DIR)/lib
endif
+ ifeq "$(OS)" "Windows_NT"
+ # Keep the DWARF sections and emit a COFF symbol table; the
+ # default CodeView route cannot represent Objective-C types.
+ LDFLAGS +=-Wl,/debug:dwarf
+ GNUSTEP_NEEDS_DLL_COPY := 1
+ endif
endif
endif
@@ -745,6 +751,18 @@ print-%:
@echo ' flavor = $(flavor $*)'
@echo ' value = $(value $*)'
+# The test harness launches inferiors with the shared-library search path
+# scrubbed (lldbtest clears PATH on Windows), so the runtime DLL must sit
+# next to the test binary. This lives at the end of the file because make
+# would otherwise fold the tab-indented lines of the next conditional block
+# into this rule's recipe.
+ifeq "$(GNUSTEP_NEEDS_DLL_COPY)" "1"
+all: objc.dll
+
+objc.dll: $(OBJC_GNUSTEP_DIR)/lib/objc.dll
+ cp $< $@
+endif
+
### Local Variables: ###
### mode:makefile ###
### End: ###
diff --git a/lldb/test/Shell/Expr/objc-gnustep-dynamic-types.m b/lldb/test/Shell/Expr/objc-gnustep-dynamic-types.m
index 2d0caf1c69426..3da7aeb01ad06 100644
--- a/lldb/test/Shell/Expr/objc-gnustep-dynamic-types.m
+++ b/lldb/test/Shell/Expr/objc-gnustep-dynamic-types.m
@@ -1,5 +1,4 @@
// REQUIRES: objc-gnustep
-// XFAIL: system-windows
//
// RUN: %build %s --compiler=clang --objc-gnustep --output=%t
@@ -38,7 +37,7 @@ @implementation Derived
// GNUstep runtime resolves the dynamic type by reading the class structure
// from the inferior's memory and attaching the matching type from debug info.
//
-// RUN: %lldb -b -o "b objc-gnustep-dynamic-types.m:47" -o "run" \
+// RUN: %lldb -b -o "b objc-gnustep-dynamic-types.m:46" -o "run" \
// RUN: -o "frame variable -d run-target object" \
// RUN: -o "frame variable -d no-dynamic-values object" -- %t | FileCheck %s
//
diff --git a/lldb/test/Shell/Expr/objc-gnustep-expr.m b/lldb/test/Shell/Expr/objc-gnustep-expr.m
index 8a511d57395e1..3605bebc9cbe2 100644
--- a/lldb/test/Shell/Expr/objc-gnustep-expr.m
+++ b/lldb/test/Shell/Expr/objc-gnustep-expr.m
@@ -1,5 +1,4 @@
// REQUIRES: objc-gnustep
-// XFAIL: system-windows
//
// RUN: %build %s --compiler=clang --objc-gnustep --output=%t
@@ -37,7 +36,7 @@ - (int)addFortyTwoTo:(int)value {
// registered with the runtime (the GNUstep plugin's IR pass does this);
// without it the dispatch reaches the runtime with an unregistered selector.
//
-// RUN: %lldb -b -o "b objc-gnustep-expr.m:47" -o "run" \
+// RUN: %lldb -b -o "b objc-gnustep-expr.m:46" -o "run" \
// RUN: -o "expr [c addFortyTwoTo:100]" \
// RUN: -o "expr (int)[[Calc new] addFortyTwoTo:1]" -- %t | FileCheck %s
//
diff --git a/lldb/test/Shell/Expr/objc-gnustep-print.m b/lldb/test/Shell/Expr/objc-gnustep-print.m
index 6e119cefc459b..873ac3092df1f 100644
--- a/lldb/test/Shell/Expr/objc-gnustep-print.m
+++ b/lldb/test/Shell/Expr/objc-gnustep-print.m
@@ -1,5 +1,4 @@
// REQUIRES: objc-gnustep
-// XFAIL: system-windows
//
// RUN: %build %s --compiler=clang --objc-gnustep --output=%t
@@ -52,9 +51,9 @@ - (void)set_ivars {
}
@end
-// RUN: %lldb -b -o "b objc-gnustep-print.m:43" -o "run" -o "p self" -o "p *self" -- %t | FileCheck %s --check-prefix=SELF
+// RUN: %lldb -b -o "b objc-gnustep-print.m:42" -o "run" -o "p self" -o "p *self" -- %t | FileCheck %s --check-prefix=SELF
//
-// SELF: (lldb) b objc-gnustep-print.m:43
+// SELF: (lldb) b objc-gnustep-print.m:42
// SELF: Breakpoint {{.*}} at objc-gnustep-print.m
//
// SELF: (lldb) run
@@ -78,7 +77,7 @@ - (void)set_ivars {
// SELF: _id_objc = nil
// SELF: }
-// RUN: %lldb -b -o "b objc-gnustep-print.m:106" -o "run" -o "p t->_int" -o "p t->_float" -o "p t->_char" \
+// RUN: %lldb -b -o "b objc-gnustep-print.m:105" -o "run" -o "p t->_int" -o "p t->_float" -o "p t->_char" \
// RUN: -o "p t->_ptr_void" -o "p t->_ptr_nsobject" -o "p t->_id_objc" -- %t | FileCheck %s --check-prefix=IVARS_SET
//
// IVARS_SET: (lldb) p t->_int
@@ -115,7 +114,7 @@ int main() {
return object_getClassName(object);
}
-// RUN: %lldb -b -o "b objc-gnustep-print.m:106" -o "run" -o "po t" \
+// RUN: %lldb -b -o "b objc-gnustep-print.m:105" -o "run" -o "po t" \
// RUN: -- %t | FileCheck %s --check-prefix=PO
//
// PO: (lldb) po t
@@ -124,7 +123,7 @@ int main() {
// Stepping at a message send goes through the objc_msgSend trampoline into
// the method implementation.
//
-// RUN: %lldb -b -o "b objc-gnustep-print.m:104" -o "run" -o "step" \
+// RUN: %lldb -b -o "b objc-gnustep-print.m:103" -o "run" -o "step" \
// RUN: -- %t | FileCheck %s --check-prefix=STEP
//
// STEP: (lldb) step
diff --git a/lldb/test/Shell/Expr/objc-gnustep-stepping.m b/lldb/test/Shell/Expr/objc-gnustep-stepping.m
index 0a35f1945ded5..d7f0278d46044 100644
--- a/lldb/test/Shell/Expr/objc-gnustep-stepping.m
+++ b/lldb/test/Shell/Expr/objc-gnustep-stepping.m
@@ -1,5 +1,4 @@
// REQUIRES: objc-gnustep
-// XFAIL: system-windows
//
// RUN: %build %s --compiler=clang --objc-gnustep --output=%t
@@ -36,13 +35,13 @@ - (int)twice:(int)value {
// Stepping at a message send has to run through the runtime's dispatch
// function and land in the method implementation.
//
-// RUN: %lldb -b -o "b objc-gnustep-stepping.m:50" -o "run" -o "step" \
+// RUN: %lldb -b -o "b objc-gnustep-stepping.m:49" -o "run" -o "step" \
// RUN: -- %t | FileCheck %s --check-prefix=STEP_IN
//
// A message to nil dispatches nowhere, so the step must simply move on
// instead of trying to run to an implementation.
//
-// RUN: %lldb -b -o "b objc-gnustep-stepping.m:52" -o "run" -o "step" \
+// RUN: %lldb -b -o "b objc-gnustep-stepping.m:51" -o "run" -o "step" \
// RUN: -- %t | FileCheck %s --check-prefix=STEP_OVER_NIL
//
int main() {
@@ -59,4 +58,4 @@ int main() {
//
// STEP_OVER_NIL: (lldb) step
// STEP_OVER_NIL: stop reason = step in
-// STEP_OVER_NIL: main at objc-gnustep-stepping.m:53
+// STEP_OVER_NIL: main at objc-gnustep-stepping.m:52
diff --git a/lldb/test/Shell/Expr/objc-gnustep-tagged-pointers.m b/lldb/test/Shell/Expr/objc-gnustep-tagged-pointers.m
index fc7d1103ff310..55744570a2dcd 100644
--- a/lldb/test/Shell/Expr/objc-gnustep-tagged-pointers.m
+++ b/lldb/test/Shell/Expr/objc-gnustep-tagged-pointers.m
@@ -1,5 +1,4 @@
// REQUIRES: objc-gnustep
-// XFAIL: system-windows
//
// RUN: %build %s --compiler=clang --objc-gnustep --output=%t
@@ -37,7 +36,7 @@ @interface Ordinary : NSObject
@implementation Ordinary
@end
-// RUN: %lldb -b -o "b objc-gnustep-tagged-pointers.m:50" -o "run" \
+// RUN: %lldb -b -o "b objc-gnustep-tagged-pointers.m:49" -o "run" \
// RUN: -o "frame variable -d run-target tagged" \
// RUN: -o "frame variable -d run-target ordinary" -- %t | FileCheck %s
//
diff --git a/lldb/test/Shell/helper/build.py b/lldb/test/Shell/helper/build.py
index fb88c1f2f44c8..d09b28c9bc17b 100755
--- a/lldb/test/Shell/helper/build.py
+++ b/lldb/test/Shell/helper/build.py
@@ -782,9 +782,10 @@ def _get_compilation_command(self, source, obj):
if source.endswith(".m") or source.endswith(".mm"):
args.extend(["-fobjc-runtime=gnustep-2.0", "-I", self.objc_gnustep_inc])
if sys.platform == "win32":
- args.extend(
- ["-Xclang", "-gcodeview", "-Xclang", "--dependent-lib=msvcrtd"]
- )
+ # CodeView cannot represent Objective-C types, so force
+ # DWARF even though the target is MSVC. The debugger needs
+ # it to recognize classes and resolve dynamic types.
+ args.extend(["-gdwarf", "-Xclang", "--dependent-lib=msvcrtd"])
elif self.sysroot:
args.extend(["--sysroot", self.sysroot])
@@ -832,9 +833,10 @@ def _get_link_command(self):
if sys.platform == "linux":
args.extend(["-Wl,-rpath," + self.objc_gnustep_lib])
elif sys.platform == "win32":
- args.extend(
- ["-fuse-ld=lld-link", "-g", "-Xclang", "--dependent-lib=msvcrtd"]
- )
+ # /debug:dwarf keeps the DWARF sections in the image and, unlike
+ # the PDB route, writes a COFF symbol table, which the debugger
+ # needs to find the runtime metadata symbols ($_OBJC_CLASS_...).
+ args.extend(["-fuse-ld=lld-link", "-Wl,/debug:dwarf"])
elif self.sysroot:
args.extend(["--sysroot", self.sysroot])
>From d4274d83b598c71cfb057eec2e3614be85da8fd2 Mon Sep 17 00:00:00 2001
From: Rob <robk at robk.dev>
Date: Sun, 16 Aug 2026 01:04:05 +0100
Subject: [PATCH 18/49] [lldb][GNUstep] Do not give class objects a dynamic
type
A root class may declare its isa ivar as `id` rather than `Class`, which
libobjc2 programs and the GNUstep tests do. `id` values are offered to
the runtime for dynamic typing, so GetDynamicTypeAndAddress ran on the
class object an isa points at, read its first word - the metaclass - and
built a descriptor from that. libobjc2 names a metaclass after its class,
so the class object was reported as an instance of its own class:
`isa` rendered as `(Derived *)`, its "ivars" were struct objc_class
fields, and expanding it recursed forever through the same isa. Apple's
runtime never sees this because Apple's NSObject declares `Class isa`
and Class is not dynamic-typeable.
Record whether a descriptor was built from a metaclass and refuse to
report a dynamic type in that case; such a value is a Class, not an
object, and keeps its static type.
Assisted-by: Claude Fable 5
---
.../GNUstepObjCClassDescriptor.cpp | 1 +
.../GNUstepObjCClassDescriptor.h | 7 ++
.../GNUstepObjCRuntime/GNUstepObjCRuntime.cpp | 12 ++++
.../Shell/Expr/objc-gnustep-class-objects.m | 72 +++++++++++++++++++
.../GNUstepObjCClassDescriptorTest.cpp | 22 ++++++
5 files changed, 114 insertions(+)
create mode 100644 lldb/test/Shell/Expr/objc-gnustep-class-objects.m
diff --git a/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCClassDescriptor.cpp b/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCClassDescriptor.cpp
index 3a00f1abb3c4a..1261aa347ab41 100644
--- a/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCClassDescriptor.cpp
+++ b/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCClassDescriptor.cpp
@@ -132,6 +132,7 @@ void GNUstepObjCClassDescriptor::Read() {
}
m_metaclass_isa = metaclass;
+ m_is_meta = is_meta;
m_name = ConstString(name_buffer);
m_valid = true;
}
diff --git a/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCClassDescriptor.h b/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCClassDescriptor.h
index e1b2ee240d931..25ef25c22400b 100644
--- a/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCClassDescriptor.h
+++ b/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCClassDescriptor.h
@@ -74,6 +74,12 @@ class GNUstepObjCClassDescriptor : public ObjCLanguageRuntime::ClassDescriptor {
ObjCLanguageRuntime::ObjCISA GetISA() override { return m_isa; }
+ /// True if this descriptor describes a metaclass, i.e. the ISA it was
+ /// built from is itself the class pointer of a class object rather than of
+ /// an instance. Instances never have a metaclass as their ISA, so a value
+ /// that resolves to one is a Class, not an object.
+ bool IsMetaclass() const { return m_is_meta; }
+
protected:
/// Parse `struct objc_class` at m_isa. Called from the constructor; sets
/// m_valid only if the structure passes the consistency checks that keep a
@@ -86,6 +92,7 @@ class GNUstepObjCClassDescriptor : public ObjCLanguageRuntime::ClassDescriptor {
ObjCLanguageRuntime::ObjCISA m_superclass_isa = 0;
ObjCLanguageRuntime::ObjCISA m_metaclass_isa = 0;
uint64_t m_instance_size = 0;
+ bool m_is_meta = false;
bool m_valid = false;
};
diff --git a/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCRuntime.cpp b/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCRuntime.cpp
index 0fe4d82cb29d2..fc5a937603cc3 100644
--- a/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCRuntime.cpp
+++ b/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCRuntime.cpp
@@ -411,6 +411,18 @@ bool GNUstepObjCRuntime::GetDynamicTypeAndAddress(
if (!objc_class_sp)
return false;
+ // The descriptor was built from the first word of the pointed-to memory.
+ // For an instance that word is its class; for a class object it is the
+ // metaclass, which libobjc2 gives the same name as the class. Reporting
+ // that name here would present the class object as an instance of itself
+ // (and, since a root class may declare `id isa`, recurse through it), so
+ // values that turn out to be Class have no dynamic type. Every descriptor
+ // this runtime creates derives from GNUstepObjCClassDescriptor, so the
+ // cast is safe.
+ if (static_cast<GNUstepObjCClassDescriptor *>(objc_class_sp.get())
+ ->IsMetaclass())
+ return false;
+
ConstString class_name(objc_class_sp->GetClassName());
if (!class_name)
return false;
diff --git a/lldb/test/Shell/Expr/objc-gnustep-class-objects.m b/lldb/test/Shell/Expr/objc-gnustep-class-objects.m
new file mode 100644
index 0000000000000..05c135b72bc4a
--- /dev/null
+++ b/lldb/test/Shell/Expr/objc-gnustep-class-objects.m
@@ -0,0 +1,72 @@
+// REQUIRES: objc-gnustep
+//
+// RUN: %build %s --compiler=clang --objc-gnustep --output=%t
+
+#import "objc/runtime.h"
+
+ at protocol NSCoding
+ at end
+
+// A root class may declare its `isa` as `id` rather than `Class` (the GNUstep
+// tests and many libobjc2 programs do). Because `id` can carry a dynamic
+// type, the value of such a field - a class object - is offered to the
+// runtime for dynamic typing. libobjc2 names a metaclass after its class, so
+// a naive runtime would then report the class object as an instance of the
+// class, and expanding it would recurse forever through the same `isa`.
+#ifdef __has_attribute
+#if __has_attribute(objc_root_class)
+__attribute__((objc_root_class))
+#endif
+#endif
+ at interface NSObject <NSCoding> {
+ id isa;
+ int refcount;
+}
+ at end
+ at implementation NSObject
++ (id)new {
+ return class_createInstance(self, 0);
+}
+ at end
+
+ at interface Base : NSObject {
+ int base_ivar;
+}
+ at end
+ at implementation Base
+ at end
+
+ at interface Derived : Base {
+ int derived_ivar;
+}
+ at end
+ at implementation Derived
+ at end
+
+// RUN: %lldb -b -o "b objc-gnustep-class-objects.m:54" -o "run" \
+// RUN: -o "frame variable -d run-target -T object" \
+// RUN: -o "frame variable -d run-target -T *object" \
+// RUN: -o "frame variable -d run-target -T object->isa" \
+// RUN: -- %t | FileCheck %s
+//
+int main() {
+ Base *object = [Derived new];
+ (void)object;
+ return object == 0;
+}
+//
+// The object itself gets its dynamic type...
+// CHECK: (lldb) frame variable -d run-target -T object
+// CHECK: (Derived *) object = 0x
+//
+// ...and its `isa` stays a plain `id`: it points at the class object, which
+// must not be presented as an instance.
+// CHECK: (lldb) frame variable -d run-target -T *object
+// CHECK: (Derived) *object = {
+// CHECK: (id) isa = 0x
+// CHECK-NOT: (Derived *) isa
+// CHECK-NOT: (Base *) isa
+// CHECK: (int) refcount
+//
+// CHECK: (lldb) frame variable -d run-target -T object->isa
+// CHECK: (id) object->isa = 0x
diff --git a/lldb/unittests/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCClassDescriptorTest.cpp b/lldb/unittests/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCClassDescriptorTest.cpp
index 5d06a61a9acb2..bc3ace1cc96f7 100644
--- a/lldb/unittests/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCClassDescriptorTest.cpp
+++ b/lldb/unittests/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCClassDescriptorTest.cpp
@@ -183,6 +183,28 @@ TEST_P(GNUstepClassDescriptorTest, ParsesWellFormedClass) {
EXPECT_EQ(descriptor.GetISA(), g_class_addr);
}
+/// A descriptor built from a class object's own ISA (its metaclass) must say
+/// so. GetDynamicTypeAndAddress relies on this to refuse a dynamic type for
+/// values that are Class rather than instances: libobjc2 gives a metaclass the
+/// same name as its class, so nothing else distinguishes the two.
+TEST_P(GNUstepClassDescriptorTest, IdentifiesMetaclass) {
+ FakeProcess &process = GetProcess();
+ process.WriteCString(g_name_addr, "Derived");
+ WriteClass(g_class_addr, g_metaclass_addr, g_superclass_addr, g_name_addr,
+ g_flag_resolved, 42);
+ WriteClass(g_metaclass_addr, g_metaclass_addr, 0, g_name_addr,
+ g_flag_meta | g_flag_resolved, 0);
+
+ GNUstepObjCClassDescriptor instance_class(m_process_sp, g_class_addr);
+ ASSERT_TRUE(instance_class.IsValid());
+ EXPECT_FALSE(instance_class.IsMetaclass());
+
+ GNUstepObjCClassDescriptor metaclass(m_process_sp, g_metaclass_addr);
+ ASSERT_TRUE(metaclass.IsValid());
+ EXPECT_TRUE(metaclass.IsMetaclass());
+ EXPECT_EQ(metaclass.GetClassName(), ConstString("Derived"));
+}
+
TEST_P(GNUstepClassDescriptorTest, WalksSuperclassChain) {
FakeProcess &process = GetProcess();
process.WriteCString(g_name_addr, "Derived");
>From d8f4896366d23c9e6d28da9ec743fe3f5cf8fed3 Mon Sep 17 00:00:00 2001
From: Rob <robk at robk.dev>
Date: Sun, 16 Aug 2026 01:25:04 +0100
Subject: [PATCH 19/49] [lldb][GNUstep] Add data formatters for gnustep-base
Foundation classes
The Objective-C data formatters recognize only Apple's Foundation: they
dispatch on class names like __NSCFString and read CoreFoundation
layouts, and the container synthetic children bail out for any runtime
that is not AppleObjCRuntime. Programs using the GNUstep libobjc2
runtime with gnustep-base therefore showed only pointer values.
Add summary providers and synthetic children for gnustep-base's
concrete classes - strings (tagged GSTinyString, NSConstantString, the
GSString family, GSMutableString), numbers (tagged and boxed), arrays,
dictionaries, sets, data, dates and NSNull - producing the same output
as the Apple formatters (@"...", @"N elements", N key/value pairs,
(int)N, YES/NO). They are registered under the concrete class names, so
ObjCLanguage's runtime-name candidate selects them for dynamic values;
for static values the Apple providers hand over to them when the process
runs the GNUstep runtime.
Two rules keep the formatters correct across configurations. Nothing
runs code in the inferior: small objects are decoded from the pointer
bits alone (GSString.m, NSNumber.m, NSDate.m, and clang's CGObjCGNU.cpp
which emits short literals as tagged strings), and everything else is
read from memory. No ivar offset is hardcoded: libobjc2 packs instance
sizes and long-typed ivars differ between LP64 and LLP64, so ivars are
found by name through the debug info of the value's dynamic type, the
way the libc++ formatters read their members. GSIMap tables are walked
through their typed nodes for the same reason, which also handles sets
whose nodes carry no value member.
Not covered: NSURL, whose ivars gnustep-base hides behind GS_EXPOSE and
which therefore appear in neither debug info nor ivar-offset symbols.
Assisted-by: Claude Fable 5
---
.../Plugins/Language/ObjC/CMakeLists.txt | 6 +
lldb/source/Plugins/Language/ObjC/Cocoa.cpp | 13 +
.../Language/ObjC/GNUstepFormatters.cpp | 290 ++++++++++++++++++
.../Plugins/Language/ObjC/GNUstepFormatters.h | 104 +++++++
.../Plugins/Language/ObjC/GNUstepNSArray.cpp | 130 ++++++++
.../Language/ObjC/GNUstepNSDictionary.cpp | 264 ++++++++++++++++
.../Plugins/Language/ObjC/GNUstepNSNumber.cpp | 126 ++++++++
.../Plugins/Language/ObjC/GNUstepNSString.cpp | 239 +++++++++++++++
lldb/source/Plugins/Language/ObjC/NSArray.cpp | 12 +-
.../Plugins/Language/ObjC/NSDictionary.cpp | 14 +-
.../Plugins/Language/ObjC/NSDictionary.h | 6 +
lldb/source/Plugins/Language/ObjC/NSSet.cpp | 9 +
.../source/Plugins/Language/ObjC/NSString.cpp | 5 +
.../Plugins/Language/ObjC/ObjCLanguage.cpp | 2 +
14 files changed, 1215 insertions(+), 5 deletions(-)
create mode 100644 lldb/source/Plugins/Language/ObjC/GNUstepFormatters.cpp
create mode 100644 lldb/source/Plugins/Language/ObjC/GNUstepFormatters.h
create mode 100644 lldb/source/Plugins/Language/ObjC/GNUstepNSArray.cpp
create mode 100644 lldb/source/Plugins/Language/ObjC/GNUstepNSDictionary.cpp
create mode 100644 lldb/source/Plugins/Language/ObjC/GNUstepNSNumber.cpp
create mode 100644 lldb/source/Plugins/Language/ObjC/GNUstepNSString.cpp
diff --git a/lldb/source/Plugins/Language/ObjC/CMakeLists.txt b/lldb/source/Plugins/Language/ObjC/CMakeLists.txt
index 5e554b22030fa..c2be0216daaa3 100644
--- a/lldb/source/Plugins/Language/ObjC/CMakeLists.txt
+++ b/lldb/source/Plugins/Language/ObjC/CMakeLists.txt
@@ -4,6 +4,11 @@ add_lldb_library(lldbPluginObjCLanguage PLUGIN
CFBasicHash.cpp
Cocoa.cpp
CoreMedia.cpp
+ GNUstepFormatters.cpp
+ GNUstepNSArray.cpp
+ GNUstepNSDictionary.cpp
+ GNUstepNSNumber.cpp
+ GNUstepNSString.cpp
NSArray.cpp
NSDictionary.cpp
NSError.cpp
@@ -22,6 +27,7 @@ add_lldb_library(lldbPluginObjCLanguage PLUGIN
lldbUtility
lldbValueObject
lldbPluginAppleObjCRuntime
+ lldbPluginGNUstepObjCRuntime
lldbPluginTypeSystemClang
CLANG_LIBS
clangAST
diff --git a/lldb/source/Plugins/Language/ObjC/Cocoa.cpp b/lldb/source/Plugins/Language/ObjC/Cocoa.cpp
index 4a8d6f1ea75ce..69f5c96351b70 100644
--- a/lldb/source/Plugins/Language/ObjC/Cocoa.cpp
+++ b/lldb/source/Plugins/Language/ObjC/Cocoa.cpp
@@ -7,6 +7,7 @@
//===----------------------------------------------------------------------===//
#include "Cocoa.h"
+#include "GNUstepFormatters.h"
#include "NSString.h"
#include "ObjCConstants.h"
@@ -445,6 +446,10 @@ bool lldb_private::formatters::NSNumberSummaryProvider(
if (!runtime)
return false;
+ // gnustep-base lays its classes out differently and names them
+ // differently; hand those over.
+ if (llvm::isa<GNUstepObjCRuntime>(runtime))
+ return GNUstepNSNumberSummaryProvider(valobj, stream, options);
ObjCLanguageRuntime::ClassDescriptorSP descriptor(
runtime->GetClassDescriptor(valobj));
@@ -910,6 +915,10 @@ bool lldb_private::formatters::NSDateSummaryProvider(
if (!runtime)
return false;
+ // gnustep-base lays its classes out differently and names them
+ // differently; hand those over.
+ if (llvm::isa<GNUstepObjCRuntime>(runtime))
+ return GNUstepNSDateSummaryProvider(valobj, stream, options);
ObjCLanguageRuntime::ClassDescriptorSP descriptor(
runtime->GetClassDescriptor(valobj));
@@ -1072,6 +1081,10 @@ bool lldb_private::formatters::NSDataSummaryProvider(
if (!runtime)
return false;
+ // gnustep-base lays its classes out differently and names them
+ // differently; hand those over.
+ if (llvm::isa<GNUstepObjCRuntime>(runtime))
+ return GNUstepNSDataSummaryProvider(valobj, stream, options);
ObjCLanguageRuntime::ClassDescriptorSP descriptor(
runtime->GetClassDescriptor(valobj));
diff --git a/lldb/source/Plugins/Language/ObjC/GNUstepFormatters.cpp b/lldb/source/Plugins/Language/ObjC/GNUstepFormatters.cpp
new file mode 100644
index 0000000000000..7609c6b5de8da
--- /dev/null
+++ b/lldb/source/Plugins/Language/ObjC/GNUstepFormatters.cpp
@@ -0,0 +1,290 @@
+//===-- GNUstepFormatters.cpp ---------------------------------------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+#include "GNUstepFormatters.h"
+#include "Cocoa.h"
+
+#include "Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCRuntime.h"
+#include "Plugins/LanguageRuntime/ObjC/ObjCLanguageRuntime.h"
+#include "lldb/DataFormatters/FormattersHelpers.h"
+#include "lldb/DataFormatters/TypeSummary.h"
+#include "lldb/Target/Language.h"
+#include "lldb/Target/Process.h"
+#include "lldb/Target/Target.h"
+#include "lldb/Utility/Stream.h"
+#include "lldb/Utility/StreamString.h"
+#include "llvm/ADT/APFloat.h"
+#include "llvm/Support/Error.h"
+
+#include <cmath>
+#include <cstring>
+#include <ctime>
+
+using namespace lldb;
+using namespace lldb_private;
+using namespace lldb_private::formatters;
+
+bool lldb_private::formatters::IsGNUstepObjCRuntime(ValueObject &valobj) {
+ ProcessSP process_sp = valobj.GetProcessSP();
+ if (!process_sp)
+ return false;
+ return llvm::isa_and_nonnull<GNUstepObjCRuntime>(
+ ObjCLanguageRuntime::Get(*process_sp));
+}
+
+ValueObjectSP lldb_private::formatters::GNUstepGetIvar(ValueObject &valobj,
+ llvm::StringRef name) {
+ // Formatters usually receive the dynamic value already, but the static
+ // value arrives when a provider registered under an abstract class name
+ // (NSArray) dispatches here; the ivars live on the concrete class, so ask
+ // for the dynamic value in that case.
+ ValueObjectSP object_sp = valobj.GetSP();
+ if (!object_sp)
+ return {};
+ // A summary runs on the value the synthetic children were attached to,
+ // whose "children" are the elements; the ivars are on the value beneath.
+ if (ValueObjectSP non_synthetic_sp = object_sp->GetNonSyntheticValue())
+ object_sp = non_synthetic_sp;
+ if (ValueObjectSP dynamic_sp =
+ object_sp->GetDynamicValue(lldb::eDynamicDontRunTarget))
+ object_sp = dynamic_sp;
+ return object_sp->GetChildMemberWithName(name);
+}
+
+std::optional<double>
+lldb_private::formatters::GNUstepGetFloatValue(ValueObject &valobj) {
+ llvm::Expected<llvm::APFloat> value = valobj.GetValueAsAPFloat();
+ if (!value) {
+ llvm::consumeError(value.takeError());
+ return std::nullopt;
+ }
+ bool ignored = false;
+ llvm::APFloat as_double(*value);
+ as_double.convert(llvm::APFloat::IEEEdouble(),
+ llvm::APFloat::rmNearestTiesToEven, &ignored);
+ return as_double.convertToDouble();
+}
+
+// --- Small object decoding -------------------------------------------------
+
+std::optional<std::string>
+lldb_private::formatters::GNUstepDecodeTinyString(uint64_t ptr) {
+ if ((ptr & g_gnustep_small_object_mask) != 4)
+ return std::nullopt;
+ // struct { uintptr_t char0..char7 : 7 each; length : 5; tag : 3; }: the
+ // characters occupy the high bits, character i at bits [57-7i, 64-7i).
+ const uint64_t length = (ptr >> 3) & 0x1f;
+ // Nine means eight characters and an implicit terminator.
+ if (length > 9)
+ return std::nullopt;
+ std::string result;
+ for (uint64_t i = 0; i < length && i < 8; ++i)
+ result.push_back(static_cast<char>((ptr >> (57 - 7 * i)) & 0x7f));
+ return result;
+}
+
+int64_t lldb_private::formatters::GNUstepDecodeSmallInt(uint64_t ptr) {
+ return static_cast<int64_t>(ptr) >> 3;
+}
+
+double
+lldb_private::formatters::GNUstepDecodeSmallExtendedDouble(uint64_t ptr) {
+ // The tag displaced the low three mantissa bits, which were all equal to
+ // bit 3; restore them from it.
+ const uint64_t low_bit = ptr & 8;
+ const uint64_t bits = (ptr & ~g_gnustep_small_object_mask) | (low_bit >> 1) |
+ (low_bit >> 2) | (low_bit >> 3);
+ double value;
+ std::memcpy(&value, &bits, sizeof(value));
+ return value;
+}
+
+double
+lldb_private::formatters::GNUstepDecodeSmallRepeatingDouble(uint64_t ptr) {
+ // Bits 3-5 hold the three mantissa bits displaced by the tag.
+ const uint64_t moved = ptr & 56;
+ const uint64_t bits = (ptr & ~g_gnustep_small_object_mask) | (moved >> 3);
+ double value;
+ std::memcpy(&value, &bits, sizeof(value));
+ return value;
+}
+
+double lldb_private::formatters::GNUstepDecodeSmallDate(uint64_t ptr) {
+ // union CompressedDouble { tag:3; fraction:52; exponent:8 (signed); sign:1 }
+ // with the exponent rebased on 0x3EF (Source/NSDate.m).
+ const uint64_t fraction = (ptr >> 3) & ((1ULL << 52) - 1);
+ const int64_t exponent = static_cast<int8_t>((ptr >> 55) & 0xff);
+ const uint64_t sign = (ptr >> 63) & 1;
+ const uint64_t bits =
+ (sign << 63) | ((static_cast<uint64_t>(exponent + 0x3EF) & 0x7ff) << 52) |
+ fraction;
+ double value;
+ std::memcpy(&value, &bits, sizeof(value));
+ return value;
+}
+
+// --- Small providers -------------------------------------------------------
+
+bool lldb_private::formatters::GNUstepNSNullSummaryProvider(
+ ValueObject &valobj, Stream &stream, const TypeSummaryOptions &options) {
+ if (!IsGNUstepObjCRuntime(valobj))
+ return false;
+ stream.PutCString("<null>");
+ return true;
+}
+
+bool lldb_private::formatters::GNUstepNSDataSummaryProvider(
+ ValueObject &valobj, Stream &stream, const TypeSummaryOptions &options) {
+ if (!IsGNUstepObjCRuntime(valobj))
+ return false;
+ // NSDataStatic and its subclasses keep `NSUInteger length` (Source/NSData.m).
+ ValueObjectSP length_sp = GNUstepGetIvar(valobj, "length");
+ if (!length_sp)
+ return false;
+ bool success = false;
+ const uint64_t length = length_sp->GetValueAsUnsigned(0, &success);
+ if (!success)
+ return false;
+ stream.Printf("%" PRIu64 " byte%s", length, length == 1 ? "" : "s");
+ return true;
+}
+
+bool lldb_private::formatters::GNUstepNSDateSummaryProvider(
+ ValueObject &valobj, Stream &stream, const TypeSummaryOptions &options) {
+ // NSCalendarDate is registered by both runtimes into the shared "objc"
+ // category, and for a given type name the last registration wins. Returning
+ // false here would leave the value with no summary at all rather than
+ // falling through, so hand back to the Apple provider explicitly.
+ if (!IsGNUstepObjCRuntime(valobj))
+ return NSDateSummaryProvider(valobj, stream, options);
+ const uint64_t ptr = valobj.GetValueAsUnsigned(0);
+ double seconds_since_2001 = 0.0;
+ if ((ptr & g_gnustep_small_object_mask) == 6) {
+ seconds_since_2001 = GNUstepDecodeSmallDate(ptr);
+ } else {
+ // NSCalendarDate (and NSGDate on targets without small objects) keep the
+ // interval in _seconds_since_ref.
+ ValueObjectSP seconds_sp = GNUstepGetIvar(valobj, "_seconds_since_ref");
+ if (!seconds_sp)
+ return false;
+ std::optional<double> seconds = GNUstepGetFloatValue(*seconds_sp);
+ if (!seconds)
+ return false;
+ seconds_since_2001 = *seconds;
+ }
+ // Same rendering as the Apple NSDate summary: seconds since 2001-01-01
+ // converted through the Unix epoch, printed as UTC.
+ constexpr time_t g_seconds_from_1970_to_2001 = 978307200;
+ time_t epoch = g_seconds_from_1970_to_2001 +
+ static_cast<time_t>(std::floor(seconds_since_2001));
+ tm *tm_date = gmtime(&epoch);
+ if (!tm_date)
+ return false;
+ stream.Printf("%04d-%02d-%02d %02d:%02d:%02d UTC", tm_date->tm_year + 1900,
+ tm_date->tm_mon + 1, tm_date->tm_mday, tm_date->tm_hour,
+ tm_date->tm_min, tm_date->tm_sec);
+ return true;
+}
+
+// --- Registration ----------------------------------------------------------
+
+void lldb_private::formatters::LoadGNUstepFormatters(
+ TypeCategoryImplSP objc_category_sp) {
+ if (!objc_category_sp)
+ return;
+
+ TypeSummaryImpl::Flags summary_flags;
+ summary_flags.SetCascades(true)
+ .SetSkipPointers(false)
+ .SetSkipReferences(false)
+ .SetDontShowChildren(false)
+ .SetDontShowValue(false)
+ .SetShowMembersOneLiner(false)
+ .SetHideItemNames(false);
+
+ SyntheticChildren::Flags synth_flags;
+ synth_flags.SetCascades(true).SetSkipPointers(false).SetSkipReferences(false);
+
+ // The names below are gnustep-base's concrete classes: the runtime reports
+ // them for a value and ObjCLanguage offers them as formatter candidates.
+ // Placeholder classes are omitted on purpose; they are what +alloc returns
+ // before -init has run and carry no contents.
+
+ // Strings (Source/GSString.m, Headers/Foundation/NSString.h).
+ static constexpr const char *g_string_classes[] = {
+ "GSTinyString", "NSConstantString", "GSString",
+ "GSCString", "GSUnicodeString", "GSCInlineString",
+ "GSUInlineString", "GSCBufferString", "GSUnicodeBufferString",
+ "GSCSubString", "GSUnicodeSubString", "GSMutableString",
+ };
+ for (const char *name : g_string_classes)
+ AddCXXSummary(objc_category_sp, GNUstepNSStringSummaryProvider,
+ "GNUstep NSString summary provider", name, summary_flags);
+
+ // Numbers (Source/NSNumber.m).
+ static constexpr const char *g_number_classes[] = {
+ "NSSmallInt",
+ "NSSmallExtendedDouble",
+ "NSSmallRepeatingDouble",
+ "NSSmallFloat",
+ "NSIntNumber",
+ "NSBoolNumber",
+ "NSLongLongNumber",
+ "NSUnsignedLongLongNumber",
+ "NSFloatNumber",
+ "NSDoubleNumber",
+ };
+ for (const char *name : g_number_classes)
+ AddCXXSummary(objc_category_sp, GNUstepNSNumberSummaryProvider,
+ "GNUstep NSNumber summary provider", name, summary_flags);
+
+ // Dates (Source/NSDate.m, Headers/Foundation/NSCalendarDate.h).
+ for (const char *name : {"GSSmallDate", "NSGDate", "NSCalendarDate"})
+ AddCXXSummary(objc_category_sp, GNUstepNSDateSummaryProvider,
+ "GNUstep NSDate summary provider", name, summary_flags);
+
+ // Arrays (Source/GSArray.m).
+ for (const char *name : {"GSArray", "GSInlineArray", "GSMutableArray"}) {
+ AddCXXSummary(objc_category_sp, GNUstepNSArraySummaryProvider,
+ "GNUstep NSArray summary provider", name, summary_flags);
+ AddCXXSynthetic(objc_category_sp, GNUstepNSArraySyntheticFrontEndCreator,
+ "GNUstep NSArray synthetic children", name, synth_flags);
+ }
+
+ // Dictionaries (Source/GSDictionary.m).
+ for (const char *name :
+ {"GSDictionary", "GSMutableDictionary", "GSCachedDictionary"}) {
+ AddCXXSummary(objc_category_sp, GNUstepNSDictionarySummaryProvider,
+ "GNUstep NSDictionary summary provider", name, summary_flags);
+ AddCXXSynthetic(
+ objc_category_sp, GNUstepNSDictionarySyntheticFrontEndCreator,
+ "GNUstep NSDictionary synthetic children", name, synth_flags);
+ }
+
+ // Sets (Source/GSSet.m, Source/GSCountedSet.m).
+ for (const char *name : {"GSSet", "GSMutableSet", "GSCountedSet"}) {
+ AddCXXSummary(objc_category_sp, GNUstepNSSetSummaryProvider,
+ "GNUstep NSSet summary provider", name, summary_flags);
+ AddCXXSynthetic(objc_category_sp, GNUstepNSSetSyntheticFrontEndCreator,
+ "GNUstep NSSet synthetic children", name, synth_flags);
+ }
+
+ // Data (Source/NSData.m).
+ for (const char *name : {"NSDataStatic", "NSDataEmpty", "NSDataMalloc",
+ "NSDataWithDeallocatorBlock", "NSMutableDataMalloc",
+ "NSMutableDataWithDeallocatorBlock"})
+ AddCXXSummary(objc_category_sp, GNUstepNSDataSummaryProvider,
+ "GNUstep NSData summary provider", name, summary_flags);
+
+ AddCXXSummary(objc_category_sp, GNUstepNSNullSummaryProvider,
+ "GNUstep NSNull summary provider", "NSNull", summary_flags);
+
+ // Not covered: NSURL. gnustep-base declares its ivars behind
+ // GS_EXPOSE(NSURL), so they are in neither the debug info nor the
+ // __objc_ivar_offset symbols of a normal build; `po` still describes it.
+}
diff --git a/lldb/source/Plugins/Language/ObjC/GNUstepFormatters.h b/lldb/source/Plugins/Language/ObjC/GNUstepFormatters.h
new file mode 100644
index 0000000000000..2ceda0aca08f7
--- /dev/null
+++ b/lldb/source/Plugins/Language/ObjC/GNUstepFormatters.h
@@ -0,0 +1,104 @@
+//===-- GNUstepFormatters.h -------------------------------------*- C++ -*-===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+//
+// Data formatters for the concrete Foundation classes of gnustep-base, the
+// Foundation implementation used with the GNUstep libobjc2 runtime.
+//
+// Like the Apple formatters these never run code in the inferior. Unlike them
+// they do not hardcode ivar offsets: libobjc2 packs instance sizes and the
+// widths of `long`-typed ivars differ between LP64 and LLP64, so ivars are
+// looked up by name through the debug info attached to the value's dynamic
+// type. Small objects (libobjc2's tagged pointers) are decoded from the
+// pointer bits alone.
+//
+//===----------------------------------------------------------------------===//
+
+#ifndef LLDB_SOURCE_PLUGINS_LANGUAGE_OBJC_GNUSTEPFORMATTERS_H
+#define LLDB_SOURCE_PLUGINS_LANGUAGE_OBJC_GNUSTEPFORMATTERS_H
+
+#include "Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCRuntime.h"
+#include "lldb/DataFormatters/TypeCategory.h"
+#include "lldb/DataFormatters/TypeSummary.h"
+#include "lldb/DataFormatters/TypeSynthetic.h"
+#include "lldb/ValueObject/ValueObject.h"
+#include "lldb/lldb-forward.h"
+
+#include <optional>
+#include <string>
+
+namespace lldb_private {
+namespace formatters {
+
+/// True if the process debugging \p valobj uses the GNUstep libobjc2 runtime.
+bool IsGNUstepObjCRuntime(ValueObject &valobj);
+
+/// Registers all GNUstep formatters into the shared "objc" category, keyed
+/// by gnustep-base's concrete class names so they are picked up through the
+/// runtime-reported class name of a value.
+void LoadGNUstepFormatters(lldb::TypeCategoryImplSP objc_category_sp);
+
+/// Finds the ivar \p name of the object \p valobj points at, using the debug
+/// info of its dynamic type. Returns an empty pointer when the ivar is not
+/// visible, which the callers treat as "cannot format".
+lldb::ValueObjectSP GNUstepGetIvar(ValueObject &valobj, llvm::StringRef name);
+
+/// The value of a floating-point ValueObject as a double, or nullopt if it
+/// cannot be read as one.
+std::optional<double> GNUstepGetFloatValue(ValueObject &valobj);
+
+// Small-object (tagged pointer) decoding. The tag is the low three bits of
+// the pointer; the payload layouts come from libobjc2 and gnustep-base:
+// 1 NSSmallInt, 2 NSSmallExtendedDouble, 3 NSSmallRepeatingDouble,
+// 4 GSTinyString, 5 NSSmallFloat, 6 GSSmallDate.
+constexpr uint64_t g_gnustep_small_object_mask = 7;
+
+/// GSTinyString packs up to eight 7-bit characters and a 5-bit length into
+/// the pointer (gnustep-base Source/GSString.m, clang CGObjCGNU.cpp).
+std::optional<std::string> GNUstepDecodeTinyString(uint64_t ptr);
+/// NSSmallInt stores an arithmetically shifted integer (Source/NSNumber.m).
+int64_t GNUstepDecodeSmallInt(uint64_t ptr);
+/// NSSmallExtendedDouble / NSSmallRepeatingDouble / NSSmallFloat store a
+/// double whose low mantissa bits were displaced by the tag
+/// (unboxSmallExtendedDouble / unboxSmallRepeatingDouble in Source/NSNumber.m).
+double GNUstepDecodeSmallExtendedDouble(uint64_t ptr);
+double GNUstepDecodeSmallRepeatingDouble(uint64_t ptr);
+/// GSSmallDate stores a compressed NSTimeInterval since the 2001 reference
+/// date (decompressTimeInterval in Source/NSDate.m).
+double GNUstepDecodeSmallDate(uint64_t ptr);
+
+bool GNUstepNSStringSummaryProvider(ValueObject &valobj, Stream &stream,
+ const TypeSummaryOptions &options);
+bool GNUstepNSNumberSummaryProvider(ValueObject &valobj, Stream &stream,
+ const TypeSummaryOptions &options);
+bool GNUstepNSDateSummaryProvider(ValueObject &valobj, Stream &stream,
+ const TypeSummaryOptions &options);
+bool GNUstepNSArraySummaryProvider(ValueObject &valobj, Stream &stream,
+ const TypeSummaryOptions &options);
+bool GNUstepNSDictionarySummaryProvider(ValueObject &valobj, Stream &stream,
+ const TypeSummaryOptions &options);
+bool GNUstepNSSetSummaryProvider(ValueObject &valobj, Stream &stream,
+ const TypeSummaryOptions &options);
+bool GNUstepNSDataSummaryProvider(ValueObject &valobj, Stream &stream,
+ const TypeSummaryOptions &options);
+bool GNUstepNSNullSummaryProvider(ValueObject &valobj, Stream &stream,
+ const TypeSummaryOptions &options);
+
+SyntheticChildrenFrontEnd *
+GNUstepNSArraySyntheticFrontEndCreator(CXXSyntheticChildren *,
+ lldb::ValueObjectSP);
+SyntheticChildrenFrontEnd *
+GNUstepNSDictionarySyntheticFrontEndCreator(CXXSyntheticChildren *,
+ lldb::ValueObjectSP);
+SyntheticChildrenFrontEnd *
+GNUstepNSSetSyntheticFrontEndCreator(CXXSyntheticChildren *,
+ lldb::ValueObjectSP);
+
+} // namespace formatters
+} // namespace lldb_private
+
+#endif // LLDB_SOURCE_PLUGINS_LANGUAGE_OBJC_GNUSTEPFORMATTERS_H
diff --git a/lldb/source/Plugins/Language/ObjC/GNUstepNSArray.cpp b/lldb/source/Plugins/Language/ObjC/GNUstepNSArray.cpp
new file mode 100644
index 0000000000000..9eba287eeecb6
--- /dev/null
+++ b/lldb/source/Plugins/Language/ObjC/GNUstepNSArray.cpp
@@ -0,0 +1,130 @@
+//===-- GNUstepNSArray.cpp ------------------------------------------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+#include "GNUstepFormatters.h"
+
+#include "Plugins/TypeSystem/Clang/TypeSystemClang.h"
+#include "lldb/DataFormatters/FormattersHelpers.h"
+#include "lldb/DataFormatters/TypeSummary.h"
+#include "lldb/DataFormatters/TypeSynthetic.h"
+#include "lldb/Target/Language.h"
+#include "lldb/Target/Process.h"
+#include "lldb/Target/Target.h"
+#include "lldb/Utility/Stream.h"
+#include "lldb/ValueObject/ValueObject.h"
+
+using namespace lldb;
+using namespace lldb_private;
+using namespace lldb_private::formatters;
+
+namespace {
+
+/// GSArray, GSInlineArray and GSMutableArray all start with
+/// `id *_contents_array; unsigned _count;` (Source/GSPrivate.h). Where the
+/// element buffer lives (a separate allocation, or right after the instance
+/// for GSInlineArray) does not matter: _contents_array is always the absolute
+/// address of element 0.
+struct ArrayContents {
+ addr_t elements = LLDB_INVALID_ADDRESS;
+ uint64_t count = 0;
+};
+
+std::optional<ArrayContents> ReadArray(ValueObject &valobj) {
+ ValueObjectSP contents_sp = GNUstepGetIvar(valobj, "_contents_array");
+ ValueObjectSP count_sp = GNUstepGetIvar(valobj, "_count");
+ if (!contents_sp || !count_sp)
+ return std::nullopt;
+ ArrayContents contents;
+ contents.elements = contents_sp->GetValueAsUnsigned(LLDB_INVALID_ADDRESS);
+ contents.count = count_sp->GetValueAsUnsigned(0);
+ if (contents.count && contents.elements == LLDB_INVALID_ADDRESS)
+ return std::nullopt;
+ return contents;
+}
+
+class GNUstepNSArraySyntheticFrontEnd : public SyntheticChildrenFrontEnd {
+public:
+ GNUstepNSArraySyntheticFrontEnd(ValueObjectSP valobj_sp)
+ : SyntheticChildrenFrontEnd(*valobj_sp) {
+ if (valobj_sp) {
+ m_exe_ctx_ref = valobj_sp->GetExecutionContextRef();
+ if (ProcessSP process_sp = valobj_sp->GetProcessSP())
+ m_ptr_size = process_sp->GetAddressByteSize();
+ // Children are created as `id` so that each element resolves its own
+ // dynamic type and formatter, exactly like the Apple frontends do.
+ if (TargetSP target_sp = valobj_sp->GetTargetSP())
+ if (TypeSystemClangSP scratch_ts_sp =
+ ScratchTypeSystemClang::GetForTarget(*target_sp))
+ m_id_type = scratch_ts_sp->GetBasicType(eBasicTypeObjCID);
+ }
+ }
+
+ llvm::Expected<uint32_t> CalculateNumChildren() override {
+ return m_contents.count;
+ }
+
+ ValueObjectSP GetChildAtIndex(uint32_t idx) override {
+ if (idx >= m_contents.count || !m_id_type.IsValid())
+ return {};
+ StreamString name;
+ name.Printf("[%u]", idx);
+ return CreateChildValueObjectFromAddress(
+ name.GetString(), m_contents.elements + idx * m_ptr_size, m_exe_ctx_ref,
+ m_id_type);
+ }
+
+ lldb::ChildCacheState Update() override {
+ m_contents = ArrayContents();
+ if (std::optional<ArrayContents> contents = ReadArray(m_backend))
+ m_contents = *contents;
+ return lldb::ChildCacheState::eRefetch;
+ }
+
+ llvm::Expected<size_t> GetIndexOfChildWithName(ConstString name) override {
+ if (std::optional<size_t> idx = ExtractIndexFromString(name.GetCString()))
+ if (*idx < m_contents.count)
+ return *idx;
+ return llvm::createStringError("Type has no child named '%s'",
+ name.AsCString(""));
+ }
+
+private:
+ ExecutionContextRef m_exe_ctx_ref;
+ uint8_t m_ptr_size = 8;
+ CompilerType m_id_type;
+ ArrayContents m_contents;
+};
+
+} // namespace
+
+bool lldb_private::formatters::GNUstepNSArraySummaryProvider(
+ ValueObject &valobj, Stream &stream, const TypeSummaryOptions &options) {
+ if (!IsGNUstepObjCRuntime(valobj))
+ return false;
+ std::optional<ArrayContents> contents = ReadArray(valobj);
+ if (!contents)
+ return false;
+
+ static constexpr llvm::StringLiteral g_TypeHint("NSArray");
+ llvm::StringRef prefix, suffix;
+ if (Language *language = Language::FindPlugin(options.GetLanguage()))
+ std::tie(prefix, suffix) = language->GetFormatterPrefixSuffix(g_TypeHint);
+ stream << prefix;
+ stream.Printf("%" PRIu64 " %s%s", contents->count, "element",
+ contents->count == 1 ? "" : "s");
+ stream << suffix;
+ return true;
+}
+
+SyntheticChildrenFrontEnd *
+lldb_private::formatters::GNUstepNSArraySyntheticFrontEndCreator(
+ CXXSyntheticChildren *, lldb::ValueObjectSP valobj_sp) {
+ if (!valobj_sp || !IsGNUstepObjCRuntime(*valobj_sp))
+ return nullptr;
+ return new GNUstepNSArraySyntheticFrontEnd(valobj_sp);
+}
diff --git a/lldb/source/Plugins/Language/ObjC/GNUstepNSDictionary.cpp b/lldb/source/Plugins/Language/ObjC/GNUstepNSDictionary.cpp
new file mode 100644
index 0000000000000..2e87d854c8efd
--- /dev/null
+++ b/lldb/source/Plugins/Language/ObjC/GNUstepNSDictionary.cpp
@@ -0,0 +1,264 @@
+//===-- GNUstepNSDictionary.cpp -------------------------------------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+//
+// gnustep-base's GSDictionary, GSSet and GSCountedSet all embed a
+// `GSIMapTable_t map` (Headers/GNUstepBase/GSIMap.h): `bucketCount` buckets,
+// each `{ nodeCount, firstNode }`, chaining nodes `{ nextInBucket, key
+// [, value] }` through nextInBucket. Sets instantiate the map without the
+// value member, so nodes are read through their debug-info types rather
+// than at fixed offsets.
+//
+//===----------------------------------------------------------------------===//
+
+#include "GNUstepFormatters.h"
+#include "NSDictionary.h"
+
+#include "Plugins/TypeSystem/Clang/TypeSystemClang.h"
+#include "lldb/DataFormatters/FormattersHelpers.h"
+#include "lldb/DataFormatters/TypeSummary.h"
+#include "lldb/DataFormatters/TypeSynthetic.h"
+#include "lldb/Target/Language.h"
+#include "lldb/Target/Process.h"
+#include "lldb/Target/Target.h"
+#include "lldb/Utility/DataBufferHeap.h"
+#include "lldb/Utility/DataExtractor.h"
+#include "lldb/Utility/Status.h"
+#include "lldb/Utility/Stream.h"
+#include "lldb/ValueObject/ValueObject.h"
+
+#include <set>
+#include <vector>
+
+using namespace lldb;
+using namespace lldb_private;
+using namespace lldb_private::formatters;
+
+namespace {
+
+struct MapEntry {
+ addr_t key = 0;
+ addr_t value = 0;
+};
+
+/// The whole map's worth of entries, in bucket order (which is hash order,
+/// like -objectEnumerator).
+struct MapContents {
+ uint64_t node_count = 0;
+ std::vector<MapEntry> entries;
+};
+
+uint64_t ReadUnsignedMember(ValueObject &value, llvm::StringRef name,
+ uint64_t fail = 0) {
+ if (ValueObjectSP member_sp = value.GetChildMemberWithName(name))
+ return member_sp->GetValueAsUnsigned(fail);
+ return fail;
+}
+
+/// A GSIMapKey/GSIMapVal is a union whose members are all pointer sized;
+/// its `obj` (or `nsu`) member is the entry.
+uint64_t ReadUnionWord(ValueObject &value, llvm::StringRef name) {
+ ValueObjectSP member_sp = value.GetChildMemberWithName(name);
+ if (!member_sp)
+ return 0;
+ if (ValueObjectSP first_sp = member_sp->GetChildAtIndex(0))
+ return first_sp->GetValueAsUnsigned(0);
+ return member_sp->GetValueAsUnsigned(0);
+}
+
+/// Reads just the element count.
+std::optional<uint64_t> ReadNodeCount(ValueObject &valobj) {
+ ValueObjectSP map_sp = GNUstepGetIvar(valobj, "map");
+ if (!map_sp)
+ return std::nullopt;
+ ValueObjectSP count_sp = map_sp->GetChildMemberWithName("nodeCount");
+ if (!count_sp)
+ return std::nullopt;
+ return count_sp->GetValueAsUnsigned(0);
+}
+
+/// Walks every bucket. \p want_value is false for sets, whose nodes have no
+/// value member (GSSet.m sets GSI_MAP_HAS_VALUE to 0). Bounded by
+/// nodeCount, by the bucket count, and by never revisiting a node.
+std::optional<MapContents> ReadMap(ValueObject &valobj, bool want_value) {
+ ValueObjectSP map_sp = GNUstepGetIvar(valobj, "map");
+ if (!map_sp)
+ return std::nullopt;
+ MapContents contents;
+ contents.node_count = ReadUnsignedMember(*map_sp, "nodeCount");
+ const uint64_t bucket_count = ReadUnsignedMember(*map_sp, "bucketCount");
+ ValueObjectSP buckets_sp = map_sp->GetChildMemberWithName("buckets");
+ if (!buckets_sp)
+ return std::nullopt;
+ if (contents.node_count == 0)
+ return contents;
+ // A table cannot sensibly have more buckets than a few times its nodes;
+ // anything else is a misread and would make the walk unbounded.
+ if (bucket_count == 0 || bucket_count > contents.node_count * 8 + 64)
+ return std::nullopt;
+
+ std::set<addr_t> seen;
+ for (uint64_t b = 0;
+ b < bucket_count && contents.entries.size() < contents.node_count; ++b) {
+ ValueObjectSP bucket_sp = buckets_sp->GetSyntheticArrayMember(b, true);
+ if (!bucket_sp)
+ break;
+ ValueObjectSP node_sp = bucket_sp->GetChildMemberWithName("firstNode");
+ while (node_sp && node_sp->GetValueAsUnsigned(0) != 0 &&
+ contents.entries.size() < contents.node_count) {
+ const addr_t node_addr = node_sp->GetValueAsUnsigned(0);
+ if (!seen.insert(node_addr).second)
+ return std::nullopt; // cycle: corrupt or racing table
+ Status error;
+ ValueObjectSP node_struct_sp = node_sp->Dereference(error);
+ if (!node_struct_sp || error.Fail())
+ break;
+ MapEntry entry;
+ entry.key = ReadUnionWord(*node_struct_sp, "key");
+ if (want_value)
+ entry.value = ReadUnionWord(*node_struct_sp, "value");
+ contents.entries.push_back(entry);
+ node_sp = node_struct_sp->GetChildMemberWithName("nextInBucket");
+ }
+ }
+ return contents;
+}
+
+/// Presents each entry as `[i] = { key, value }` (dictionaries) or as the
+/// key object itself (sets).
+class GNUstepMapSyntheticFrontEnd : public SyntheticChildrenFrontEnd {
+public:
+ GNUstepMapSyntheticFrontEnd(ValueObjectSP valobj_sp, bool is_dictionary)
+ : SyntheticChildrenFrontEnd(*valobj_sp), m_is_dictionary(is_dictionary) {
+ if (valobj_sp) {
+ m_exe_ctx_ref = valobj_sp->GetExecutionContextRef();
+ if (ProcessSP process_sp = valobj_sp->GetProcessSP()) {
+ m_ptr_size = process_sp->GetAddressByteSize();
+ m_order = process_sp->GetByteOrder();
+ }
+ if (TargetSP target_sp = valobj_sp->GetTargetSP()) {
+ if (TypeSystemClangSP scratch_ts_sp =
+ ScratchTypeSystemClang::GetForTarget(*target_sp))
+ m_id_type = scratch_ts_sp->GetBasicType(eBasicTypeObjCID);
+ if (m_is_dictionary)
+ m_pair_type = GetLLDBNSPairType(target_sp);
+ }
+ }
+ }
+
+ llvm::Expected<uint32_t> CalculateNumChildren() override {
+ return m_contents.entries.size();
+ }
+
+ ValueObjectSP GetChildAtIndex(uint32_t idx) override {
+ if (idx >= m_contents.entries.size())
+ return {};
+ if (m_children[idx])
+ return m_children[idx];
+ const MapEntry &entry = m_contents.entries[idx];
+ StreamString name;
+ name.Printf("[%u]", idx);
+ if (!m_is_dictionary) {
+ // A set element is the key object itself.
+ m_children[idx] =
+ CreateChildFromWords(name.GetString(), {entry.key}, m_id_type);
+ return m_children[idx];
+ }
+ if (!m_pair_type.IsValid())
+ return {};
+ m_children[idx] = CreateChildFromWords(
+ name.GetString(), {entry.key, entry.value}, m_pair_type);
+ return m_children[idx];
+ }
+
+ lldb::ChildCacheState Update() override {
+ m_contents = MapContents();
+ m_children.clear();
+ if (std::optional<MapContents> contents =
+ ReadMap(m_backend, m_is_dictionary))
+ m_contents = *contents;
+ m_children.resize(m_contents.entries.size());
+ return lldb::ChildCacheState::eRefetch;
+ }
+
+ llvm::Expected<size_t> GetIndexOfChildWithName(ConstString name) override {
+ if (std::optional<size_t> idx = ExtractIndexFromString(name.GetCString()))
+ if (*idx < m_contents.entries.size())
+ return *idx;
+ return llvm::createStringError("Type has no child named '%s'",
+ name.AsCString(""));
+ }
+
+private:
+ ValueObjectSP CreateChildFromWords(llvm::StringRef name,
+ std::initializer_list<addr_t> words,
+ CompilerType type) {
+ WritableDataBufferSP buffer_sp(
+ new DataBufferHeap(words.size() * m_ptr_size, 0));
+ uint8_t *bytes = buffer_sp->GetBytes();
+ for (addr_t word : words) {
+ if (m_ptr_size == 8)
+ memcpy(bytes, &word, 8);
+ else {
+ uint32_t narrow = static_cast<uint32_t>(word);
+ memcpy(bytes, &narrow, 4);
+ }
+ bytes += m_ptr_size;
+ }
+ DataExtractor data(buffer_sp, m_order, m_ptr_size);
+ return CreateChildValueObjectFromData(name, data, m_exe_ctx_ref, type);
+ }
+
+ bool m_is_dictionary;
+ ExecutionContextRef m_exe_ctx_ref;
+ uint8_t m_ptr_size = 8;
+ lldb::ByteOrder m_order = lldb::eByteOrderLittle;
+ CompilerType m_id_type;
+ CompilerType m_pair_type;
+ MapContents m_contents;
+ std::vector<ValueObjectSP> m_children;
+};
+
+} // namespace
+
+bool lldb_private::formatters::GNUstepNSDictionarySummaryProvider(
+ ValueObject &valobj, Stream &stream, const TypeSummaryOptions &options) {
+ if (!IsGNUstepObjCRuntime(valobj))
+ return false;
+ std::optional<uint64_t> count = ReadNodeCount(valobj);
+ if (!count)
+ return false;
+ stream.Printf("%" PRIu64 " key/value pair%s", *count, *count == 1 ? "" : "s");
+ return true;
+}
+
+bool lldb_private::formatters::GNUstepNSSetSummaryProvider(
+ ValueObject &valobj, Stream &stream, const TypeSummaryOptions &options) {
+ if (!IsGNUstepObjCRuntime(valobj))
+ return false;
+ std::optional<uint64_t> count = ReadNodeCount(valobj);
+ if (!count)
+ return false;
+ stream.Printf("%" PRIu64 " element%s", *count, *count == 1 ? "" : "s");
+ return true;
+}
+
+SyntheticChildrenFrontEnd *
+lldb_private::formatters::GNUstepNSDictionarySyntheticFrontEndCreator(
+ CXXSyntheticChildren *, lldb::ValueObjectSP valobj_sp) {
+ if (!valobj_sp || !IsGNUstepObjCRuntime(*valobj_sp))
+ return nullptr;
+ return new GNUstepMapSyntheticFrontEnd(valobj_sp, /*is_dictionary=*/true);
+}
+
+SyntheticChildrenFrontEnd *
+lldb_private::formatters::GNUstepNSSetSyntheticFrontEndCreator(
+ CXXSyntheticChildren *, lldb::ValueObjectSP valobj_sp) {
+ if (!valobj_sp || !IsGNUstepObjCRuntime(*valobj_sp))
+ return nullptr;
+ return new GNUstepMapSyntheticFrontEnd(valobj_sp, /*is_dictionary=*/false);
+}
diff --git a/lldb/source/Plugins/Language/ObjC/GNUstepNSNumber.cpp b/lldb/source/Plugins/Language/ObjC/GNUstepNSNumber.cpp
new file mode 100644
index 0000000000000..7992b72558d41
--- /dev/null
+++ b/lldb/source/Plugins/Language/ObjC/GNUstepNSNumber.cpp
@@ -0,0 +1,126 @@
+//===-- GNUstepNSNumber.cpp -----------------------------------------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+#include "GNUstepFormatters.h"
+
+#include "Plugins/LanguageRuntime/ObjC/ObjCLanguageRuntime.h"
+#include "lldb/DataFormatters/TypeSummary.h"
+#include "lldb/Target/Language.h"
+#include "lldb/Target/Process.h"
+#include "lldb/Utility/Stream.h"
+
+#include <cstdarg>
+
+using namespace lldb;
+using namespace lldb_private;
+using namespace lldb_private::formatters;
+
+namespace {
+
+void PrintWithHint(Stream &stream, lldb::LanguageType lang,
+ llvm::StringRef hint, const char *format, ...)
+ __attribute__((format(printf, 4, 5)));
+
+void PrintWithHint(Stream &stream, lldb::LanguageType lang,
+ llvm::StringRef hint, const char *format, ...) {
+ llvm::StringRef prefix, suffix;
+ if (Language *language = Language::FindPlugin(lang))
+ std::tie(prefix, suffix) = language->GetFormatterPrefixSuffix(hint);
+ stream << prefix;
+ va_list args;
+ va_start(args, format);
+ stream.PrintfVarArg(format, args);
+ va_end(args);
+ stream << suffix;
+}
+
+} // namespace
+
+bool lldb_private::formatters::GNUstepNSNumberSummaryProvider(
+ ValueObject &valobj, Stream &stream, const TypeSummaryOptions &options) {
+ if (!IsGNUstepObjCRuntime(valobj))
+ return false;
+ const uint64_t ptr = valobj.GetValueAsUnsigned(0);
+ if (ptr == 0)
+ return false;
+ const lldb::LanguageType lang = options.GetLanguage();
+
+ // Small objects: the tag selects the class and the payload is in the
+ // pointer (Source/NSNumber.m).
+ switch (ptr & g_gnustep_small_object_mask) {
+ case 1: // NSSmallInt
+ PrintWithHint(stream, lang, "NSNumber:long", "%" PRId64,
+ GNUstepDecodeSmallInt(ptr));
+ return true;
+ case 2: // NSSmallExtendedDouble
+ PrintWithHint(stream, lang, "NSNumber:double", "%g",
+ GNUstepDecodeSmallExtendedDouble(ptr));
+ return true;
+ case 3: // NSSmallRepeatingDouble
+ PrintWithHint(stream, lang, "NSNumber:double", "%g",
+ GNUstepDecodeSmallRepeatingDouble(ptr));
+ return true;
+ case 5: // NSSmallFloat: same encoding, single precision when created
+ PrintWithHint(stream, lang, "NSNumber:float", "%f",
+ static_cast<float>(GNUstepDecodeSmallRepeatingDouble(ptr)));
+ return true;
+ case 0:
+ break;
+ default:
+ return false;
+ }
+
+ ProcessSP process_sp = valobj.GetProcessSP();
+ ObjCLanguageRuntime *runtime =
+ process_sp ? ObjCLanguageRuntime::Get(*process_sp) : nullptr;
+ if (!runtime)
+ return false;
+ ObjCLanguageRuntime::ClassDescriptorSP descriptor =
+ runtime->GetClassDescriptor(valobj);
+ if (!descriptor || !descriptor->IsValid())
+ return false;
+ llvm::StringRef class_name = descriptor->GetClassName().GetStringRef();
+
+ // Every heap NSNumber subclass has exactly one ivar, `value`, whose C type
+ // is what the class name says (Source/NSNumber.m).
+ ValueObjectSP value_sp = GNUstepGetIvar(valobj, "value");
+ if (!value_sp)
+ return false;
+
+ if (class_name == "NSBoolNumber") {
+ stream.PutCString(value_sp->GetValueAsUnsigned(0) ? "YES" : "NO");
+ return true;
+ }
+ if (class_name == "NSIntNumber") {
+ PrintWithHint(stream, lang, "NSNumber:int", "%d",
+ static_cast<int>(value_sp->GetValueAsSigned(0)));
+ return true;
+ }
+ if (class_name == "NSLongLongNumber") {
+ PrintWithHint(stream, lang, "NSNumber:long", "%" PRId64,
+ value_sp->GetValueAsSigned(0));
+ return true;
+ }
+ if (class_name == "NSUnsignedLongLongNumber") {
+ PrintWithHint(stream, lang, "NSNumber:long", "%" PRIu64,
+ value_sp->GetValueAsUnsigned(0));
+ return true;
+ }
+ if (class_name == "NSFloatNumber" || class_name == "NSDoubleNumber") {
+ std::optional<double> value = GNUstepGetFloatValue(*value_sp);
+ if (!value)
+ return false;
+ if (class_name == "NSFloatNumber")
+ PrintWithHint(stream, lang, "NSNumber:float", "%f",
+ static_cast<float>(*value));
+ else
+ PrintWithHint(stream, lang, "NSNumber:double", "%g", *value);
+ return true;
+ }
+ return false;
+}
diff --git a/lldb/source/Plugins/Language/ObjC/GNUstepNSString.cpp b/lldb/source/Plugins/Language/ObjC/GNUstepNSString.cpp
new file mode 100644
index 0000000000000..20f7ab889e9de
--- /dev/null
+++ b/lldb/source/Plugins/Language/ObjC/GNUstepNSString.cpp
@@ -0,0 +1,239 @@
+//===-- GNUstepNSString.cpp -----------------------------------------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+#include "GNUstepFormatters.h"
+
+#include "Plugins/LanguageRuntime/ObjC/ObjCLanguageRuntime.h"
+#include "lldb/DataFormatters/StringPrinter.h"
+#include "lldb/DataFormatters/TypeSummary.h"
+#include "lldb/Target/Language.h"
+#include "lldb/Target/Process.h"
+#include "lldb/Target/Target.h"
+#include "lldb/Utility/DataExtractor.h"
+#include "lldb/Utility/Status.h"
+#include "lldb/Utility/Stream.h"
+
+#include <vector>
+
+using namespace lldb;
+using namespace lldb_private;
+using namespace lldb_private::formatters;
+
+namespace {
+
+/// The character encodings a string may hold. gnustep-base's 8-bit strings
+/// use its "internal encoding", which defaults to ISO Latin-1
+/// (Source/GSString.m), so one byte is one code point.
+enum class Encoding { Latin1, UTF8, UTF16, UTF32 };
+
+/// Where the characters live and how many there are.
+struct StringContents {
+ addr_t address = LLDB_INVALID_ADDRESS;
+ /// Number of code units of the encoding, not bytes.
+ uint64_t count = 0;
+ Encoding encoding = Encoding::Latin1;
+};
+
+/// A ValueObject for the union `GSCharPtr _contents` (or a plain pointer)
+/// yields the buffer address either way.
+addr_t GetPointerValue(ValueObject &value) {
+ if (value.GetCompilerType().IsPointerType())
+ return value.GetValueAsUnsigned(LLDB_INVALID_ADDRESS);
+ // A union: any member is the same pointer.
+ if (ValueObjectSP first_sp = value.GetChildAtIndex(0))
+ return first_sp->GetValueAsUnsigned(LLDB_INVALID_ADDRESS);
+ return LLDB_INVALID_ADDRESS;
+}
+
+/// NSConstantString with the gnustep-2.x string ABI: `uint32_t flags`
+/// (low two bits: 0 ASCII, 1 UTF-8, 2 UTF-16, 3 UTF-32), `uint32_t nxcslen`
+/// (characters), `uint32_t size` (bytes), `uint32_t hash`, `const char
+/// *nxcsptr` (Headers/Foundation/NSString.h). The legacy ABI has only
+/// `nxcsptr` and a byte count `nxcslen`.
+std::optional<StringContents> ReadConstantString(ValueObject &valobj) {
+ ValueObjectSP ptr_sp = GNUstepGetIvar(valobj, "nxcsptr");
+ ValueObjectSP len_sp = GNUstepGetIvar(valobj, "nxcslen");
+ if (!ptr_sp || !len_sp)
+ return std::nullopt;
+ StringContents contents;
+ contents.address = ptr_sp->GetValueAsUnsigned(LLDB_INVALID_ADDRESS);
+ if (contents.address == LLDB_INVALID_ADDRESS)
+ return std::nullopt;
+
+ ValueObjectSP flags_sp = GNUstepGetIvar(valobj, "flags");
+ ValueObjectSP size_sp = GNUstepGetIvar(valobj, "size");
+ if (!flags_sp || !size_sp) {
+ // Legacy ABI: nxcslen is a byte count of UTF-8 data.
+ contents.encoding = Encoding::UTF8;
+ contents.count = len_sp->GetValueAsUnsigned(0);
+ return contents;
+ }
+ const uint64_t bytes = size_sp->GetValueAsUnsigned(0);
+ switch (flags_sp->GetValueAsUnsigned(0) & 3) {
+ case 0:
+ case 1:
+ contents.encoding = Encoding::UTF8;
+ contents.count = bytes;
+ break;
+ case 2:
+ contents.encoding = Encoding::UTF16;
+ contents.count = bytes / 2;
+ break;
+ default:
+ contents.encoding = Encoding::UTF32;
+ contents.count = bytes / 4;
+ break;
+ }
+ return contents;
+}
+
+/// GSString and everything derived from it, plus GSMutableString: the buffer
+/// pointer `_contents`, the character count `_count`, and `_flags` whose bit
+/// 0 (`wide`) selects 16-bit characters (Source/GSPrivate.h). The buffer is
+/// never NUL-terminated.
+std::optional<StringContents> ReadGSString(ValueObject &valobj) {
+ ValueObjectSP contents_sp = GNUstepGetIvar(valobj, "_contents");
+ ValueObjectSP count_sp = GNUstepGetIvar(valobj, "_count");
+ ValueObjectSP flags_sp = GNUstepGetIvar(valobj, "_flags");
+ if (!contents_sp || !count_sp || !flags_sp)
+ return std::nullopt;
+ StringContents contents;
+ contents.address = GetPointerValue(*contents_sp);
+ if (contents.address == LLDB_INVALID_ADDRESS)
+ return std::nullopt;
+ contents.count = count_sp->GetValueAsUnsigned(0);
+ bool wide = false;
+ if (ValueObjectSP wide_sp = flags_sp->GetChildMemberWithName("wide"))
+ wide = wide_sp->GetValueAsUnsigned(0) != 0;
+ contents.encoding = wide ? Encoding::UTF16 : Encoding::Latin1;
+ return contents;
+}
+
+bool DumpContents(ValueObject &valobj, Stream &stream,
+ const TypeSummaryOptions &summary_options,
+ const StringContents &contents) {
+ static constexpr llvm::StringLiteral g_TypeHint("NSString");
+ llvm::StringRef prefix, suffix;
+ if (Language *language = Language::FindPlugin(summary_options.GetLanguage()))
+ std::tie(prefix, suffix) = language->GetFormatterPrefixSuffix(g_TypeHint);
+
+ if (contents.count == 0) {
+ stream << prefix << "\"\"" << suffix;
+ return true;
+ }
+
+ StringPrinter::ReadStringAndDumpToStreamOptions options(valobj);
+ options.SetLocation(Address(contents.address));
+ options.SetTargetSP(valobj.GetTargetSP());
+ options.SetStream(&stream);
+ options.SetPrefixToken(prefix.str());
+ options.SetSuffixToken(suffix.str());
+ options.SetQuote('"');
+ options.SetSourceSize(contents.count);
+ options.SetHasSourceSize(true);
+ options.SetZeroTermination(StringPrinter::ZeroTermination::Ignore);
+ options.SetIgnoreMaxLength(summary_options.GetCapping() ==
+ TypeSummaryCapping::eTypeSummaryUncapped);
+
+ switch (contents.encoding) {
+ case Encoding::Latin1: {
+ // Read exactly `count` bytes and transcode to UTF-8 ourselves: the ASCII
+ // path of the printer reads a C string (dropping the last byte for a
+ // terminator the buffer does not have) and cannot represent code points
+ // above 0x7f.
+ ProcessSP process_sp = valobj.GetProcessSP();
+ if (!process_sp)
+ return false;
+ const uint64_t max_size =
+ valobj.GetTargetSP()->GetMaximumSizeOfStringSummary();
+ uint64_t to_read = contents.count;
+ bool truncated = false;
+ if (!options.GetIgnoreMaxLength() && to_read > max_size) {
+ to_read = max_size;
+ truncated = true;
+ }
+ std::vector<uint8_t> latin1(to_read);
+ Status error;
+ if (to_read && process_sp->ReadMemory(contents.address, latin1.data(),
+ to_read, error) != to_read)
+ return false;
+ std::string utf8;
+ utf8.reserve(to_read * 2);
+ for (uint8_t byte : latin1) {
+ if (byte < 0x80) {
+ utf8.push_back(static_cast<char>(byte));
+ } else {
+ utf8.push_back(static_cast<char>(0xC0 | (byte >> 6)));
+ utf8.push_back(static_cast<char>(0x80 | (byte & 0x3F)));
+ }
+ }
+ StringPrinter::ReadBufferAndDumpToStreamOptions dump_options(options);
+ dump_options.SetData(DataExtractor(utf8.data(), utf8.size(),
+ process_sp->GetByteOrder(),
+ process_sp->GetAddressByteSize()));
+ dump_options.SetSourceSize(utf8.size());
+ dump_options.SetIsTruncated(truncated);
+ return StringPrinter::ReadBufferAndDumpToStream<
+ StringPrinter::StringElementType::UTF8>(dump_options);
+ }
+ case Encoding::UTF8:
+ return StringPrinter::ReadStringAndDumpToStream<
+ StringPrinter::StringElementType::UTF8>(options);
+ case Encoding::UTF16:
+ return StringPrinter::ReadStringAndDumpToStream<
+ StringPrinter::StringElementType::UTF16>(options);
+ case Encoding::UTF32:
+ return StringPrinter::ReadStringAndDumpToStream<
+ StringPrinter::StringElementType::UTF32>(options);
+ }
+ return false;
+}
+
+} // namespace
+
+bool lldb_private::formatters::GNUstepNSStringSummaryProvider(
+ ValueObject &valobj, Stream &stream, const TypeSummaryOptions &options) {
+ if (!IsGNUstepObjCRuntime(valobj))
+ return false;
+ const uint64_t ptr = valobj.GetValueAsUnsigned(0);
+ if (ptr == 0)
+ return false;
+
+ // Up to eight ASCII characters live in the pointer itself; clang emits
+ // such literals directly (CGObjCGNU.cpp) and the runtime never allocates a
+ // GSTinyString object.
+ if (std::optional<std::string> tiny = GNUstepDecodeTinyString(ptr)) {
+ static constexpr llvm::StringLiteral g_TypeHint("NSString");
+ llvm::StringRef prefix, suffix;
+ if (Language *language = Language::FindPlugin(options.GetLanguage()))
+ std::tie(prefix, suffix) = language->GetFormatterPrefixSuffix(g_TypeHint);
+ stream << prefix << '"' << *tiny << '"' << suffix;
+ return true;
+ }
+ if (ptr & g_gnustep_small_object_mask)
+ return false;
+
+ ProcessSP process_sp = valobj.GetProcessSP();
+ ObjCLanguageRuntime *runtime =
+ process_sp ? ObjCLanguageRuntime::Get(*process_sp) : nullptr;
+ if (!runtime)
+ return false;
+ ObjCLanguageRuntime::ClassDescriptorSP descriptor =
+ runtime->GetClassDescriptor(valobj);
+ if (!descriptor || !descriptor->IsValid())
+ return false;
+
+ std::optional<StringContents> contents;
+ if (descriptor->GetClassName() == "NSConstantString")
+ contents = ReadConstantString(valobj);
+ else
+ contents = ReadGSString(valobj);
+ if (!contents)
+ return false;
+ return DumpContents(valobj, stream, options, *contents);
+}
diff --git a/lldb/source/Plugins/Language/ObjC/NSArray.cpp b/lldb/source/Plugins/Language/ObjC/NSArray.cpp
index 333aa1f683b5d..722e5e1e9e4b6 100644
--- a/lldb/source/Plugins/Language/ObjC/NSArray.cpp
+++ b/lldb/source/Plugins/Language/ObjC/NSArray.cpp
@@ -10,6 +10,7 @@
#include "clang/Basic/TargetInfo.h"
#include "Cocoa.h"
+#include "GNUstepFormatters.h"
#include "Plugins/LanguageRuntime/ObjC/AppleObjCRuntime/AppleObjCRuntime.h"
#include "Plugins/TypeSystem/Clang/TypeSystemClang.h"
@@ -334,6 +335,10 @@ bool lldb_private::formatters::NSArraySummaryProvider(
if (!runtime)
return false;
+ // gnustep-base lays its classes out differently and names them
+ // differently; hand those over.
+ if (llvm::isa<GNUstepObjCRuntime>(runtime))
+ return GNUstepNSArraySummaryProvider(valobj, stream, options);
ObjCLanguageRuntime::ClassDescriptorSP descriptor(
runtime->GetClassDescriptor(valobj));
@@ -752,8 +757,11 @@ lldb_private::formatters::NSArraySyntheticFrontEndCreator(
lldb::ProcessSP process_sp(valobj_sp->GetProcessSP());
if (!process_sp)
return nullptr;
- AppleObjCRuntime *runtime = llvm::dyn_cast_or_null<AppleObjCRuntime>(
- ObjCLanguageRuntime::Get(*process_sp));
+ ObjCLanguageRuntime *objc_runtime = ObjCLanguageRuntime::Get(*process_sp);
+ if (llvm::isa_and_nonnull<GNUstepObjCRuntime>(objc_runtime))
+ return GNUstepNSArraySyntheticFrontEndCreator(synth, valobj_sp);
+ AppleObjCRuntime *runtime =
+ llvm::dyn_cast_or_null<AppleObjCRuntime>(objc_runtime);
if (!runtime)
return nullptr;
diff --git a/lldb/source/Plugins/Language/ObjC/NSDictionary.cpp b/lldb/source/Plugins/Language/ObjC/NSDictionary.cpp
index e9a73b4013249..01f72aca1958d 100644
--- a/lldb/source/Plugins/Language/ObjC/NSDictionary.cpp
+++ b/lldb/source/Plugins/Language/ObjC/NSDictionary.cpp
@@ -11,6 +11,7 @@
#include "clang/AST/DeclCXX.h"
#include "CFBasicHash.h"
+#include "GNUstepFormatters.h"
#include "NSDictionary.h"
#include "Plugins/LanguageRuntime/ObjC/AppleObjCRuntime/AppleObjCRuntime.h"
@@ -65,7 +66,7 @@ NSDictionary_Additionals::GetAdditionalSynthetics() {
return g_map;
}
-static CompilerType GetLLDBNSPairType(TargetSP target_sp) {
+CompilerType lldb_private::formatters::GetLLDBNSPairType(TargetSP target_sp) {
CompilerType compiler_type;
TypeSystemClangSP scratch_ts_sp =
ScratchTypeSystemClang::GetForTarget(*target_sp);
@@ -397,6 +398,10 @@ bool lldb_private::formatters::NSDictionarySummaryProvider(
if (!runtime)
return false;
+ // gnustep-base lays its classes out differently and names them
+ // differently; hand those over.
+ if (llvm::isa<GNUstepObjCRuntime>(runtime))
+ return GNUstepNSDictionarySummaryProvider(valobj, stream, options);
ObjCLanguageRuntime::ClassDescriptorSP descriptor(
runtime->GetNonKVOClassDescriptor(valobj));
@@ -506,8 +511,11 @@ lldb_private::formatters::NSDictionarySyntheticFrontEndCreator(
lldb::ProcessSP process_sp(valobj_sp->GetProcessSP());
if (!process_sp)
return nullptr;
- AppleObjCRuntime *runtime = llvm::dyn_cast_or_null<AppleObjCRuntime>(
- ObjCLanguageRuntime::Get(*process_sp));
+ ObjCLanguageRuntime *objc_runtime = ObjCLanguageRuntime::Get(*process_sp);
+ if (llvm::isa_and_nonnull<GNUstepObjCRuntime>(objc_runtime))
+ return GNUstepNSDictionarySyntheticFrontEndCreator(synth, valobj_sp);
+ AppleObjCRuntime *runtime =
+ llvm::dyn_cast_or_null<AppleObjCRuntime>(objc_runtime);
if (!runtime)
return nullptr;
diff --git a/lldb/source/Plugins/Language/ObjC/NSDictionary.h b/lldb/source/Plugins/Language/ObjC/NSDictionary.h
index a65298de56b90..42f677c621190 100644
--- a/lldb/source/Plugins/Language/ObjC/NSDictionary.h
+++ b/lldb/source/Plugins/Language/ObjC/NSDictionary.h
@@ -11,6 +11,7 @@
#include "lldb/DataFormatters/TypeSummary.h"
#include "lldb/DataFormatters/TypeSynthetic.h"
+#include "lldb/Symbol/CompilerType.h"
#include "lldb/Utility/ConstString.h"
#include "lldb/Utility/Stream.h"
#include "lldb/ValueObject/ValueObject.h"
@@ -36,6 +37,11 @@ SyntheticChildrenFrontEnd *
NSDictionarySyntheticFrontEndCreator(CXXSyntheticChildren *,
lldb::ValueObjectSP);
+/// The `struct { id key; id value; }` type used to present dictionary
+/// entries as children. Shared with the GNUstep dictionary frontend so both
+/// runtimes present entries identically.
+CompilerType GetLLDBNSPairType(lldb::TargetSP target_sp);
+
class NSDictionary_Additionals {
public:
class AdditionalFormatterMatching {
diff --git a/lldb/source/Plugins/Language/ObjC/NSSet.cpp b/lldb/source/Plugins/Language/ObjC/NSSet.cpp
index 9dd177b52fb83..bf82528fdfd16 100644
--- a/lldb/source/Plugins/Language/ObjC/NSSet.cpp
+++ b/lldb/source/Plugins/Language/ObjC/NSSet.cpp
@@ -8,6 +8,7 @@
#include "NSSet.h"
#include "CFBasicHash.h"
+#include "GNUstepFormatters.h"
#include "Plugins/LanguageRuntime/ObjC/AppleObjCRuntime/AppleObjCRuntime.h"
#include "lldb/DataFormatters/FormattersHelpers.h"
@@ -229,6 +230,10 @@ bool lldb_private::formatters::NSSetSummaryProvider(
if (!runtime)
return false;
+ // gnustep-base lays its classes out differently and names them
+ // differently; hand those over.
+ if (llvm::isa<GNUstepObjCRuntime>(runtime))
+ return GNUstepNSSetSummaryProvider(valobj, stream, options);
ObjCLanguageRuntime::ClassDescriptorSP descriptor(
runtime->GetClassDescriptor(valobj));
@@ -312,6 +317,10 @@ lldb_private::formatters::NSSetSyntheticFrontEndCreator(
ObjCLanguageRuntime *runtime = ObjCLanguageRuntime::Get(*process_sp);
if (!runtime)
return nullptr;
+ // gnustep-base lays its classes out differently and names them
+ // differently; hand those over.
+ if (llvm::isa<GNUstepObjCRuntime>(runtime))
+ return GNUstepNSSetSyntheticFrontEndCreator(synth, valobj_sp);
CompilerType valobj_type(valobj_sp->GetCompilerType());
Flags flags(valobj_type.GetTypeInfo());
diff --git a/lldb/source/Plugins/Language/ObjC/NSString.cpp b/lldb/source/Plugins/Language/ObjC/NSString.cpp
index 7a295119bd031..4bec85168603e 100644
--- a/lldb/source/Plugins/Language/ObjC/NSString.cpp
+++ b/lldb/source/Plugins/Language/ObjC/NSString.cpp
@@ -7,6 +7,7 @@
//===----------------------------------------------------------------------===//
#include "NSString.h"
+#include "GNUstepFormatters.h"
#include "lldb/DataFormatters/FormattersHelpers.h"
#include "lldb/DataFormatters/StringPrinter.h"
@@ -43,6 +44,10 @@ bool lldb_private::formatters::NSStringSummaryProvider(
if (!runtime)
return false;
+ // gnustep-base lays its classes out differently and names them
+ // differently; hand those over.
+ if (llvm::isa<GNUstepObjCRuntime>(runtime))
+ return GNUstepNSStringSummaryProvider(valobj, stream, summary_options);
ObjCLanguageRuntime::ClassDescriptorSP descriptor(
runtime->GetClassDescriptor(valobj));
diff --git a/lldb/source/Plugins/Language/ObjC/ObjCLanguage.cpp b/lldb/source/Plugins/Language/ObjC/ObjCLanguage.cpp
index 42c53e1b81cfb..b09eeaeac8513 100644
--- a/lldb/source/Plugins/Language/ObjC/ObjCLanguage.cpp
+++ b/lldb/source/Plugins/Language/ObjC/ObjCLanguage.cpp
@@ -30,6 +30,7 @@
#include "CF.h"
#include "Cocoa.h"
#include "CoreMedia.h"
+#include "GNUstepFormatters.h"
#include "NSDictionary.h"
#include "NSSet.h"
#include "NSString.h"
@@ -876,6 +877,7 @@ lldb::TypeCategoryImplSP ObjCLanguage::GetFormatters() {
if (g_category) {
LoadCoreMediaFormatters(g_category);
LoadObjCFormatters(g_category);
+ lldb_private::formatters::LoadGNUstepFormatters(g_category);
}
});
return g_category;
>From e4e52eb76f1643aa98ea074777875ed965cf5a7d Mon Sep 17 00:00:00 2001
From: Rob <robk at robk.dev>
Date: Sun, 16 Aug 2026 01:38:57 +0100
Subject: [PATCH 20/49] [lldb][GNUstep] Find SmallObjectClasses through debug
info as a fallback
The tagged-pointer vendor locates libobjc2's SmallObjectClasses table by
symbol name. The table has hidden visibility, so a linked image only has
a symbol for it if a PDB or an unstripped symbol table is present; a
libobjc2 built with DWARF into a PE - the layout the tools-windows-msvc
toolchain produces once -gdwarf is requested - has neither, and every
tagged pointer then went untyped. The debug info still describes the
table as a global variable, so fall back to that to find its address.
Assisted-by: Claude Fable 5
---
.../GNUstepObjCClassDescriptor.cpp | 42 ++++++++++++++++---
1 file changed, 36 insertions(+), 6 deletions(-)
diff --git a/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCClassDescriptor.cpp b/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCClassDescriptor.cpp
index 1261aa347ab41..061320b590f86 100644
--- a/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCClassDescriptor.cpp
+++ b/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCClassDescriptor.cpp
@@ -12,6 +12,8 @@
#include "lldb/Core/ModuleList.h"
#include "lldb/Symbol/Symbol.h"
#include "lldb/Symbol/SymbolContext.h"
+#include "lldb/Symbol/Variable.h"
+#include "lldb/Symbol/VariableList.h"
#include "lldb/Target/Process.h"
#include "lldb/Target/Target.h"
#include "lldb/Utility/ArchSpec.h"
@@ -19,6 +21,16 @@
#include "lldb/Utility/LLDBLog.h"
#include "lldb/Utility/Log.h"
#include "lldb/Utility/Status.h"
+#include "lldb/ValueObject/ValueObjectVariable.h"
+
+#include "llvm/ADT/DenseSet.h"
+#include "llvm/ADT/SmallPtrSet.h"
+#include "llvm/Support/MathExtras.h"
+
+#include <functional>
+#include <memory>
+#include <string>
+#include <vector>
using namespace lldb;
using namespace lldb_private;
@@ -183,11 +195,11 @@ bool GNUstepObjCTaggedPointerClassDescriptor::GetTaggedPointerInfoSigned(
if (value_bits) {
// Sign-extend from the target's pointer width before shifting, so that a
// negative payload in a 32-bit pointer is not read as a large positive.
+ // Done through SignExtend64 rather than by hand: shifting a signed value
+ // into its own sign bit is undefined.
const uint32_t pointer_bits = m_pointer_size * 8;
- int64_t signed_value = static_cast<int64_t>(m_pointer_value)
- << (64 - pointer_bits);
- signed_value >>= (64 - pointer_bits);
- *value_bits = signed_value >> m_payload_shift;
+ *value_bits =
+ llvm::SignExtend64(m_pointer_value, pointer_bits) >> m_payload_shift;
}
if (payload)
*payload = m_pointer_value;
@@ -232,10 +244,28 @@ GNUstepTaggedPointerVendor::GetClassDescriptor(lldb::addr_t ptr) {
break;
}
}
+ // The table has hidden visibility, so a linked image carries no symbol
+ // for it unless a PDB or an unstripped symtab is around; the debug info
+ // still describes it as a global, which is enough to find its address.
+ if (*m_table_addr == LLDB_INVALID_ADDRESS) {
+ VariableList variables;
+ target.GetImages().FindGlobalVariables(ConstString("SmallObjectClasses"),
+ 1, variables);
+ if (VariableSP variable_sp = variables.GetVariableAtIndex(0)) {
+ ValueObjectSP valobj_sp =
+ ValueObjectVariable::Create(&target, variable_sp);
+ if (valobj_sp) {
+ const addr_t table = valobj_sp->GetAddressOf(false).address;
+ if (table != 0 && table != LLDB_INVALID_ADDRESS)
+ m_table_addr = table;
+ }
+ }
+ }
if (*m_table_addr == LLDB_INVALID_ADDRESS)
LLDB_LOG(GetLog(LLDBLog::Language),
- "GNUstepTaggedPointerVendor: SmallObjectClasses symbol not "
- "found (stripped libobjc?); tagged pointer classes unknown");
+ "GNUstepTaggedPointerVendor: SmallObjectClasses not found in "
+ "any symbol table or debug info (stripped libobjc?); tagged "
+ "pointer classes unknown");
}
if (*m_table_addr == LLDB_INVALID_ADDRESS)
return nullptr;
>From 65f2803f4f1867dffbaf3c8e19d2899a00f0dbbc Mon Sep 17 00:00:00 2001
From: Rob <robk at robk.dev>
Date: Sun, 16 Aug 2026 01:38:58 +0100
Subject: [PATCH 21/49] [lldb][GNUstep] Add tests for the gnustep-base data
formatters
Three layers:
- Unit tests for the small-object decoders, hermetic and hence run
everywhere. The constants are what clang and gnustep-base actually
produce (a live process was the oracle), including clang's own
encoding of @"Hello" as a tagged GSTinyString.
- An API test, lang/objc-gnustep/data-formatters, whose inferior creates
every concrete class the formatters know - strings in each storage
form, tagged and boxed numbers, arrays, dictionaries, sets, data,
dates, NSNull, and a custom object - and which checks the summaries,
the synthetic children and the SB API. It needs gnustep-base, a
larger dependency than the runtime, so it is gated behind a new
objc-gnustep-base category driven by a new
LLDB_TEST_OBJC_GNUSTEP_BASE_DIR CMake variable, plumbed through lit,
dotest and Makefile.rules the same way LLDB_TEST_OBJC_GNUSTEP_DIR is.
On Windows the runtime and Foundation DLLs, and their PDBs, are copied
next to the test binary because the test harness scrubs the inferior's
PATH.
- A Shell test, objc-gnustep-class-objects, for the class-object fix
in the runtime, which needs no Foundation.
Assisted-by: Claude Fable 5
---
.../Python/lldbsuite/test/builders/builder.py | 9 +-
.../Python/lldbsuite/test/configuration.py | 4 +
lldb/packages/Python/lldbsuite/test/dotest.py | 9 +
.../Python/lldbsuite/test/dotest_args.py | 8 +
.../Python/lldbsuite/test/make/Makefile.rules | 34 ++++
.../Python/lldbsuite/test/test_categories.py | 1 +
.../objc-gnustep/data-formatters/Makefile | 3 +
.../TestGNUstepDataFormatters.py | 169 ++++++++++++++++++
.../objc-gnustep/data-formatters/categories | 1 +
.../lang/objc-gnustep/data-formatters/main.m | 103 +++++++++++
lldb/test/API/lit.cfg.py | 2 +
lldb/test/API/lit.site.cfg.py.in | 1 +
lldb/test/CMakeLists.txt | 14 ++
lldb/unittests/Language/ObjC/CMakeLists.txt | 1 +
.../Language/ObjC/GNUstepFormattersTest.cpp | 95 ++++++++++
15 files changed, 452 insertions(+), 2 deletions(-)
create mode 100644 lldb/test/API/lang/objc-gnustep/data-formatters/Makefile
create mode 100644 lldb/test/API/lang/objc-gnustep/data-formatters/TestGNUstepDataFormatters.py
create mode 100644 lldb/test/API/lang/objc-gnustep/data-formatters/categories
create mode 100644 lldb/test/API/lang/objc-gnustep/data-formatters/main.m
create mode 100644 lldb/unittests/Language/ObjC/GNUstepFormattersTest.cpp
diff --git a/lldb/packages/Python/lldbsuite/test/builders/builder.py b/lldb/packages/Python/lldbsuite/test/builders/builder.py
index 3ed463ae76049..e88e3d824e981 100644
--- a/lldb/packages/Python/lldbsuite/test/builders/builder.py
+++ b/lldb/packages/Python/lldbsuite/test/builders/builder.py
@@ -244,9 +244,14 @@ def getLibCxxArgs(self):
return []
def getObjcGnustepArgs(self):
+ args = []
if configuration.objc_gnustep_dir:
- return ["OBJC_GNUSTEP_DIR={}".format(configuration.objc_gnustep_dir)]
- return []
+ args.append("OBJC_GNUSTEP_DIR={}".format(configuration.objc_gnustep_dir))
+ if configuration.objc_gnustep_base_dir:
+ args.append(
+ "OBJC_GNUSTEP_BASE_DIR={}".format(configuration.objc_gnustep_base_dir)
+ )
+ return args
def getLLDBObjRoot(self):
if configuration.lldb_obj_root:
diff --git a/lldb/packages/Python/lldbsuite/test/configuration.py b/lldb/packages/Python/lldbsuite/test/configuration.py
index 2c7a4f5f12afe..6b3afeb38bfa2 100644
--- a/lldb/packages/Python/lldbsuite/test/configuration.py
+++ b/lldb/packages/Python/lldbsuite/test/configuration.py
@@ -152,6 +152,10 @@
# non-Apple platforms.
objc_gnustep_dir = None
+# GNUstep gnustep-base (Foundation) installation directory, for the tests
+# that need Foundation classes on top of the runtime.
+objc_gnustep_base_dir = None
+
# A plugin whose tests will be enabled, like intel-pt.
enabled_plugins = []
diff --git a/lldb/packages/Python/lldbsuite/test/dotest.py b/lldb/packages/Python/lldbsuite/test/dotest.py
index 3e4279dc9d2ac..d91a7d6c8fe3b 100644
--- a/lldb/packages/Python/lldbsuite/test/dotest.py
+++ b/lldb/packages/Python/lldbsuite/test/dotest.py
@@ -47,6 +47,7 @@
from ..support import temp_file
from ..support import xcode
+
def is_exe(fpath):
"""Returns true if fpath is an executable."""
if fpath is None:
@@ -294,6 +295,9 @@ def parseOptionsAndInitTestdirs():
if args.objc_gnustep_dir:
configuration.objc_gnustep_dir = args.objc_gnustep_dir
+ if args.objc_gnustep_base_dir:
+ configuration.objc_gnustep_base_dir = args.objc_gnustep_base_dir
+
if args.libcxx_include_dir or args.libcxx_library_dir:
if args.lldb_platform_name:
logging.warning(
@@ -1029,6 +1033,11 @@ def checkObjcGnustepSupport():
print("objc-gnustep tests will be skipped because no GNUstep")
print("libobjc2 installation was specified")
configuration.skip_categories.append("objc-gnustep")
+ if not configuration.objc_gnustep_base_dir:
+ if configuration.verbose:
+ print("objc-gnustep-base tests will be skipped because no")
+ print("gnustep-base installation was specified")
+ configuration.skip_categories.append("objc-gnustep-base")
def checkExpressionSupport():
diff --git a/lldb/packages/Python/lldbsuite/test/dotest_args.py b/lldb/packages/Python/lldbsuite/test/dotest_args.py
index b3dbd1c378bb2..c2913637353c5 100644
--- a/lldb/packages/Python/lldbsuite/test/dotest_args.py
+++ b/lldb/packages/Python/lldbsuite/test/dotest_args.py
@@ -93,6 +93,14 @@ def create_parser():
"Specify the path to a GNUstep libobjc2 installation to build Objective-C tests against on non-Apple platforms."
),
)
+ group.add_argument(
+ "--objc-gnustep-base-dir",
+ metavar="dir",
+ dest="objc_gnustep_base_dir",
+ help=textwrap.dedent(
+ "Specify the path to a GNUstep gnustep-base (Foundation) installation; enables the objc-gnustep-base tests."
+ ),
+ )
# FIXME? This won't work for different extra flags according to each triple.
group.add_argument(
"-E",
diff --git a/lldb/packages/Python/lldbsuite/test/make/Makefile.rules b/lldb/packages/Python/lldbsuite/test/make/Makefile.rules
index 33c2b0788f87d..700291012273d 100644
--- a/lldb/packages/Python/lldbsuite/test/make/Makefile.rules
+++ b/lldb/packages/Python/lldbsuite/test/make/Makefile.rules
@@ -565,6 +565,29 @@ ifneq "$(strip $(OBJC_GNUSTEP_DIR))" ""
LDFLAGS +=-Wl,/debug:dwarf
GNUSTEP_NEEDS_DLL_COPY := 1
endif
+ # gnustep-base (Foundation) on top of the runtime, for tests of the
+ # Foundation data formatters. Its headers need the gnustep-2.x string
+ # ABI (a newer -fobjc-runtime than the bare-runtime tests use), blocks
+ # and exceptions, and on Windows the dllimport declarations.
+ ifneq "$(strip $(OBJC_GNUSTEP_BASE_DIR))" ""
+ GNUSTEP_BASE_FLAGS := -fobjc-runtime=gnustep-2.2 -fblocks -fexceptions
+ GNUSTEP_BASE_FLAGS += -fobjc-exceptions -fconstant-string-class=NSConstantString
+ GNUSTEP_BASE_FLAGS += -DGNUSTEP -DGNUSTEP_BASE_LIBRARY=1 -DGNU_RUNTIME=1
+ GNUSTEP_BASE_FLAGS += -I$(OBJC_GNUSTEP_BASE_DIR)/include
+ ifeq "$(OS)" "Windows_NT"
+ GNUSTEP_BASE_FLAGS += -DGNUSTEP_WITH_DLL
+ endif
+ # .m files are compiled with CFLAGS in this file.
+ OBJCFLAGS += $(GNUSTEP_BASE_FLAGS)
+ CFLAGS += $(GNUSTEP_BASE_FLAGS)
+ LDFLAGS +=-L$(OBJC_GNUSTEP_BASE_DIR)/lib -lgnustep-base
+ ifeq "$(OS)" "Linux"
+ LDFLAGS +=-Wl,-rpath,$(OBJC_GNUSTEP_BASE_DIR)/lib
+ endif
+ ifeq "$(OS)" "Windows_NT"
+ GNUSTEP_NEEDS_BASE_DLL_COPY := 1
+ endif
+ endif
endif
endif
@@ -763,6 +786,17 @@ objc.dll: $(OBJC_GNUSTEP_DIR)/lib/objc.dll
cp $< $@
endif
+# Likewise for gnustep-base and the runtime DLLs it depends on, which the
+# tools-windows-msvc layout keeps under bin/. Their PDBs come along: the
+# runtime's small-object class table is a hidden symbol that LLDB can only
+# see through the PDB, and tagged pointers cannot be typed without it.
+ifeq "$(GNUSTEP_NEEDS_BASE_DLL_COPY)" "1"
+all: gnustep-base-dlls
+
+gnustep-base-dlls:
+ cp $(OBJC_GNUSTEP_BASE_DIR)/bin/*.dll $(OBJC_GNUSTEP_BASE_DIR)/bin/*.pdb .
+endif
+
### Local Variables: ###
### mode:makefile ###
### End: ###
diff --git a/lldb/packages/Python/lldbsuite/test/test_categories.py b/lldb/packages/Python/lldbsuite/test/test_categories.py
index efc55d4284e24..194abb55d54cb 100644
--- a/lldb/packages/Python/lldbsuite/test/test_categories.py
+++ b/lldb/packages/Python/lldbsuite/test/test_categories.py
@@ -44,6 +44,7 @@
"pexpect": "Tests requiring the pexpect library to be available",
"objc": "Tests related to the Objective-C programming language support",
"objc-gnustep": "Tests requiring the GNUstep libobjc2 Objective-C runtime",
+ "objc-gnustep-base": "Tests requiring GNUstep's gnustep-base Foundation library",
"pyapi": "Tests related to the Python API",
"std-module": "Tests related to importing the std module",
"stresstest": "Tests related to stressing lldb limits",
diff --git a/lldb/test/API/lang/objc-gnustep/data-formatters/Makefile b/lldb/test/API/lang/objc-gnustep/data-formatters/Makefile
new file mode 100644
index 0000000000000..845553d5e3f2f
--- /dev/null
+++ b/lldb/test/API/lang/objc-gnustep/data-formatters/Makefile
@@ -0,0 +1,3 @@
+OBJC_SOURCES := main.m
+
+include Makefile.rules
diff --git a/lldb/test/API/lang/objc-gnustep/data-formatters/TestGNUstepDataFormatters.py b/lldb/test/API/lang/objc-gnustep/data-formatters/TestGNUstepDataFormatters.py
new file mode 100644
index 0000000000000..726b6889a5795
--- /dev/null
+++ b/lldb/test/API/lang/objc-gnustep/data-formatters/TestGNUstepDataFormatters.py
@@ -0,0 +1,169 @@
+"""
+Test the data formatters for gnustep-base's Foundation classes.
+"""
+
+import lldb
+from lldbsuite.test.decorators import *
+from lldbsuite.test.lldbtest import *
+from lldbsuite.test import lldbutil
+
+
+class TestGNUstepDataFormatters(TestBase):
+ def stop_at_end(self):
+ self.build()
+ lldbutil.run_to_source_breakpoint(
+ self, "// break here", lldb.SBFileSpec("main.m")
+ )
+ # Every check below goes through the dynamic type, which is what a
+ # debugger front end asks for. The setting is per target, so it has
+ # to be applied to the one run_to_source_breakpoint created.
+ self.runCmd("settings set target.prefer-dynamic-value run-target")
+
+ def test_strings(self):
+ """Every concrete string class prints its characters as @"..."."""
+ self.stop_at_end()
+ self.expect("frame variable -d run-target tinyString", substrs=['@"Hi"'])
+ self.expect(
+ "frame variable -d run-target constantString",
+ substrs=['@"A constant string literal"'],
+ )
+ self.expect(
+ "frame variable -d run-target unicodeConstant", substrs=['@"Grüße, 世界"']
+ )
+ self.expect("frame variable -d run-target emptyString", substrs=['@""'])
+ self.expect("frame variable -d run-target builtString", substrs=['@"built 42"'])
+ self.expect(
+ "frame variable -d run-target unicodeBuilt", substrs=['@"ünïcödé 7"']
+ )
+ self.expect(
+ "frame variable -d run-target mutableString", substrs=['@"mutable string"']
+ )
+
+ def test_numbers(self):
+ """Tagged and boxed numbers print their value with a type prefix."""
+ self.stop_at_end()
+ self.expect("frame variable -d run-target boolYes", substrs=["YES"])
+ self.expect("frame variable -d run-target smallInt", substrs=["(int)5"])
+ self.expect("frame variable -d run-target taggedInt", substrs=["(long)123456"])
+ self.expect("frame variable -d run-target negativeInt", substrs=["(long)-99"])
+ self.expect(
+ "frame variable -d run-target longLong",
+ substrs=["(long)9223372036854775807"],
+ )
+ self.expect(
+ "frame variable -d run-target unsignedLongLong",
+ substrs=["(long)18446744073709551615"],
+ )
+ self.expect("frame variable -d run-target floatNumber", substrs=["(float)1.5"])
+ self.expect(
+ "frame variable -d run-target doubleNumber", substrs=["(double)3.14159"]
+ )
+ self.expect("frame variable -d run-target heapDouble", substrs=["(double)0.1"])
+
+ def test_collections(self):
+ """Collections summarize their count and expose elements as children."""
+ self.stop_at_end()
+ self.expect(
+ "frame variable -d run-target emptyArray", substrs=['@"0 elements"']
+ )
+ self.expect("frame variable -d run-target fruits", substrs=['@"3 elements"'])
+ self.expect(
+ "frame variable -d run-target mutableArray", substrs=['@"4 elements"']
+ )
+ self.expect("frame variable -d run-target nested", substrs=['@"2 elements"'])
+ self.expect(
+ "frame variable -d run-target emptyDict", substrs=["0 key/value pairs"]
+ )
+ self.expect(
+ "frame variable -d run-target person", substrs=["3 key/value pairs"]
+ )
+ self.expect(
+ "frame variable -d run-target mutableDict", substrs=["4 key/value pairs"]
+ )
+ self.expect("frame variable -d run-target colors", substrs=["3 elements"])
+ self.expect("frame variable -d run-target mutableSet", substrs=["4 elements"])
+ self.expect("frame variable -d run-target counted", substrs=["2 elements"])
+
+ # Children.
+ self.expect(
+ "frame variable -d run-target fruits[0] fruits[1] fruits[2]",
+ substrs=['@"apple"', '@"banana"', '@"cherry"'],
+ )
+ self.expect("frame variable -d run-target nested[0]", substrs=['@"3 elements"'])
+ # Dictionary entries are key/value pairs; order is hash order, so
+ # look at the whole set of entries.
+ self.expect(
+ "frame variable -d run-target person[0] person[1] person[2]",
+ # @30 lands in a tagged NSSmallInt (only -1..12 are boxed
+ # singletons), which prints as a long.
+ substrs=[
+ "key = ",
+ "value = ",
+ '@"name"',
+ '@"John Doe"',
+ '@"age"',
+ "(long)30",
+ '@"skills"',
+ '@"2 elements"',
+ ],
+ ordered=False,
+ )
+ self.expect(
+ "frame variable -d run-target colors[0] colors[1] colors[2]",
+ substrs=['@"red"', '@"green"', '@"blue"'],
+ ordered=False,
+ )
+
+ def test_others(self):
+ """NSData, NSDate, NSNull, nil and a custom object."""
+ self.stop_at_end()
+ self.expect("frame variable -d run-target data", substrs=["12 bytes"])
+ self.expect(
+ "frame variable -d run-target epoch", substrs=["2001-01-01 00:00:0"]
+ )
+ self.expect(
+ "frame variable -d run-target someDate", substrs=["2023-11-14 22:13:20 UTC"]
+ )
+ self.expect("frame variable -d run-target null", substrs=["<null>"])
+ self.expect("frame variable -d run-target nilObject", substrs=["nil"])
+ # A custom class: dynamic type plus formatted ivars, and its class
+ # object is not itself presented as an instance.
+ self.expect(
+ "frame variable -d run-target anonymous",
+ substrs=["(Account *) anonymous"],
+ )
+ self.expect(
+ "frame variable -d run-target *account",
+ substrs=[
+ "owner = ",
+ '@"Jane"',
+ "balance = ",
+ "(double)1234.5",
+ "tags = ",
+ '@"2 elements"',
+ ],
+ )
+ self.expect(
+ "frame variable -d run-target *account",
+ matching=False,
+ substrs=["(Account *) isa"],
+ )
+
+ def test_api(self):
+ """The same summaries come back through the SB API."""
+ self.stop_at_end()
+ frame = self.frame()
+ greeting = frame.FindVariable("tinyString").GetDynamicValue(
+ lldb.eDynamicCanRunTarget
+ )
+ self.assertEqual(greeting.GetSummary(), '@"Hi"')
+ fruits = frame.FindVariable("fruits").GetDynamicValue(lldb.eDynamicCanRunTarget)
+ self.assertEqual(fruits.GetSummary(), '@"3 elements"')
+ self.assertEqual(fruits.GetNumChildren(), 3)
+ first = fruits.GetChildAtIndex(0).GetDynamicValue(lldb.eDynamicCanRunTarget)
+ self.assertEqual(first.GetSummary(), '@"apple"')
+ person = frame.FindVariable("person").GetDynamicValue(lldb.eDynamicCanRunTarget)
+ self.assertEqual(person.GetNumChildren(), 3)
+ pair = person.GetChildAtIndex(0)
+ self.assertEqual(pair.GetChildMemberWithName("key").GetName(), "key")
+ self.assertEqual(pair.GetNumChildren(), 2)
diff --git a/lldb/test/API/lang/objc-gnustep/data-formatters/categories b/lldb/test/API/lang/objc-gnustep/data-formatters/categories
new file mode 100644
index 0000000000000..70b14bf34d6cc
--- /dev/null
+++ b/lldb/test/API/lang/objc-gnustep/data-formatters/categories
@@ -0,0 +1 @@
+objc-gnustep-base
diff --git a/lldb/test/API/lang/objc-gnustep/data-formatters/main.m b/lldb/test/API/lang/objc-gnustep/data-formatters/main.m
new file mode 100644
index 0000000000000..f709b62c75055
--- /dev/null
+++ b/lldb/test/API/lang/objc-gnustep/data-formatters/main.m
@@ -0,0 +1,103 @@
+// Every local below is a distinct concrete gnustep-base class, so the data
+// formatters for strings in all their storage forms, boxed and tagged
+// numbers, arrays, dictionaries, sets, data, dates and NSNull each get
+// exercised, plus a custom object.
+
+#import <Foundation/Foundation.h>
+
+ at interface Account : NSObject {
+ NSString *owner;
+ NSNumber *balance;
+ NSArray *tags;
+}
+- (instancetype)initWithOwner:(NSString *)o balance:(double)b;
+ at end
+
+ at implementation Account
+- (instancetype)initWithOwner:(NSString *)o balance:(double)b {
+ if ((self = [super init])) {
+ owner = o;
+ balance = @(b);
+ tags = @[ @"premium", @"verified" ];
+ }
+ return self;
+}
+- (NSString *)description {
+ return [NSString stringWithFormat:@"<Account %@: %@>", owner, balance];
+}
+ at end
+
+int main(int argc, const char *argv[]) {
+ @autoreleasepool {
+ // Strings: each literal/operation lands in a different concrete class.
+ NSString *tinyString = @"Hi"; // GSTinyString
+ NSString *constantString = @"A constant string literal"; // NSConstantString
+ NSString *unicodeConstant = @"Grüße, 世界"; // NSConstantString, UTF-16
+ NSString *emptyString = @"";
+ NSString *builtString =
+ [NSString stringWithFormat:@"built %d", 42]; // GSCInlineString
+ NSString *unicodeBuilt =
+ [NSString stringWithFormat:@"ünïcödé %d", 7]; // GSUInlineString
+ NSMutableString *mutableString =
+ [NSMutableString stringWithString:@"mutable"]; // GSMutableString
+ [mutableString appendString:@" string"];
+
+ // Numbers: singletons, tagged small objects, and heap boxes.
+ NSNumber *boolYes = @YES; // NSBoolNumber
+ NSNumber *smallInt = @5; // NSIntNumber (singleton)
+ NSNumber *taggedInt = @123456; // NSSmallInt
+ NSNumber *negativeInt = @-99; // NSSmallInt
+ NSNumber *longLong = @9223372036854775807LL; // NSLongLongNumber
+ NSNumber *unsignedLongLong =
+ @18446744073709551615ULL; // NSUnsignedLongLongNumber
+ NSNumber *floatNumber = @1.5f; // NSSmallFloat
+ NSNumber *doubleNumber = @3.14159; // NSSmallRepeatingDouble
+ NSNumber *heapDouble = @0.1; // NSSmallExtendedDouble or NSDoubleNumber
+
+ // Collections.
+ NSArray *emptyArray = @[];
+ NSArray *fruits = @[ @"apple", @"banana", @"cherry" ]; // GSInlineArray
+ NSMutableArray *mutableArray = [NSMutableArray arrayWithArray:fruits];
+ [mutableArray addObject:@"date"]; // GSMutableArray
+ NSArray *nested = @[ fruits, @[ @1, @2 ] ];
+ NSDictionary *emptyDict = @{};
+ NSDictionary *person = @{
+ @"name" : @"John Doe",
+ @"age" : @30,
+ @"skills" : @[ @"Objective-C", @"Swift" ]
+ }; // GSDictionary
+ NSMutableDictionary *mutableDict =
+ [NSMutableDictionary dictionaryWithDictionary:person];
+ mutableDict[@"city"] = @"Berlin"; // GSMutableDictionary
+ NSSet *colors =
+ [NSSet setWithObjects:@"red", @"green", @"blue", nil]; // GSSet
+ NSMutableSet *mutableSet = [NSMutableSet setWithSet:colors];
+ [mutableSet addObject:@"yellow"]; // GSMutableSet
+ NSCountedSet *counted = [NSCountedSet setWithArray:@[ @"a", @"a", @"b" ]];
+
+ // Other value types.
+ NSData *data = [@"Hello, data!"
+ dataUsingEncoding:NSUTF8StringEncoding]; // NSDataMalloc
+ NSDate *epoch =
+ [NSDate dateWithTimeIntervalSinceReferenceDate:0]; // GSSmallDate
+ NSDate *someDate = [NSDate dateWithTimeIntervalSince1970:1700000000];
+ NSNull *null = [NSNull null];
+ NSURL *url = [NSURL URLWithString:@"https://www.gnustep.org/resources"];
+ id nilObject = nil;
+
+ // A custom class: gets dynamic type + ivars, po runs -description.
+ Account *account = [[Account alloc] initWithOwner:@"Jane" balance:1234.5];
+ id anonymous = account;
+
+ NSLog(@"%@ %@ %@ %@ %@ %@ %@", tinyString, constantString, unicodeConstant,
+ emptyString, builtString, unicodeBuilt, mutableString);
+ NSLog(@"%@ %@ %@ %@ %@ %@ %@ %@ %@", boolYes, smallInt, taggedInt,
+ negativeInt, longLong, unsignedLongLong, floatNumber, doubleNumber,
+ heapDouble);
+ NSLog(@"%@ %@ %@ %@ %@ %@ %@ %@ %@ %@", emptyArray, fruits, mutableArray,
+ nested, emptyDict, person, mutableDict, colors, mutableSet, counted);
+ NSLog(@"%@ %@ %@ %@ %@ %@ %@", data, epoch, someDate, null, url, account,
+ anonymous);
+ return nilObject != nil; // break here
+ }
+}
diff --git a/lldb/test/API/lit.cfg.py b/lldb/test/API/lit.cfg.py
index 3f32433a3beec..b7f873b3142fd 100644
--- a/lldb/test/API/lit.cfg.py
+++ b/lldb/test/API/lit.cfg.py
@@ -241,6 +241,8 @@ def delete_module_cache(path):
# against it on non-Apple platforms.
if is_configured("objc_gnustep_dir"):
dotest_cmd += ["--objc-gnustep-dir", config.objc_gnustep_dir]
+if is_configured("objc_gnustep_base_dir"):
+ dotest_cmd += ["--objc-gnustep-base-dir", config.objc_gnustep_base_dir]
# Forward ASan-specific environment variables to tests, as a test may load an
# ASan-ified dylib.
diff --git a/lldb/test/API/lit.site.cfg.py.in b/lldb/test/API/lit.site.cfg.py.in
index 30bb3733121c3..7e7524ba229f8 100644
--- a/lldb/test/API/lit.site.cfg.py.in
+++ b/lldb/test/API/lit.site.cfg.py.in
@@ -43,6 +43,7 @@ config.libcxx_libs_dir = "@LIBCXX_LIBRARY_DIR@"
config.libcxx_include_dir = "@LIBCXX_GENERATED_INCLUDE_DIR@"
config.libcxx_include_target_dir = "@LIBCXX_GENERATED_INCLUDE_TARGET_DIR@"
config.objc_gnustep_dir = "@LLDB_TEST_OBJC_GNUSTEP_DIR@"
+config.objc_gnustep_base_dir = "@LLDB_TEST_OBJC_GNUSTEP_BASE_DIR@"
config.lldb_launcher = "@LLDB_LAUNCHER@"
config.test_resource_dir = "@LLDB_TEST_RESOURCE_DIR@"
config.lldb_enable_mte = @LLDB_ENABLE_MTE@
diff --git a/lldb/test/CMakeLists.txt b/lldb/test/CMakeLists.txt
index c79f05cf85841..b2ed4d5b9c36a 100644
--- a/lldb/test/CMakeLists.txt
+++ b/lldb/test/CMakeLists.txt
@@ -83,6 +83,20 @@ elseif (LLDB_TEST_OBJC_GNUSTEP_DIR)
set(LLDB_TEST_OBJC_GNUSTEP_DIR "" CACHE PATH "Custom path to the GNUstep shared library" FORCE)
endif()
+# gnustep-base (Foundation) is a separate, larger dependency than the runtime;
+# tests of the Foundation data formatters need it and are gated on this.
+set(LLDB_TEST_OBJC_GNUSTEP_BASE_DIR "" CACHE PATH
+ "Path to a GNUstep gnustep-base install (Foundation/Foundation.h under include/) used by data-formatter tests")
+if (LLDB_TEST_OBJC_GNUSTEP_BASE_DIR)
+ if (NOT LLDB_TEST_OBJC_GNUSTEP)
+ message(SEND_ERROR "LLDB_TEST_OBJC_GNUSTEP_BASE_DIR requires LLDB_TEST_OBJC_GNUSTEP=On.")
+ endif()
+ if (NOT EXISTS "${LLDB_TEST_OBJC_GNUSTEP_BASE_DIR}/include/Foundation/Foundation.h")
+ message(SEND_ERROR "Failed to find Foundation/Foundation.h under ${LLDB_TEST_OBJC_GNUSTEP_BASE_DIR}/include. "
+ "Please check LLDB_TEST_OBJC_GNUSTEP_BASE_DIR.")
+ endif()
+endif()
+
# LLVM_BUILD_MODE is used in lit.site.cfg
if (CMAKE_CFG_INTDIR STREQUAL ".")
set(LLVM_BUILD_MODE ".")
diff --git a/lldb/unittests/Language/ObjC/CMakeLists.txt b/lldb/unittests/Language/ObjC/CMakeLists.txt
index 82cc847e1045f..851a98975e4e2 100644
--- a/lldb/unittests/Language/ObjC/CMakeLists.txt
+++ b/lldb/unittests/Language/ObjC/CMakeLists.txt
@@ -1,4 +1,5 @@
add_lldb_unittest(LanguageObjCTests
+ GNUstepFormattersTest.cpp
ObjCLanguageTest.cpp
LINK_LIBS
diff --git a/lldb/unittests/Language/ObjC/GNUstepFormattersTest.cpp b/lldb/unittests/Language/ObjC/GNUstepFormattersTest.cpp
new file mode 100644
index 0000000000000..3fa1a31e40bd5
--- /dev/null
+++ b/lldb/unittests/Language/ObjC/GNUstepFormattersTest.cpp
@@ -0,0 +1,95 @@
+//===-- GNUstepFormattersTest.cpp -----------------------------------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+#include "Plugins/Language/ObjC/GNUstepFormatters.h"
+#include "gtest/gtest.h"
+
+#include <cmath>
+#include <cstring>
+
+using namespace lldb_private::formatters;
+
+// The small-object payloads below come from libobjc2's tag layout (three low
+// bits) and gnustep-base's encodings; the constants are what the compiler and
+// runtime actually produce, checked against a live process.
+
+TEST(GNUstepFormattersTest, TinyStringDecodesClangEmittedLiteral) {
+ // clang emits @"Hello" for the gnustep-2.x ABI as this constant
+ // (CGObjCGNU.cpp: 7-bit characters from the top, 5-bit length, tag 4).
+ EXPECT_EQ(GNUstepDecodeTinyString(0x919766cde000002cULL), "Hello");
+ EXPECT_EQ(GNUstepDecodeTinyString(0x91a4000000000014ULL), "Hi");
+ EXPECT_EQ(GNUstepDecodeTinyString(0xc3c386cca000002cULL), "apple");
+ EXPECT_EQ(GNUstepDecodeTinyString(0xc587761dd8400034ULL), "banana");
+}
+
+TEST(GNUstepFormattersTest, TinyStringEmptyAndLimits) {
+ // Zero characters: just the tag.
+ EXPECT_EQ(GNUstepDecodeTinyString(0x4), "");
+ // Eight characters fill every slot; nine means eight plus a terminator.
+ uint64_t eight = 4 | (8ULL << 3);
+ for (int i = 0; i < 8; ++i)
+ eight |= static_cast<uint64_t>('a' + i) << (57 - 7 * i);
+ EXPECT_EQ(GNUstepDecodeTinyString(eight), "abcdefgh");
+ uint64_t nine = (eight & ~(0x1fULL << 3)) | (9ULL << 3);
+ EXPECT_EQ(GNUstepDecodeTinyString(nine), "abcdefgh");
+ // Not a tiny string: wrong tag, or an impossible length.
+ EXPECT_FALSE(GNUstepDecodeTinyString(0x919766cde000002dULL).has_value());
+ EXPECT_FALSE(GNUstepDecodeTinyString(4 | (10ULL << 3)).has_value());
+}
+
+TEST(GNUstepFormattersTest, SmallIntIsArithmeticallyShifted) {
+ // NSSmallInt: value << 3 | 1 (Source/NSNumber.m).
+ EXPECT_EQ(GNUstepDecodeSmallInt((42ULL << 3) | 1), 42);
+ EXPECT_EQ(GNUstepDecodeSmallInt(0x00000000000f1201ULL), 123456);
+ // Negative values keep their sign through the shift.
+ EXPECT_EQ(GNUstepDecodeSmallInt(0xfffffffffffffce9ULL), -99);
+ EXPECT_EQ(GNUstepDecodeSmallInt((static_cast<uint64_t>(-1LL) << 3) | 1), -1);
+}
+
+TEST(GNUstepFormattersTest, SmallDoublesRoundTrip) {
+ // Box a double the way boxDouble() does for the repeating (tag 3 / 5) and
+ // extended (tag 2) encodings, then check the decoders invert it.
+ auto bits_of = [](double d) {
+ uint64_t bits;
+ std::memcpy(&bits, &d, sizeof(bits));
+ return bits;
+ };
+ // Repeating: the low three mantissa bits are moved up into bits 3-5 and
+ // the tag takes their place. 1.5f as boxed by gnustep-base:
+ EXPECT_DOUBLE_EQ(GNUstepDecodeSmallRepeatingDouble(0x3ff8000000000005ULL),
+ 1.5);
+ {
+ // A double is boxable as "repeating" when its mantissa bits 3-5 equal
+ // its bits 0-2 (boxDouble in Source/NSNumber.m); the box then simply
+ // replaces bits 0-2 with the tag. Make 3.14159 satisfy that and check the
+ // decoder restores it exactly.
+ uint64_t b = bits_of(3.14159);
+ const uint64_t low = b & 7;
+ b = (b & ~0x38ULL) | (low << 3);
+ const uint64_t boxed = (b & ~7ULL) | 3;
+ EXPECT_EQ(bits_of(GNUstepDecodeSmallRepeatingDouble(boxed)), b);
+ }
+ {
+ // Extended: the low three mantissa bits are all equal to bit 3.
+ const uint64_t b = bits_of(0.1) & ~0xfULL; // clear low nibble
+ const uint64_t boxed_zero = b | 2; // bit 3 = 0 -> low bits 000
+ EXPECT_EQ(bits_of(GNUstepDecodeSmallExtendedDouble(boxed_zero)), b);
+ const uint64_t boxed_one = b | 8 | 2; // bit 3 = 1 -> low bits 111
+ EXPECT_EQ(bits_of(GNUstepDecodeSmallExtendedDouble(boxed_one)), b | 0xf);
+ }
+}
+
+TEST(GNUstepFormattersTest, SmallDateDecodesReferenceDate) {
+ // [NSDate dateWithTimeIntervalSinceReferenceDate: 0] and 1700000000 seconds
+ // after 1970 (2023-11-14 22:13:20 UTC = 721692800 seconds after 2001), as
+ // observed in a live process. The compressed encoding drops low mantissa
+ // bits, so the reference date itself comes back a couple of seconds off -
+ // gnustep-base prints the same "00:00:02".
+ EXPECT_NEAR(GNUstepDecodeSmallDate(0x0880000000000006ULL), 0.0, 3.0);
+ EXPECT_DOUBLE_EQ(GNUstepDecodeSmallDate(0x16ac10a200000006ULL), 721692800.0);
+}
>From 23f3ac3de0a01e553ac975644b325a9bf6f85fda Mon Sep 17 00:00:00 2001
From: Rob <robk at robk.dev>
Date: Mon, 17 Aug 2026 12:24:56 +0100
Subject: [PATCH 22/49] [lldb][GNUstep] Extend the data formatter and class
object tests
Cover a few more Foundation types in the formatter test, and drop the
ivars from the class object test: what it checks is that a class object
is not presented as an instance of its own class, which its root class's
ivars already demonstrate.
Assisted-by: Claude Opus 5
---
.../TestGNUstepDataFormatters.py | 22 +++++++++++++++++++
.../lang/objc-gnustep/data-formatters/main.m | 9 ++++++--
.../Shell/Expr/objc-gnustep-class-objects.m | 13 +++++------
3 files changed, 35 insertions(+), 9 deletions(-)
diff --git a/lldb/test/API/lang/objc-gnustep/data-formatters/TestGNUstepDataFormatters.py b/lldb/test/API/lang/objc-gnustep/data-formatters/TestGNUstepDataFormatters.py
index 726b6889a5795..f1772b037e7f1 100644
--- a/lldb/test/API/lang/objc-gnustep/data-formatters/TestGNUstepDataFormatters.py
+++ b/lldb/test/API/lang/objc-gnustep/data-formatters/TestGNUstepDataFormatters.py
@@ -149,6 +149,28 @@ def test_others(self):
substrs=["(Account *) isa"],
)
+ def test_step_through_dispatch(self):
+ """`step` at a message send lands in the method, not in objc_msgSend:
+ the runtime's step-through plan resolves the implementation. Covers
+ a method inside gnustep-base and one in the program."""
+ self.build()
+ target, process, thread, _ = lldbutil.run_to_source_breakpoint(
+ self, "// step here: Foundation", lldb.SBFileSpec("main.m")
+ )
+ thread.StepInto()
+ frame = thread.GetFrameAtIndex(0)
+ self.assertEqual(frame.GetFunctionName(), "-[GSArray count]")
+ self.assertEqual(frame.GetLineEntry().GetFileSpec().GetFilename(), "GSArray.m")
+ thread.StepOut()
+ # Now the send to the user class.
+ lldbutil.continue_to_source_breakpoint(
+ self, process, "// step here: user class", lldb.SBFileSpec("main.m")
+ )
+ thread.StepInto()
+ frame = thread.GetFrameAtIndex(0)
+ self.assertEqual(frame.GetFunctionName(), "-[Account description]")
+ self.assertEqual(frame.GetLineEntry().GetFileSpec().GetFilename(), "main.m")
+
def test_api(self):
"""The same summaries come back through the SB API."""
self.stop_at_end()
diff --git a/lldb/test/API/lang/objc-gnustep/data-formatters/main.m b/lldb/test/API/lang/objc-gnustep/data-formatters/main.m
index f709b62c75055..ff0456818ead8 100644
--- a/lldb/test/API/lang/objc-gnustep/data-formatters/main.m
+++ b/lldb/test/API/lang/objc-gnustep/data-formatters/main.m
@@ -89,6 +89,11 @@ int main(int argc, const char *argv[]) {
Account *account = [[Account alloc] initWithOwner:@"Jane" balance:1234.5];
id anonymous = account;
+ // Message sends to step into (through objc_msgSend): one into
+ // gnustep-base, one into this file.
+ NSUInteger fruitCount = [fruits count]; // step here: Foundation
+ NSString *accountText = [account description]; // step here: user class
+
NSLog(@"%@ %@ %@ %@ %@ %@ %@", tinyString, constantString, unicodeConstant,
emptyString, builtString, unicodeBuilt, mutableString);
NSLog(@"%@ %@ %@ %@ %@ %@ %@ %@ %@", boolYes, smallInt, taggedInt,
@@ -96,8 +101,8 @@ int main(int argc, const char *argv[]) {
heapDouble);
NSLog(@"%@ %@ %@ %@ %@ %@ %@ %@ %@ %@", emptyArray, fruits, mutableArray,
nested, emptyDict, person, mutableDict, colors, mutableSet, counted);
- NSLog(@"%@ %@ %@ %@ %@ %@ %@", data, epoch, someDate, null, url, account,
- anonymous);
+ NSLog(@"%@ %@ %@ %@ %@ %@ %@ %lu %@", data, epoch, someDate, null, url,
+ account, anonymous, (unsigned long)fruitCount, accountText);
return nilObject != nil; // break here
}
}
diff --git a/lldb/test/Shell/Expr/objc-gnustep-class-objects.m b/lldb/test/Shell/Expr/objc-gnustep-class-objects.m
index 05c135b72bc4a..32d6152b3b04b 100644
--- a/lldb/test/Shell/Expr/objc-gnustep-class-objects.m
+++ b/lldb/test/Shell/Expr/objc-gnustep-class-objects.m
@@ -29,21 +29,20 @@ + (id)new {
}
@end
- at interface Base : NSObject {
- int base_ivar;
-}
+// (No ivars beyond the root class's: clang trips an assertion compiling
+// some GNUstep classes with ivars in +assertions builds, see
+// objc-gnustep-print.m.)
+ at interface Base : NSObject
@end
@implementation Base
@end
- at interface Derived : Base {
- int derived_ivar;
-}
+ at interface Derived : Base
@end
@implementation Derived
@end
-// RUN: %lldb -b -o "b objc-gnustep-class-objects.m:54" -o "run" \
+// RUN: %lldb -b -o "b objc-gnustep-class-objects.m:53" -o "run" \
// RUN: -o "frame variable -d run-target -T object" \
// RUN: -o "frame variable -d run-target -T *object" \
// RUN: -o "frame variable -d run-target -T object->isa" \
>From 77fc14c777a68deb5032d686abb0e23b95f85112 Mon Sep 17 00:00:00 2001
From: Rob <robk at robk.dev>
Date: Mon, 17 Aug 2026 12:00:16 +0100
Subject: [PATCH 23/49] [lldb][GNUstep] Make the nil-receiver stepping check
portable
Where a step at a message to nil ends up depends on whether the runtime
was built with source line information for its hand-written dispatch
assembly: with it, LLDB has source to step into and stops there; without
it, the step returns to the caller. Neither says anything about this
plugin, so assert what actually matters - that no method was entered -
rather than a specific line in the caller.
Assisted-by: Claude Opus 5
---
lldb/test/Shell/Expr/objc-gnustep-stepping.m | 12 +++++++-----
1 file changed, 7 insertions(+), 5 deletions(-)
diff --git a/lldb/test/Shell/Expr/objc-gnustep-stepping.m b/lldb/test/Shell/Expr/objc-gnustep-stepping.m
index d7f0278d46044..3696f96f4f9a9 100644
--- a/lldb/test/Shell/Expr/objc-gnustep-stepping.m
+++ b/lldb/test/Shell/Expr/objc-gnustep-stepping.m
@@ -35,13 +35,15 @@ - (int)twice:(int)value {
// Stepping at a message send has to run through the runtime's dispatch
// function and land in the method implementation.
//
-// RUN: %lldb -b -o "b objc-gnustep-stepping.m:49" -o "run" -o "step" \
+// RUN: %lldb -b -o "b objc-gnustep-stepping.m:51" -o "run" -o "step" \
// RUN: -- %t | FileCheck %s --check-prefix=STEP_IN
//
-// A message to nil dispatches nowhere, so the step must simply move on
-// instead of trying to run to an implementation.
+// A message to nil dispatches nowhere, so the step must not try to run to an
+// implementation. Where it does land depends on whether the runtime build
+// carries source line information for its hand-written dispatch assembly, so
+// the check below only asserts that no method was entered.
//
-// RUN: %lldb -b -o "b objc-gnustep-stepping.m:51" -o "run" -o "step" \
+// RUN: %lldb -b -o "b objc-gnustep-stepping.m:53" -o "run" -o "step" \
// RUN: -- %t | FileCheck %s --check-prefix=STEP_OVER_NIL
//
int main() {
@@ -58,4 +60,4 @@ int main() {
//
// STEP_OVER_NIL: (lldb) step
// STEP_OVER_NIL: stop reason = step in
-// STEP_OVER_NIL: main at objc-gnustep-stepping.m:52
+// STEP_OVER_NIL-NOT: -[Doubler twice:]
>From b3c13b99791ac6ade87b8c42985c3d4b996aa003 Mon Sep 17 00:00:00 2001
From: Rob <robk at robk.dev>
Date: Mon, 17 Aug 2026 14:35:15 +0100
Subject: [PATCH 24/49] [lldb] Add a test for Objective-C type completion on
MSVC targets
Compile an Objective-C interface hierarchy for x86_64-pc-windows-msvc,
link it into a PE image and dump the Clang AST LLDB builds from its
DWARF. Without the guard in CompleteRecordType() this crashes in
CXXRecordDecl::calculateInheritanceModel() on a null pointer.
Requires lld so the object can be linked into an image; lldb-test has
no symbol vendor for a bare COFF object file.
Assisted-by: Claude Opus 5
---
...clang-ast-from-dwarf-objc-interface-msvc.m | 38 +++++++++++++++++++
1 file changed, 38 insertions(+)
create mode 100644 lldb/test/Shell/SymbolFile/DWARF/clang-ast-from-dwarf-objc-interface-msvc.m
diff --git a/lldb/test/Shell/SymbolFile/DWARF/clang-ast-from-dwarf-objc-interface-msvc.m b/lldb/test/Shell/SymbolFile/DWARF/clang-ast-from-dwarf-objc-interface-msvc.m
new file mode 100644
index 0000000000000..42d6f5bf7d6f9
--- /dev/null
+++ b/lldb/test/Shell/SymbolFile/DWARF/clang-ast-from-dwarf-objc-interface-msvc.m
@@ -0,0 +1,38 @@
+// Completing an Objective-C interface from DWARF must not go through the
+// Microsoft C++ ABI inheritance model code, which only applies to
+// CXXRecordDecls. Before this was guarded, every Objective-C type completion
+// for a target using the Microsoft C++ ABI crashed LLDB.
+// REQUIRES: lld, x86
+
+// RUN: %clang --target=x86_64-pc-windows-msvc -gdwarf -c -o %t.obj -- %s
+// RUN: lld-link -debug:dwarf -nodefaultlib -force:unresolved -entry:main \
+// RUN: -out:%t.exe -- %t.obj
+// RUN: lldb-test symbols -dump-clang-ast %t.exe | FileCheck %s
+
+// CHECK: ObjCInterfaceDecl {{.*}} Base
+// CHECK-NEXT: ObjCIvarDecl {{.*}} base_ivar 'int'
+// CHECK: ObjCInterfaceDecl {{.*}} Derived
+// CHECK-NEXT: super ObjCInterface {{.*}} 'Base'
+// CHECK-NEXT: ObjCIvarDecl {{.*}} derived_ivar 'int'
+
+__attribute__((objc_root_class))
+ at interface Base {
+ int base_ivar;
+}
+ at end
+
+ at implementation Base
+ at end
+
+ at interface Derived : Base {
+ int derived_ivar;
+}
+ at end
+
+ at implementation Derived
+ at end
+
+int main(void) {
+ Derived *d = 0;
+ return (int)(__SIZE_TYPE__)d;
+}
>From 61c3249416ece25a3a46306de5bd16c15dd555d8 Mon Sep 17 00:00:00 2001
From: Rob <robk at robk.dev>
Date: Tue, 18 Aug 2026 08:58:49 +0100
Subject: [PATCH 25/49] [lldb][GNUstep] Harden the ISA-to-descriptor map
Three latent problems in the class map, none reachable today but all
reachable as soon as anything resolves a class from inside the sweep:
- libobjc2 gives a metaclass the same `name` as its class (class.h), so
indexing a metaclass by name let a by-name lookup return it instead of
the class - an interface whose class methods look like instance methods
and which has no ivars. Metaclasses are still cached by ISA, just not
indexed by name.
- UpdateISAToDescriptorMapIfNeeded iterated m_pending_modules by reference
and cleared it at the end, publishing the "up to date" flags only at the
end too. A reentrant call therefore restarted the sweep and cleared the
vector the outer loop was walking. Publish the flags up front, take
ownership of the pending list, and guard against reentry.
- The unit test's ClassSize() counted the three fields after instance_size
as `long`; they are pointers. Harmless while classes are written 0x100
apart, wrong as soon as a test lays out an ivar or method list. Replace
it with the real sizeof(struct objc_class), add the ivars/methods/dtable
offsets, and pin all of them with a test against the values libobjc2
hard-codes itself: DTABLE_OFFSET of 64/56/32 (asmconstants.h, enforced
by a _Static_assert in dtable.c) and sizeof 136/128/68.
Assisted-by: Claude Opus 5
---
.../GNUstepObjCRuntime/GNUstepObjCRuntime.cpp | 32 +++++++++++---
.../GNUstepObjCRuntime/GNUstepObjCRuntime.h | 3 ++
.../GNUstepObjCClassDescriptorTest.cpp | 43 ++++++++++++++++++-
3 files changed, 71 insertions(+), 7 deletions(-)
diff --git a/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCRuntime.cpp b/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCRuntime.cpp
index fc5a937603cc3..8fca1c369c397 100644
--- a/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCRuntime.cpp
+++ b/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCRuntime.cpp
@@ -22,6 +22,7 @@
#include "lldb/Expression/UtilityFunction.h"
#include "lldb/Symbol/Symbol.h"
#include "lldb/Symbol/SymbolContext.h"
+#include "lldb/Symbol/Symtab.h"
#include "lldb/Symbol/Type.h"
#include "lldb/Target/ABI.h"
#include "lldb/Target/ExecutionContext.h"
@@ -38,11 +39,13 @@
#include "lldb/Utility/StructuredData.h"
#include "lldb/ValueObject/ValueObject.h"
+#include "llvm/ADT/STLExtras.h"
#include "llvm/IR/IRBuilder.h"
#include "llvm/IR/LegacyPassManager.h"
#include "llvm/IR/Module.h"
#include "llvm/Pass.h"
#include "llvm/Support/Regex.h"
+#include "llvm/Support/SaveAndRestore.h"
using namespace lldb;
using namespace lldb_private;
@@ -773,11 +776,23 @@ void GNUstepObjCRuntime::UpdateISAToDescriptorMapIfNeeded() {
return;
}
+ // Anything reached from the sweep that resolves a class by ISA or by name
+ // lands back here, so publish the "up to date" state and take ownership of
+ // the pending list up front: a reentrant call must not restart the sweep
+ // and clear the vector the outer loop is walking.
+ if (m_updating_isa_map)
+ return;
+ llvm::SaveAndRestore<bool> updating(m_updating_isa_map, true);
+ m_isa_map_dirty = false;
+ m_isa_to_descriptor_stop_id = stop_id;
+ std::vector<ModuleSP> pending;
+ pending.swap(m_pending_modules);
+
// The first update has to look at everything already loaded; afterwards only
// the modules that arrived since need scanning, so a dlopen does not re-walk
// every symbol table in the process.
if (m_swept_all_modules) {
- for (const ModuleSP &module_sp : m_pending_modules)
+ for (const ModuleSP &module_sp : pending)
AddClassesFromModule(module_sp);
} else {
const ModuleList &images = GetTargetRef().GetImages();
@@ -787,10 +802,6 @@ void GNUstepObjCRuntime::UpdateISAToDescriptorMapIfNeeded() {
AddClassesFromModule(images.GetModuleAtIndex(i));
m_swept_all_modules = true;
}
-
- m_pending_modules.clear();
- m_isa_map_dirty = false;
- m_isa_to_descriptor_stop_id = stop_id;
}
bool GNUstepObjCRuntime::IsRuntimeInternalAddress(lldb::addr_t addr) {
@@ -876,7 +887,16 @@ GNUstepObjCRuntime::GetClassDescriptorFromISA(ObjCISA isa) {
m_process->shared_from_this(), isa);
if (!descriptor_sp->IsValid())
return ClassDescriptorSP();
- AddClass(isa, descriptor_sp, descriptor_sp->GetClassName().GetCString());
+ // libobjc2 gives a metaclass the same `name` as its class, so indexing one
+ // by name would let a by-name lookup (GetISA, and through it anything that
+ // resolves a class by name) hand back the metaclass: an interface whose
+ // class methods look like instance methods and which has no ivars. Cache
+ // metaclasses by ISA all the same, so they are not re-parsed on every
+ // lookup.
+ if (descriptor_sp->IsMetaclass())
+ AddClass(isa, descriptor_sp);
+ else
+ AddClass(isa, descriptor_sp, descriptor_sp->GetClassName().GetCString());
return descriptor_sp;
}
diff --git a/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCRuntime.h b/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCRuntime.h
index dd64a8f0d9044..48f68cac12060 100644
--- a/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCRuntime.h
+++ b/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCRuntime.h
@@ -204,6 +204,9 @@ class GNUstepObjCRuntime : public lldb_private::ObjCLanguageRuntime {
/// Set when new modules arrive; cleared once the ISA-to-descriptor map has
/// been refreshed, so the symbol sweep only reruns after module changes.
bool m_isa_map_dirty = true;
+
+ /// Guards against a sweep that resolves a class re-entering the sweep.
+ bool m_updating_isa_map = false;
};
} // namespace lldb_private
diff --git a/lldb/unittests/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCClassDescriptorTest.cpp b/lldb/unittests/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCClassDescriptorTest.cpp
index bc3ace1cc96f7..c19b783a57632 100644
--- a/lldb/unittests/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCClassDescriptorTest.cpp
+++ b/lldb/unittests/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCClassDescriptorTest.cpp
@@ -129,7 +129,28 @@ class GNUstepClassDescriptorTest : public ::testing::TestWithParam<DataModel> {
addr_t InstanceSizeOffset() const {
return 3 * PointerSize() + 2 * LongSize();
}
- addr_t ClassSize() const { return 3 * PointerSize() + 6 * LongSize(); }
+
+ static addr_t AlignUp(addr_t value, uint32_t alignment) {
+ return (value + alignment - 1) & ~static_cast<addr_t>(alignment - 1);
+ }
+
+ /// `ivars` is the first field after the three `long`s, so it picks up
+ /// whatever tail padding the target's alignment requires.
+ addr_t IvarsOffset() const {
+ return AlignUp(3 * PointerSize() + 3 * LongSize(), PointerSize());
+ }
+ addr_t MethodsOffset() const { return IvarsOffset() + PointerSize(); }
+
+ /// sizeof(struct objc_class): the three leading pointers, three `long`s,
+ /// then nine pointers through `extra_data`, `abi_version` (a `long`), and
+ /// `properties`. Cross-check: `dtable` lands at libobjc2's DTABLE_OFFSET
+ /// of 64 / 56 / 32 (asmconstants.h), which DTableOffset() asserts.
+ addr_t DTableOffset() const { return IvarsOffset() + 2 * PointerSize(); }
+ addr_t ClassSize() const {
+ return AlignUp(IvarsOffset() + 9 * PointerSize() + LongSize(),
+ PointerSize()) +
+ PointerSize();
+ }
/// Lays out a class structure, returning its address.
addr_t WriteClass(addr_t addr, addr_t metaclass, addr_t superclass,
@@ -168,6 +189,26 @@ constexpr addr_t g_super_name_addr = FakeProcess::g_base_addr + 0x480;
/// A well-formed class parses on every data model. This is what proves the
/// field offsets track the target's `long` size rather than its pointer size:
/// on Windows the instance size sits four bytes earlier than on Linux.
+// libobjc2 hard-codes the offset of `dtable` per data model in
+// asmconstants.h and pins it with a _Static_assert in dtable.c, so it is the
+// one field whose position is guaranteed by the runtime itself. Checking the
+// layout helpers against it catches a wrong `long` width, which would
+// otherwise silently shift every field after the class name.
+TEST_P(GNUstepClassDescriptorTest, LayoutMatchesLibobjc2) {
+ const bool is_lp64 = PointerSize() == 8 && LongSize() == 8;
+ const bool is_llp64 = PointerSize() == 8 && LongSize() == 4;
+ const addr_t expected_dtable = is_lp64 ? 64 : (is_llp64 ? 56 : 32);
+ const addr_t expected_size = is_lp64 ? 136 : (is_llp64 ? 128 : 68);
+
+ EXPECT_EQ(DTableOffset(), expected_dtable);
+ EXPECT_EQ(ClassSize(), expected_size);
+ // The fields this descriptor actually reads, for the same three models.
+ EXPECT_EQ(InfoOffset(), is_lp64 ? 32u : (is_llp64 ? 28u : 16u));
+ EXPECT_EQ(InstanceSizeOffset(), is_lp64 ? 40u : (is_llp64 ? 32u : 20u));
+ EXPECT_EQ(IvarsOffset(), is_lp64 ? 48u : (is_llp64 ? 40u : 24u));
+ EXPECT_EQ(MethodsOffset(), is_lp64 ? 56u : (is_llp64 ? 48u : 28u));
+}
+
TEST_P(GNUstepClassDescriptorTest, ParsesWellFormedClass) {
FakeProcess &process = GetProcess();
process.WriteCString(g_name_addr, "Derived");
>From cdb7990fe9bd673904b320f46d5da5aaa4e4968f Mon Sep 17 00:00:00 2001
From: Rob <robk at robk.dev>
Date: Tue, 18 Aug 2026 08:59:45 +0100
Subject: [PATCH 26/49] [lldb][GNUstep] Implement `po` without depending on
gnustep-base
`po` resolved gnustep-base's `_NSPrintForDebugger` by symbol and called it.
gnustep-base defines that hook (Source/NSDebug.m) but does not dllexport it
from the MSVC DLL, so on Windows there was nothing to call and the test
suite carried a shim.m defining it in the test executable.
The hook's entire body is:
if (object && [object respondsToSelector: @selector(description)])
return [[object description] UTF8String];
return NULL;
and class_respondsToSelector, objc_msg_lookup, sel_registerName and
object_getClass are all OBJC_PUBLIC in libobjc2. So build that as a utility
function instead of resolving the symbol. This is not an approximation of
_NSPrintForDebugger - it is the same function.
Doing it in the runtime rather than through the symbol also means `po` no
longer differs by platform: the symbol is exported from libgnustep-base.so
but not from the Windows DLL, so keeping it as a "fast path" would have left
Linux and Windows on different code paths computing the same thing.
Two things fall out. `po` now works against a bare libobjc2 with no
Foundation at all, for any object implementing -description; and the
resolved-address cache that was never invalidated on module load - so `po`
stayed broken after a dlopen of gnustep-base - is gone rather than fixed.
object_getClass is used rather than a raw isa read so tagged pointers
dispatch correctly, which the added tests cover.
shim.m is deleted. The Shell test's hermetic _NSPrintForDebugger stand-in is
replaced by a real -description/-UTF8String pair, so it now exercises the
path that ships.
Assisted-by: Claude Opus 5
---
.../GNUstepObjCRuntime/GNUstepObjCRuntime.cpp | 157 +++++++++++++-----
.../GNUstepObjCRuntime/GNUstepObjCRuntime.h | 22 ++-
.../TestGNUstepDataFormatters.py | 31 ++++
lldb/test/Shell/Expr/objc-gnustep-print.m | 39 ++++-
4 files changed, 192 insertions(+), 57 deletions(-)
diff --git a/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCRuntime.cpp b/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCRuntime.cpp
index 8fca1c369c397..7546450588982 100644
--- a/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCRuntime.cpp
+++ b/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCRuntime.cpp
@@ -248,20 +248,102 @@ GNUstepObjCRuntime::GNUstepObjCRuntime(Process *process)
ReadObjCLibraryIfNeeded(process->GetTarget().GetImages());
}
-Address *GNUstepObjCRuntime::GetPrintForDebuggerAddr() {
- if (!m_print_for_debugger_addr_up) {
- SymbolContextList sc_list;
- GetTargetRef().GetImages().FindSymbolsWithNameAndType(
- ConstString("_NSPrintForDebugger"), eSymbolTypeCode, sc_list);
- for (const SymbolContext &sc : sc_list) {
- if (!sc.symbol)
- continue;
- m_print_for_debugger_addr_up =
- std::make_unique<Address>(sc.symbol->GetAddress());
- break;
- }
+FunctionCaller *
+GNUstepObjCRuntime::GetObjectDescriptionCaller(ExecutionContext &exe_ctx) {
+ // gnustep-base's _NSPrintForDebugger (Source/NSDebug.m) is exactly:
+ //
+ // if (object && [object respondsToSelector: @selector(description)])
+ // return [[object description] UTF8String];
+ // return NULL;
+ //
+ // Every runtime function it needs is OBJC_PUBLIC in libobjc2, so rather
+ // than depending on Foundation exporting that hook - it does from
+ // libgnustep-base.so but not from the MSVC gnustep-base DLL, which is why
+ // this used to need a shim on Windows - reproduce it here. As a bonus this
+ // makes `po` work against a bare libobjc2 with no Foundation at all, for
+ // any object implementing -description.
+ //
+ // object_getClass() rather than a raw isa read, because it also resolves
+ // tagged pointers (classForObject) and skips libobjc2's hidden classes.
+ static const char *g_description_name = "$__lldb_gnustep_object_description";
+ static const char *g_description_code =
+ "void *object_getClass(void *object);\n"
+ "void *sel_registerName(const char *name);\n"
+ "signed char class_respondsToSelector(void *cls, void *sel);\n"
+ "void *objc_msg_lookup(void *receiver, void *selector);\n"
+ "\n"
+ "const char *$__lldb_gnustep_object_description(void *object) {\n"
+ " if (!object)\n"
+ " return 0;\n"
+ " void *description_sel = sel_registerName(\"description\");\n"
+ " if (!class_respondsToSelector(object_getClass(object), "
+ "description_sel))\n"
+ " return 0;\n"
+ " void *(*description_imp)(void *, void *) =\n"
+ " (void *(*)(void *, void *))objc_msg_lookup(object, "
+ "description_sel);\n"
+ " if (!description_imp)\n"
+ " return 0;\n"
+ " void *description = description_imp(object, description_sel);\n"
+ " if (!description)\n"
+ " return 0;\n"
+ " void *utf8_sel = sel_registerName(\"UTF8String\");\n"
+ " if (!class_respondsToSelector(object_getClass(description), "
+ "utf8_sel))\n"
+ " return 0;\n"
+ " const char *(*utf8_imp)(void *, void *) =\n"
+ " (const char *(*)(void *, void *))objc_msg_lookup(description, "
+ "utf8_sel);\n"
+ " if (!utf8_imp)\n"
+ " return 0;\n"
+ " return utf8_imp(description, utf8_sel);\n"
+ "}\n";
+
+ std::lock_guard<std::mutex> guard(m_description_mutex);
+ if (m_description_caller)
+ return m_description_caller;
+ // Don't pay for compiling the wrapper again once it is known not to work
+ // in this process.
+ if (m_description_failed)
+ return nullptr;
+
+ Log *log = GetLog(LLDBLog::Expressions);
+
+ auto utility_fn_or_error = exe_ctx.GetTargetRef().CreateUtilityFunction(
+ g_description_code, g_description_name, eLanguageTypeC, exe_ctx);
+ if (!utility_fn_or_error) {
+ LLDB_LOG_ERROR(log, utility_fn_or_error.takeError(),
+ "failed to build object description utility: {0}");
+ m_description_failed = true;
+ return nullptr;
+ }
+ m_description_utility_up = std::move(*utility_fn_or_error);
+
+ TypeSystemClangSP scratch_ts_sp =
+ ScratchTypeSystemClang::GetForTarget(GetTargetRef());
+ if (!scratch_ts_sp) {
+ m_description_failed = true;
+ return nullptr;
}
- return m_print_for_debugger_addr_up.get();
+
+ Value void_ptr_value;
+ void_ptr_value.SetValueType(Value::ValueType::Scalar);
+ void_ptr_value.SetCompilerType(
+ scratch_ts_sp->GetBasicType(eBasicTypeVoid).GetPointerType());
+ ValueList args;
+ args.PushValue(void_ptr_value);
+
+ Status error;
+ m_description_caller = m_description_utility_up->MakeFunctionCaller(
+ scratch_ts_sp->GetCStringType(true), args, exe_ctx.GetThreadSP(), error);
+ if (error.Fail()) {
+ LLDB_LOG(log, "failed to make object description caller: {0}",
+ error.AsCString());
+ m_description_caller = nullptr;
+ m_description_failed = true;
+ return nullptr;
+ }
+ return m_description_caller;
}
llvm::Error GNUstepObjCRuntime::GetObjectDescription(Stream &str,
@@ -293,13 +375,6 @@ llvm::Error GNUstepObjCRuntime::GetObjectDescription(Stream &str,
llvm::Error
GNUstepObjCRuntime::GetObjectDescription(Stream &strm, Value &value,
ExecutionContextScope *exe_scope) {
- // The libobjc2 runtime alone cannot describe objects; the hook lives in
- // gnustep-base (Foundation), just like on Darwin.
- Address *function_address = GetPrintForDebuggerAddr();
- if (!function_address)
- return llvm::createStringError(
- "gnustep-base is not loaded: _NSPrintForDebugger not found");
-
ExecutionContext exe_ctx;
exe_scope->CalculateExecutionContext(exe_ctx);
Process *process = exe_ctx.GetProcessPtr();
@@ -339,25 +414,18 @@ GNUstepObjCRuntime::GetObjectDescription(Stream &strm, Value &value,
DiagnosticManager diagnostics;
lldb::addr_t wrapper_struct_addr = LLDB_INVALID_ADDRESS;
- if (!m_print_object_caller_up) {
- Status error;
- m_print_object_caller_up.reset(
- exe_scope->CalculateTarget()->GetFunctionCallerForLanguage(
- eLanguageTypeC, return_compiler_type, *function_address,
- arg_value_list, "gnustep-object-description", error));
- if (error.Fail()) {
- m_print_object_caller_up.reset();
- return llvm::createStringError(
- llvm::Twine("could not get function runner to call "
- "_NSPrintForDebugger: ") +
- error.AsCString());
- }
- m_print_object_caller_up->InsertFunction(exe_ctx, wrapper_struct_addr,
- diagnostics);
- } else {
- m_print_object_caller_up->WriteFunctionArguments(
- exe_ctx, wrapper_struct_addr, arg_value_list, diagnostics);
- }
+ // Building the caller needs a thread, so this has to follow the frame
+ // selection above. MakeFunctionCaller already compiled the wrapper into
+ // the inferior, so only a fresh argument struct is needed per call.
+ FunctionCaller *caller = GetObjectDescriptionCaller(exe_ctx);
+ if (!caller)
+ return llvm::createStringError(
+ "could not build the object description function");
+
+ if (!caller->WriteFunctionArguments(exe_ctx, wrapper_struct_addr,
+ arg_value_list, diagnostics))
+ return llvm::createStringError(
+ "could not write the object description arguments");
EvaluateExpressionOptions options;
options.SetUnwindOnError(true);
@@ -367,21 +435,26 @@ GNUstepObjCRuntime::GetObjectDescription(Stream &strm, Value &value,
options.SetTimeout(process->GetUtilityExpressionTimeout());
options.SetIsForUtilityExpr(true);
- ExpressionResults results = m_print_object_caller_up->ExecuteFunction(
+ ExpressionResults results = caller->ExecuteFunction(
exe_ctx, &wrapper_struct_addr, options, diagnostics, ret);
if (results != eExpressionCompleted)
return llvm::createStringError(
- "could not evaluate _NSPrintForDebugger in the inferior");
+ "could not evaluate the object description in the inferior");
addr_t result_ptr = ret.GetScalar().ULongLong(LLDB_INVALID_ADDRESS);
if (result_ptr == 0 || result_ptr == LLDB_INVALID_ADDRESS)
return llvm::createStringError("object returned no description");
+ // -description returns inferior memory, so a string that never terminates
+ // would otherwise be read until the host runs out of it. No real
+ // description approaches this.
+ static constexpr size_t g_max_description_length = 1024 * 1024;
+
char buf[512];
size_t cstr_len = 0;
size_t full_buffer_len = sizeof(buf) - 1;
size_t curr_len = full_buffer_len;
- while (curr_len == full_buffer_len) {
+ while (curr_len == full_buffer_len && cstr_len < g_max_description_length) {
Status error;
curr_len = process->ReadCStringFromMemory(result_ptr + cstr_len, buf,
sizeof(buf), error);
diff --git a/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCRuntime.h b/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCRuntime.h
index 48f68cac12060..d00ed899656f2 100644
--- a/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCRuntime.h
+++ b/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCRuntime.h
@@ -169,16 +169,24 @@ class GNUstepObjCRuntime : public lldb_private::ObjCLanguageRuntime {
/// symbol is not named after the class).
lldb::TypeSP LookupClassTypeInDebugInfo(ConstString class_name);
- /// Address of gnustep-base's `const char *_NSPrintForDebugger(id)`, the
- /// same debugger hook AppleObjCRuntime uses. Resolved lazily; nullptr when
- /// gnustep-base is not loaded in the inferior.
- Address *GetPrintForDebuggerAddr();
+ /// Lazily-built FunctionCaller for a utility function that reproduces
+ /// gnustep-base's `_NSPrintForDebugger` (NSDebug.m) using nothing but
+ /// libobjc2's exported API, so `po` works whether or not Foundation is
+ /// loaded and regardless of whether gnustep-base exports that hook - it
+ /// does on ELF but not from the MSVC DLL. Returns nullptr on failure; the
+ /// caller is owned by the utility function and stays valid for the
+ /// runtime's life.
+ FunctionCaller *GetObjectDescriptionCaller(ExecutionContext &exe_ctx);
lldb::ModuleSP m_objc_module_sp;
- std::unique_ptr<Address> m_print_for_debugger_addr_up;
-
- std::unique_ptr<FunctionCaller> m_print_object_caller_up;
+ /// Utility function wrapping the -description/-UTF8String pair; owns
+ /// m_description_caller. Guarded by m_description_mutex, which also
+ /// latches a failed build so it is not retried on every `po`.
+ std::mutex m_description_mutex;
+ std::unique_ptr<UtilityFunction> m_description_utility_up;
+ FunctionCaller *m_description_caller = nullptr;
+ bool m_description_failed = false;
/// Utility function wrapping objc_msg_lookup; owns m_msg_lookup_caller.
/// Guarded by m_msg_lookup_mutex, which also latches a failed build so it
diff --git a/lldb/test/API/lang/objc-gnustep/data-formatters/TestGNUstepDataFormatters.py b/lldb/test/API/lang/objc-gnustep/data-formatters/TestGNUstepDataFormatters.py
index f1772b037e7f1..6c71fa24ed360 100644
--- a/lldb/test/API/lang/objc-gnustep/data-formatters/TestGNUstepDataFormatters.py
+++ b/lldb/test/API/lang/objc-gnustep/data-formatters/TestGNUstepDataFormatters.py
@@ -189,3 +189,34 @@ def test_api(self):
pair = person.GetChildAtIndex(0)
self.assertEqual(pair.GetChildMemberWithName("key").GetName(), "key")
self.assertEqual(pair.GetNumChildren(), 2)
+
+ def test_po(self):
+ """`po` describes objects without gnustep-base exporting a hook.
+
+ LLDB reproduces _NSPrintForDebugger (Source/NSDebug.m) from libobjc2's
+ exported runtime API rather than resolving that symbol, which
+ gnustep-base defines but does not dllexport on Windows. Nothing in
+ this directory supplies it, so these assertions fail if that
+ dependency ever comes back.
+ """
+ self.stop_at_end()
+ # A user class reached through its own -description.
+ self.expect("po account", substrs=["<Account Jane: 1234.5>"])
+ # Tagged pointers: the receiver's class comes from the pointer bits,
+ # not from a first-word read, so these only work if the description
+ # call dispatches through the runtime rather than dereferencing.
+ self.expect("po tinyString", substrs=["Hi"])
+ self.expect("po taggedInt", substrs=["123456"])
+ # A container, whose description recurses through its elements.
+ self.expect("po fruits", substrs=["apple", "banana", "cherry"])
+ # nil must not run anything in the inferior.
+ self.expect("po nilObject", substrs=["nil"])
+ # None of the above may have fallen back to `p`.
+ for expr in ("account", "tinyString", "taggedInt", "fruits"):
+ self.expect("po " + expr, matching=False, substrs=["was unsuccessful"])
+
+ def test_po_is_repeatable(self):
+ """Repeated `po` on the same object keeps working."""
+ self.stop_at_end()
+ for _ in range(3):
+ self.expect("po account", substrs=["<Account Jane: 1234.5>"])
diff --git a/lldb/test/Shell/Expr/objc-gnustep-print.m b/lldb/test/Shell/Expr/objc-gnustep-print.m
index 873ac3092df1f..04b9f693f8d0e 100644
--- a/lldb/test/Shell/Expr/objc-gnustep-print.m
+++ b/lldb/test/Shell/Expr/objc-gnustep-print.m
@@ -105,20 +105,43 @@ int main() {
return 0;
}
-// LLDB resolves `_NSPrintForDebugger` by symbol in any loaded module and
-// calls it to implement `po`. In a full GNUstep environment gnustep-base
-// provides it; this hermetic stand-in exercises the same machinery.
-const char *_NSPrintForDebugger(id object) {
- if (!object)
- return 0;
- return object_getClassName(object);
+// `po` sends -description and then -UTF8String to the result, which is what
+// gnustep-base's _NSPrintForDebugger does (Source/NSDebug.m), reached
+// through libobjc2's exported runtime API rather than through that symbol.
+// Nothing below needs Foundation, so this also covers `po` against a bare
+// runtime - a configuration where gnustep-base's hook does not exist at all.
+ at interface Str : NSObject {
+ const char *_bytes;
}
++ (id)withBytes:(const char *)bytes;
+- (const char *)UTF8String;
+ at end
+ at implementation Str
++ (id)withBytes:(const char *)bytes {
+ Str *str = [Str new];
+ str->_bytes = bytes;
+ return str;
+}
+- (const char *)UTF8String {
+ return _bytes;
+}
+ at end
+
+ at interface TestObj (Description)
+- (id)description;
+ at end
+ at implementation TestObj (Description)
+- (id)description {
+ return [Str withBytes:"<TestObj: described>"];
+}
+ at end
// RUN: %lldb -b -o "b objc-gnustep-print.m:105" -o "run" -o "po t" \
// RUN: -- %t | FileCheck %s --check-prefix=PO
//
// PO: (lldb) po t
-// PO: TestObj
+// PO: <TestObj: described>
+// PO-NOT: warning: `po` was unsuccessful
// Stepping at a message send goes through the objc_msgSend trampoline into
// the method implementation.
>From e4d493405d032d6d7223fb8fb2c5bdefb6403716 Mon Sep 17 00:00:00 2001
From: Rob <robk at robk.dev>
Date: Tue, 18 Aug 2026 09:02:04 +0100
Subject: [PATCH 27/49] [lldb][GNUstep] Step over a message to nil instead of
into the runtime
A message to nil dispatches nowhere, so GetStepThroughTrampolinePlan had
nothing to run to and declined to provide a plan at all. That leaves the
thread stopped on the dispatch function's first instruction. When the
runtime carries no line information for its hand-written assembly this is
invisible - LLDB steps back out on its own because there is no source - but
a libobjc2 built from source does have it, and the user lands in
objc_msgSend.x86-64.S with no way to tell why.
Return a step-out plan instead, which is where a nil send returns to and
what the user asked to step over.
Only the nil case is affected. A non-nil send already matched the dispatch
entry point on its first instruction and ran to the implementation, verified
against a libobjc2 built with debug info covering objc_msgSend.S; widening
that match was considered and rejected, since the receiver and selector are
read from argument registers and are only valid at the entry.
The existing test could only assert that no method was entered, because
where a nil send landed depended on how the runtime was built. It now
asserts the frame's source location, which is what distinguishes "back in
the caller" from "stranded in the runtime" - the function name alone looks
plausible either way.
Assisted-by: Claude Opus 5
---
.../GNUstepObjCRuntime/GNUstepObjCRuntime.cpp | 22 ++++++++++++++++---
lldb/test/Shell/Expr/objc-gnustep-stepping.m | 20 ++++++++++-------
2 files changed, 31 insertions(+), 11 deletions(-)
diff --git a/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCRuntime.cpp b/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCRuntime.cpp
index 7546450588982..ae8cd9708c214 100644
--- a/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCRuntime.cpp
+++ b/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCRuntime.cpp
@@ -31,6 +31,7 @@
#include "lldb/Target/Target.h"
#include "lldb/Target/Thread.h"
#include "lldb/Target/ThreadPlanRunToAddress.h"
+#include "lldb/Target/ThreadPlanStepOut.h"
#include "lldb/Utility/ArchSpec.h"
#include "lldb/Utility/ConstString.h"
#include "lldb/Utility/LLDBLog.h"
@@ -690,9 +691,24 @@ GNUstepObjCRuntime::GetStepThroughTrampolinePlan(Thread &thread,
return {};
}
- // A message to nil does not dispatch anywhere.
- if (receiver == 0 || receiver == LLDB_INVALID_ADDRESS)
- return {};
+ // A message to nil does not dispatch anywhere, so there is no
+ // implementation to run to. Declining to provide a plan at all would leave
+ // the thread stopped on the dispatch function's first instruction: harmless
+ // when the runtime carries no line information for its hand-written
+ // assembly, but with a runtime built from source it drops the user into
+ // objc_msgSend.S. Step back out to the sender instead, which is where a nil
+ // send returns to and what the user asked to step over.
+ if (receiver == 0 || receiver == LLDB_INVALID_ADDRESS) {
+ const bool continue_to_next_branch = true;
+ const bool gather_return_value = false;
+ auto step_out_sp = std::make_shared<ThreadPlanStepOut>(
+ thread, stop_others, eVoteNo, eVoteNoOpinion, /*frame_idx=*/0,
+ continue_to_next_branch, gather_return_value);
+ // Nothing further should be stepped through on the way out; the enclosing
+ // step-in plan decides where to stop once we are back in the sender.
+ step_out_sp->ClearShouldStopHereCallbacks();
+ return step_out_sp;
+ }
// Consult the method cache before running anything in the inferior.
// Tagged pointers skip the cache: their ISA is not the object's first word.
diff --git a/lldb/test/Shell/Expr/objc-gnustep-stepping.m b/lldb/test/Shell/Expr/objc-gnustep-stepping.m
index 3696f96f4f9a9..17cfde7b05bb3 100644
--- a/lldb/test/Shell/Expr/objc-gnustep-stepping.m
+++ b/lldb/test/Shell/Expr/objc-gnustep-stepping.m
@@ -35,22 +35,24 @@ - (int)twice:(int)value {
// Stepping at a message send has to run through the runtime's dispatch
// function and land in the method implementation.
//
-// RUN: %lldb -b -o "b objc-gnustep-stepping.m:51" -o "run" -o "step" \
+// RUN: %lldb -b -o "b objc-gnustep-stepping.m:53" -o "run" -o "step" \
// RUN: -- %t | FileCheck %s --check-prefix=STEP_IN
//
// A message to nil dispatches nowhere, so the step must not try to run to an
-// implementation. Where it does land depends on whether the runtime build
-// carries source line information for its hand-written dispatch assembly, so
-// the check below only asserts that no method was entered.
+// implementation - and must not strand the user in the runtime's dispatch
+// assembly either. A runtime built from source has line information for that
+// assembly, so without stepping back out the debugger would stop in
+// objc_msgSend.S; asserting on the frame's source location is what catches
+// that, since the function name alone looks plausible either way.
//
-// RUN: %lldb -b -o "b objc-gnustep-stepping.m:53" -o "run" -o "step" \
-// RUN: -- %t | FileCheck %s --check-prefix=STEP_OVER_NIL
+// RUN: %lldb -b -o "b objc-gnustep-stepping.m:55" -o "run" -o "step" \
+// RUN: -o "frame info" -- %t | FileCheck %s --check-prefix=STEP_OVER_NIL
//
int main() {
Doubler *doubler = [Doubler new];
- int value = [doubler twice:21];
+ int value = [doubler twice:21]; // line 53: STEP_IN breaks here
Doubler *nothing = (Doubler *)0;
- int none = [nothing twice:1];
+ int none = [nothing twice:1]; // line 55: STEP_OVER_NIL breaks here
return value + none;
}
//
@@ -61,3 +63,5 @@ int main() {
// STEP_OVER_NIL: (lldb) step
// STEP_OVER_NIL: stop reason = step in
// STEP_OVER_NIL-NOT: -[Doubler twice:]
+// STEP_OVER_NIL: (lldb) frame info
+// STEP_OVER_NIL: frame #0: {{.*}}main at objc-gnustep-stepping.m:{{[0-9]+}}
>From 21d6c8a81cb94f6cae7f96b86c128467ff925576 Mon Sep 17 00:00:00 2001
From: Rob <robk at robk.dev>
Date: Tue, 18 Aug 2026 09:09:38 +0100
Subject: [PATCH 28/49] [lldb][GNUstep] Add Objective-C exception breakpoints
and exception objects
The runtime had a throw-site resolver but nothing wired to it, so
`breakpoint set -E objc` produced a breakpoint that stopped with no
explanation, `thread exception` printed nothing, and lldb-dap's exception
view was empty.
Add the pieces around it:
- A frame recognizer on objc_exception_throw. `void
objc_exception_throw(id)` is the entry point on all three of libobjc2's
exception back-ends, so argument 0 is the thrown object regardless of
whether the platform unwinds via Itanium, __cxa_* over SEH, or native MSVC
exceptions. This is what produces both "stop reason = hit Objective-C
exception" and the synthesized `exception` argument, and it is what
Thread::GetCurrentException consults first.
- SetExceptionBreakpoints/Clear/AreSet/ExplainStop over that resolver.
- GetBreakpointExceptionPrecondition, which was simply not passed to
RegisterPlugin, so `breakpoint set -E objc -O <class>` silently did
nothing.
- Catch breakpoints, which Apple's runtime still has a FIXME for. libobjc2
exports objc_begin_catch wherever exceptions unwind through the Itanium
ABI; the MSVC build raises a native SEH exception instead and catch is a
__CxxFrameHandler3 funclet with no symbol to break on. The resolver asks
the loaded runtime module rather than the target triple, so a MinGW build
on Windows still gets catch breakpoints.
- GetExceptionObjectForThread. At the throw itself the recognizer already
has the object, which works everywhere; a stop further into the unwind
falls back to the C++ runtime and confirms the thrown object really is an
NSException before claiming it, since an ObjC exception raised through the
Itanium ABI is otherwise indistinguishable from a C++ one.
The recognizer is registered without a module filter because the runtime
library's file name differs by platform (libobjc.so.4.6, objc.dll); an empty
module ConstString matches any module.
Assisted-by: Claude Opus 5
---
.../GNUstepObjCRuntime/GNUstepObjCRuntime.cpp | 211 +++++++++++++++++-
.../GNUstepObjCRuntime/GNUstepObjCRuntime.h | 24 ++
.../test/Shell/Expr/objc-gnustep-exceptions.m | 98 ++++++++
3 files changed, 325 insertions(+), 8 deletions(-)
create mode 100644 lldb/test/Shell/Expr/objc-gnustep-exceptions.m
diff --git a/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCRuntime.cpp b/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCRuntime.cpp
index ae8cd9708c214..4b0c75dcb94a6 100644
--- a/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCRuntime.cpp
+++ b/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCRuntime.cpp
@@ -28,6 +28,8 @@
#include "lldb/Target/ExecutionContext.h"
#include "lldb/Target/Process.h"
#include "lldb/Target/RegisterContext.h"
+#include "lldb/Target/StackFrameRecognizer.h"
+#include "lldb/Target/StopInfo.h"
#include "lldb/Target/Target.h"
#include "lldb/Target/Thread.h"
#include "lldb/Target/ThreadPlanRunToAddress.h"
@@ -39,6 +41,8 @@
#include "lldb/Utility/RegularExpression.h"
#include "lldb/Utility/StructuredData.h"
#include "lldb/ValueObject/ValueObject.h"
+#include "lldb/ValueObject/ValueObjectConstResult.h"
+#include "lldb/ValueObject/ValueObjectList.h"
#include "llvm/ADT/STLExtras.h"
#include "llvm/IR/IRBuilder.h"
@@ -157,6 +161,81 @@ class GNUstepObjCSelectorRegistrationPass : public llvm::ModulePass {
};
char GNUstepObjCSelectorRegistrationPass::ID = 0;
+
+/// Presents the object being thrown as an `exception` argument on a frame
+/// stopped at libobjc2's throw entry point, which is what `thread exception`
+/// and lldb-dap's exception view read.
+class GNUstepObjCExceptionRecognizedStackFrame : public RecognizedStackFrame {
+public:
+ explicit GNUstepObjCExceptionRecognizedStackFrame(StackFrameSP frame_sp) {
+ ThreadSP thread_sp = frame_sp->GetThread();
+ if (!thread_sp)
+ return;
+ ProcessSP process_sp = thread_sp->GetProcess();
+ if (!process_sp)
+ return;
+
+ const ABISP &abi = process_sp->GetABI();
+ if (!abi)
+ return;
+
+ TypeSystemClangSP scratch_ts_sp =
+ ScratchTypeSystemClang::GetForTarget(process_sp->GetTarget());
+ if (!scratch_ts_sp)
+ return;
+ CompilerType void_ptr_type =
+ scratch_ts_sp->GetBasicType(eBasicTypeVoid).GetPointerType();
+
+ // `void objc_exception_throw(id object)` on every libobjc2 exception
+ // back-end, so the thrown object is argument 0 regardless of how the
+ // unwinder is implemented on this platform.
+ ValueList args;
+ Value input_value;
+ input_value.SetCompilerType(void_ptr_type);
+ args.PushValue(input_value);
+ if (!abi->GetArgumentValues(*thread_sp, args))
+ return;
+
+ Value value(args.GetValueAtIndex(0)->GetScalar().ULongLong());
+ value.SetCompilerType(void_ptr_type);
+ m_exception_sp = ValueObjectConstResult::Create(frame_sp.get(), value,
+ ConstString("exception"));
+ m_exception_sp = ValueObjectRecognizerSynthesizedValue::Create(
+ *m_exception_sp, eValueTypeVariableArgument);
+ m_exception_sp = m_exception_sp->GetDynamicValue(eDynamicDontRunTarget);
+
+ m_arguments = std::make_shared<ValueObjectList>();
+ m_arguments->Append(m_exception_sp);
+ m_stop_desc = "hit Objective-C exception";
+ }
+
+ ValueObjectSP GetExceptionObject() override { return m_exception_sp; }
+
+private:
+ ValueObjectSP m_exception_sp;
+};
+
+class GNUstepObjCExceptionThrowFrameRecognizer : public StackFrameRecognizer {
+ RecognizedStackFrameSP RecognizeFrame(StackFrameSP frame) override {
+ return std::make_shared<GNUstepObjCExceptionRecognizedStackFrame>(frame);
+ }
+ std::string GetName() override {
+ return "GNUstep ObjC Exception Throw StackFrame Recognizer";
+ }
+};
+
+/// The runtime library's file name differs by platform (libobjc.so.4.6,
+/// objc.dll, ...), so the recognizer is registered without a module filter
+/// and matched on the symbol alone; an empty module ConstString matches any
+/// module (StackFrameRecognizer.cpp).
+void RegisterGNUstepObjCExceptionRecognizer(Process *process) {
+ static const std::vector<ConstString> g_symbols = {
+ ConstString("objc_exception_throw")};
+ process->GetTarget().GetFrameRecognizerManager().AddRecognizer(
+ std::make_shared<GNUstepObjCExceptionThrowFrameRecognizer>(),
+ ConstString(), g_symbols, Mangled::NamePreference::ePreferDemangled,
+ /*first_instruction_only=*/true);
+}
} // namespace
char GNUstepObjCRuntime::ID = 0;
@@ -164,7 +243,8 @@ char GNUstepObjCRuntime::ID = 0;
void GNUstepObjCRuntime::Initialize() {
PluginManager::RegisterPlugin(
GetPluginNameStatic(), "GNUstep Objective-C Language Runtime - libobjc2",
- CreateInstance);
+ CreateInstance, /*command_callback=*/nullptr,
+ GetBreakpointExceptionPrecondition);
}
void GNUstepObjCRuntime::Terminate() {
@@ -237,6 +317,7 @@ LanguageRuntime *GNUstepObjCRuntime::CreateInstance(Process *process,
if (!FindGNUstepObjCRuntimeModule(target.GetImages()))
return nullptr;
+ RegisterGNUstepObjCExceptionRecognizer(process);
return new GNUstepObjCRuntime(process);
}
@@ -600,15 +681,129 @@ GNUstepObjCRuntime::FixUpDynamicType(const TypeAndOrName &type_and_or_name,
BreakpointResolverSP
GNUstepObjCRuntime::CreateExceptionResolver(const BreakpointSP &bkpt,
bool catch_bp, bool throw_bp) {
- BreakpointResolverSP resolver_sp;
-
+ std::vector<std::string> names;
if (throw_bp)
- resolver_sp = std::make_shared<BreakpointResolverName>(
- bkpt, "objc_exception_throw", eFunctionNameTypeBase,
- eLanguageTypeUnknown, Breakpoint::Exact, 0,
- /*offset_is_insn_count = */ false, eLazyBoolNo);
+ names.emplace_back("objc_exception_throw");
+
+ // Unlike Apple's runtime, libobjc2 has an entry point for entering a
+ // handler - but only where exceptions unwind through the Itanium ABI. The
+ // MSVC build raises a native SEH exception instead (eh_win32_msvc.cc) and
+ // catch is a __CxxFrameHandler3 funclet with no symbol to break on, so ask
+ // the runtime module rather than the target triple.
+ if (catch_bp && ModuleDefinesFunction(m_objc_module_sp, "objc_begin_catch")) {
+ names.emplace_back("objc_begin_catch");
+ // For gnustep-2.x on MinGW clang routes @catch through the C++ ABI
+ // instead (CGObjCGNU.cpp, usesCxxExceptions), leaving libobjc2's entry
+ // point exported but never reached. Which one a program calls depends on
+ // how it was compiled, so offer both; the unused name never resolves.
+ // That entry point is shared with C++ catch, so on MinGW this stops on
+ // those too.
+ //
+ // isOSWindows(), not the environment: LLDB reports a MinGW PE as msvc
+ // unless plugin.object-file.pe-coff.abi says otherwise.
+ if (GetTargetRef().GetArchitecture().GetTriple().isOSWindows())
+ names.emplace_back("__cxa_begin_catch");
+ }
+
+ if (names.empty())
+ return {};
+
+ return std::make_shared<BreakpointResolverName>(
+ bkpt, names, eFunctionNameTypeBase, eLanguageTypeUnknown,
+ /*offset=*/0, /*skip_prologue=*/eLazyBoolNo);
+}
+
+void GNUstepObjCRuntime::SetExceptionBreakpoints() {
+ if (!m_process)
+ return;
+
+ const bool catch_bp = false;
+ const bool throw_bp = true;
+ const bool is_internal = true;
+
+ if (!m_objc_exception_bp_sp) {
+ m_objc_exception_bp_sp = LanguageRuntime::CreateExceptionBreakpoint(
+ m_process->GetTarget(), GetLanguageType(), catch_bp, throw_bp,
+ is_internal);
+ if (m_objc_exception_bp_sp)
+ m_objc_exception_bp_sp->SetBreakpointKind("ObjC exception");
+ } else {
+ m_objc_exception_bp_sp->SetEnabled(true);
+ }
+}
- return resolver_sp;
+void GNUstepObjCRuntime::ClearExceptionBreakpoints() {
+ if (!m_process)
+ return;
+
+ if (m_objc_exception_bp_sp)
+ m_objc_exception_bp_sp->SetEnabled(false);
+}
+
+bool GNUstepObjCRuntime::ExceptionBreakpointsAreSet() {
+ return m_objc_exception_bp_sp && m_objc_exception_bp_sp->IsEnabled();
+}
+
+bool GNUstepObjCRuntime::ExceptionBreakpointsExplainStop(
+ StopInfoSP stop_reason) {
+ if (!m_process || !m_objc_exception_bp_sp)
+ return false;
+
+ if (!stop_reason || stop_reason->GetStopReason() != eStopReasonBreakpoint)
+ return false;
+
+ const uint64_t break_site_id = stop_reason->GetValue();
+ return m_process->GetBreakpointSiteList().StopPointSiteContainsBreakpoint(
+ break_site_id, m_objc_exception_bp_sp->GetID());
+}
+
+ValueObjectSP
+GNUstepObjCRuntime::GetExceptionObjectForThread(ThreadSP thread_sp) {
+ if (!thread_sp || !thread_sp->SafeToCallFunctions())
+ return {};
+
+ // Stopped at the throw itself the object is simply argument 0, which holds
+ // on every one of libobjc2's exception back-ends and is where the frame
+ // recognizer already presents it. That is the path that works.
+ //
+ // Further into the unwind there is nothing runtime-independent to read, so
+ // ask the C++ runtime - which currently recovers nothing on either
+ // back-end that reaches here, for two different reasons. On ELF libobjc2
+ // raises with its own exception class rather than through __cxa_throw, so
+ // libstdc++ never records it and __cxa_current_exception_type() is null. On
+ // MinGW it does record it, but ItaniumABIRuntime reads the word before the
+ // type_info, which only locates the object for Apple's runtime - objc4
+ // embeds the type_info in the same allocation. libobjc2 shares one exported
+ // type_info, so that word is an unrelated global. `thread exception` is
+ // therefore reliable at the throw site and empty elsewhere.
+ if (StackFrameSP frame_sp = thread_sp->GetStackFrameAtIndex(0)) {
+ if (RecognizedStackFrameSP recognized_sp = frame_sp->GetRecognizedFrame()) {
+ if (ValueObjectSP exception_sp = recognized_sp->GetExceptionObject())
+ return exception_sp;
+ }
+ }
+
+ auto *cpp_runtime = m_process->GetLanguageRuntime(eLanguageTypeC_plus_plus);
+ if (!cpp_runtime)
+ return {};
+ ValueObjectSP cpp_exception_sp =
+ cpp_runtime->GetExceptionObjectForThread(thread_sp);
+ if (!cpp_exception_sp)
+ return {};
+
+ // An ObjC exception raised through the Itanium ABI is indistinguishable
+ // from a C++ one at this level, so confirm the thrown object really is an
+ // NSException (or a subclass) before claiming it.
+ ClassDescriptorSP descriptor_sp = GetClassDescriptor(*cpp_exception_sp);
+ if (!descriptor_sp || !descriptor_sp->IsValid())
+ return {};
+
+ static const ConstString g_NSException("NSException");
+ for (; descriptor_sp; descriptor_sp = descriptor_sp->GetSuperclass()) {
+ if (descriptor_sp->GetClassName() == g_NSException)
+ return cpp_exception_sp;
+ }
+ return {};
}
llvm::Expected<std::unique_ptr<UtilityFunction>>
diff --git a/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCRuntime.h b/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCRuntime.h
index d00ed899656f2..87e5179de170f 100644
--- a/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCRuntime.h
+++ b/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCRuntime.h
@@ -80,10 +80,29 @@ class GNUstepObjCRuntime : public lldb_private::ObjCLanguageRuntime {
TypeAndOrName FixUpDynamicType(const TypeAndOrName &type_and_or_name,
ValueObject &static_value) override;
+ /// CreateExceptionSearchFilter is deliberately not overridden. Apple's
+ /// runtime narrows the filter to the module holding its throw entry point,
+ /// which it can because that is always libobjc.A.dylib. Ours is not always
+ /// in the runtime library: where clang routes @catch through the C++ ABI
+ /// the entry point is libstdc++'s __cxa_begin_catch, so the search has to
+ /// stay unrestricted.
lldb::BreakpointResolverSP
CreateExceptionResolver(const lldb::BreakpointSP &bkpt, bool catch_bp,
bool throw_bp) override;
+ void SetExceptionBreakpoints() override;
+
+ void ClearExceptionBreakpoints() override;
+
+ bool ExceptionBreakpointsAreSet() override;
+
+ bool ExceptionBreakpointsExplainStop(lldb::StopInfoSP stop_reason) override;
+
+ /// The object of the Objective-C exception being thrown on \p thread, or an
+ /// empty ValueObjectSP if the thread is not throwing one.
+ lldb::ValueObjectSP
+ GetExceptionObjectForThread(lldb::ThreadSP thread_sp) override;
+
lldb::ThreadPlanSP GetStepThroughTrampolinePlan(Thread &thread,
bool stop_others) override;
@@ -215,6 +234,11 @@ class GNUstepObjCRuntime : public lldb_private::ObjCLanguageRuntime {
/// Guards against a sweep that resolves a class re-entering the sweep.
bool m_updating_isa_map = false;
+
+ /// The internal breakpoint on the runtime's throw entry point, used by
+ /// `process handle`/`thread exception` to stop where an exception is
+ /// raised rather than where it is caught.
+ lldb::BreakpointSP m_objc_exception_bp_sp;
};
} // namespace lldb_private
diff --git a/lldb/test/Shell/Expr/objc-gnustep-exceptions.m b/lldb/test/Shell/Expr/objc-gnustep-exceptions.m
new file mode 100644
index 0000000000000..de44edee8914c
--- /dev/null
+++ b/lldb/test/Shell/Expr/objc-gnustep-exceptions.m
@@ -0,0 +1,98 @@
+// REQUIRES: objc-gnustep
+//
+// RUN: %build %s --compiler=clang --objc-gnustep --output=%t
+
+#import "objc/runtime.h"
+
+ at protocol NSCoding
+ at end
+
+#ifdef __has_attribute
+#if __has_attribute(objc_root_class)
+__attribute__((objc_root_class))
+#endif
+#endif
+ at interface NSObject <NSCoding> {
+ id isa;
+}
++ (id)new;
+ at end
+ at implementation NSObject
++ (id)new {
+ return class_createInstance(self, 0);
+}
+ at end
+
+// `po` on the exception object goes through -description/-UTF8String, so a
+// minimal pair of those is all this needs; nothing here uses Foundation.
+ at interface Str : NSObject {
+ const char *_bytes;
+}
++ (id)withBytes:(const char *)bytes;
+- (const char *)UTF8String;
+ at end
+ at implementation Str
++ (id)withBytes:(const char *)bytes {
+ Str *str = [Str new];
+ str->_bytes = bytes;
+ return str;
+}
+- (const char *)UTF8String {
+ return _bytes;
+}
+ at end
+
+ at interface Boom : NSObject
+- (id)description;
+ at end
+ at implementation Boom
+- (id)description {
+ return [Str withBytes:"<Boom: thrown>"];
+}
+ at end
+
+void thrower(void) { @throw [Boom new]; }
+
+const char *g_caught_name = 0;
+
+int main() {
+ @try {
+ thrower();
+ } @catch (id caught) {
+ // Something with real code, so a breakpoint can land inside the handler.
+ g_caught_name = object_getClassName(caught);
+ }
+ return g_caught_name == 0;
+}
+
+// An Objective-C exception breakpoint stops where the exception is raised,
+// and the frame recognizer presents the thrown object as `exception`.
+//
+// RUN: %lldb -b -o "breakpoint set -E objc" -o "run" -o "frame variable" \
+// RUN: -o "thread exception" \
+// RUN: -- %t | FileCheck %s --check-prefix=THROW
+//
+// THROW: (lldb) breakpoint set -E objc
+// THROW: Breakpoint {{[0-9]+}}:
+//
+// THROW: (lldb) run
+// THROW: stop reason = hit Objective-C exception
+//
+// The recognizer synthesizes the argument, so the thrown object shows up in
+// `frame variable` even though objc_exception_throw has no debug info - and
+// it carries the dynamic type, not the `id` the runtime declares.
+// THROW: (lldb) frame variable
+// THROW: (Boom *) exception = 0x
+//
+// THROW: (lldb) thread exception
+// THROW: (Boom *) exception = 0x
+
+// The object is reachable as a real local inside the handler, where `po`
+// works on it normally.
+//
+// RUN: %lldb -b -o "b objc-gnustep-exceptions.m:63" -o "run" -o "po caught" \
+// RUN: -- %t | FileCheck %s --check-prefix=CAUGHT
+//
+// CAUGHT: (lldb) po caught
+// CAUGHT: <Boom: thrown>
+// CAUGHT-NOT: warning: `po` was unsuccessful
>From 5c056975112cf8c06bcffc31c7ef21363a291c61 Mon Sep 17 00:00:00 2001
From: Rob <robk at robk.dev>
Date: Tue, 18 Aug 2026 09:12:54 +0100
Subject: [PATCH 29/49] [lldb][GNUstep] Test catch breakpoints where the
runtime supports them
libobjc2 exports objc_begin_catch wherever exceptions unwind through the
Itanium ABI, so a catch breakpoint stops on entry to the handler with the
@catch line as its caller. Gate the check on !windows-msvc rather than on
the OS: the MSVC build raises a native SEH exception and has no such entry
point, but MinGW (windows-gnu) uses __cxa_* over SEH and does.
Assisted-by: Claude Opus 5
---
.../test/Shell/Expr/objc-gnustep-exceptions.m | 12 +++++
lldb/test/Shell/lit.cfg.py | 47 +++++++++++++++++++
2 files changed, 59 insertions(+)
diff --git a/lldb/test/Shell/Expr/objc-gnustep-exceptions.m b/lldb/test/Shell/Expr/objc-gnustep-exceptions.m
index de44edee8914c..f9966f61b7b96 100644
--- a/lldb/test/Shell/Expr/objc-gnustep-exceptions.m
+++ b/lldb/test/Shell/Expr/objc-gnustep-exceptions.m
@@ -96,3 +96,15 @@ int main() {
// CAUGHT: (lldb) po caught
// CAUGHT: <Boom: thrown>
// CAUGHT-NOT: warning: `po` was unsuccessful
+
+// Unlike Apple's runtime, libobjc2 has an entry point for entering a handler,
+// so catch breakpoints are real, but only where exceptions unwind through
+// the Itanium ABI, which its MSVC build does not.
+//
+// RUN: %if objc-gnustep-catch %{ %lldb -b -o "breakpoint set -E objc --on-catch true --on-throw false" -o "run" -o "bt" -- %t | FileCheck %s --check-prefix=CATCH %}
+//
+// CATCH: stop reason = breakpoint
+// objc_begin_catch, or __cxa_begin_catch where clang routes @catch through
+// the C++ ABI.
+// CATCH: frame #0: {{.*}}{{(objc|__cxa)_begin_catch}}
+// CATCH: frame #1: {{.*}}main at objc-gnustep-exceptions.m:
diff --git a/lldb/test/Shell/lit.cfg.py b/lldb/test/Shell/lit.cfg.py
index 134dba7f1c494..ef18de401cc7d 100644
--- a/lldb/test/Shell/lit.cfg.py
+++ b/lldb/test/Shell/lit.cfg.py
@@ -157,6 +157,47 @@ def calculate_arch_features(arch_string):
if config.have_lldb_server:
config.available_features.add("lldb-server")
+def runtime_exports(directory, symbol):
+ """Whether a libobjc2 under `directory` exports `symbol`."""
+
+ def tool(name):
+ return shutil.which(name, path=config.llvm_tools_dir) or shutil.which(name)
+
+ for subdir in ("lib", "bin"):
+ subdir_path = os.path.join(directory, subdir)
+ if not os.path.isdir(subdir_path):
+ continue
+ for entry in os.listdir(subdir_path):
+ root, ext = os.path.splitext(entry)
+ is_pe = ext.lower() == ".dll"
+ is_unix_shared = ext.lower() in (".so", ".dylib") or ".so." in entry
+ if not (is_pe or is_unix_shared):
+ continue
+ if not re.match(r"(lib)?objc([.-]|$)", root, re.IGNORECASE):
+ continue
+ path = os.path.join(subdir_path, entry)
+ # The export/dynamic table, not the symbol table: a stripped
+ # library still exports.
+ if is_pe:
+ argv = [tool("llvm-readobj"), "--coff-exports", path]
+ else:
+ argv = [tool("llvm-nm"), "--dynamic", "--defined-only", path]
+ if not argv[0]:
+ lit_config.warning("no tool to read exports from " + path)
+ continue
+ try:
+ probe = subprocess.run(argv, capture_output=True, text=True, timeout=60)
+ except (OSError, subprocess.SubprocessError) as e:
+ lit_config.warning("could not read exports from %s: %s" % (path, e))
+ continue
+ if probe.returncode != 0:
+ lit_config.warning("could not read exports from " + path)
+ continue
+ if symbol in probe.stdout:
+ return True
+ return False
+
+
if config.objc_gnustep_dir:
config.available_features.add("objc-gnustep")
if platform.system() == "Windows":
@@ -168,6 +209,12 @@ def calculate_arch_features(arch_string):
)
)
+ # Catch breakpoints need an entry point for entering a handler. libobjc2
+ # exports one only where exceptions unwind through the Itanium ABI: a
+ # property of how the configured runtime was built, not of any triple.
+ if runtime_exports(config.objc_gnustep_dir, "objc_begin_catch"):
+ config.available_features.add("objc-gnustep-catch")
+
if config.have_dia_sdk:
config.available_features.add("diasdk")
>From e4d749b0f8cc1762a6459555a3c899c90e6a818a Mon Sep 17 00:00:00 2001
From: Rob <robk at robk.dev>
Date: Tue, 18 Aug 2026 09:24:55 +0100
Subject: [PATCH 30/49] [lldb][GNUstep] Add a type encoding parser for libobjc2
metadata
libobjc2 stores an Objective-C type encoding alongside every ivar, method
and property. Nothing could turn those into types, so GetEncodingToType()
returned nullptr and all of that metadata was inert.
The grammar is the standard one, so this is close to
AppleObjCTypeEncodingParser, and deliberately differs in four places:
- Leading argument qualifiers ("nNoORVA", libobjc2's encoding2.c) are
skipped. clang emits these for declarations such as
`- (oneway void)release`, and an unhandled one fails the whole encoding,
which would drop the method. `r` is deliberately not in that set: it is
_C_CONST, a real qualifier that changes the type.
- `@"Name"` is always a class name. Apple has to disambiguate it from a
field named "Name" because its runtime emits quoted field names inside
struct encodings. clang's GNUstep output never does - verified against the
shipped library, whose ivar encodings expand struct members positionally
(`{_NSRange=QQ}`, not `{_NSRange="location"Q"length"Q}`) - so applying
that disambiguation here would mis-parse `{Foo=@"NSString"i}`, reading the
trailing `i` as evidence that "NSString" had been a field name.
- A class name the runtime cannot resolve yields `id`. Apple asserts, which
would abort a debug LLDB; an encoding routinely outlives the class it
names.
- Records are cached, keyed on the AST and on the exact encoding text. Both
halves are needed: RealizeType is called with different ASTContexts and a
QualType is only valid in the one that made it, and keying on the tag
alone would let an opaque `{Foo=}` shadow a later `{Foo=ii}`.
`D` (long double) is also handled, since it is a real type on x86-64 ELF and
Apple's parser drops any method mentioning one.
The encoding comes from inferior memory, so recursion is depth-bounded. The
parser takes a triple rather than a runtime, so it needs no target state and
is tested directly: 51 cases across LP64, LLP64 and ILP32, including every
truncation of a nested encoding and a 4096-deep pointer chain.
Assisted-by: Claude Opus 5
---
.../ObjC/GNUstepObjCRuntime/CMakeLists.txt | 1 +
.../GNUstepObjCRuntime/GNUstepObjCRuntime.cpp | 9 +
.../GNUstepObjCRuntime/GNUstepObjCRuntime.h | 7 +
.../GNUstepObjCTypeEncodingParser.cpp | 396 ++++++++++++++++++
.../GNUstepObjCTypeEncodingParser.h | 103 +++++
.../ObjC/GNUstepObjCRuntime/CMakeLists.txt | 3 +
.../GNUstepObjCTypeEncodingParserTest.cpp | 263 ++++++++++++
7 files changed, 782 insertions(+)
create mode 100644 lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCTypeEncodingParser.cpp
create mode 100644 lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCTypeEncodingParser.h
create mode 100644 lldb/unittests/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCTypeEncodingParserTest.cpp
diff --git a/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/CMakeLists.txt b/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/CMakeLists.txt
index 14364c12dfcde..46cf4ef300797 100644
--- a/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/CMakeLists.txt
+++ b/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/CMakeLists.txt
@@ -1,5 +1,6 @@
add_lldb_library(lldbPluginGNUstepObjCRuntime PLUGIN
GNUstepObjCClassDescriptor.cpp
+ GNUstepObjCTypeEncodingParser.cpp
GNUstepObjCRuntime.cpp
GNUstepThreadPlanStepThroughObjCTrampoline.cpp
diff --git a/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCRuntime.cpp b/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCRuntime.cpp
index 4b0c75dcb94a6..2008e542720d8 100644
--- a/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCRuntime.cpp
+++ b/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCRuntime.cpp
@@ -8,6 +8,7 @@
#include "GNUstepObjCRuntime.h"
#include "GNUstepObjCClassDescriptor.h"
+#include "GNUstepObjCTypeEncodingParser.h"
#include "GNUstepThreadPlanStepThroughObjCTrampoline.h"
#include "Plugins/TypeSystem/Clang/TypeSystemClang.h"
@@ -1156,6 +1157,14 @@ GNUstepObjCRuntime::GetClassDescriptor(ValueObject &in_value) {
return ObjCLanguageRuntime::GetClassDescriptor(in_value);
}
+ObjCLanguageRuntime::EncodingToTypeSP
+GNUstepObjCRuntime::GetEncodingToType() {
+ if (!m_encoding_to_type_sp)
+ m_encoding_to_type_sp = std::make_shared<GNUstepObjCTypeEncodingParser>(
+ GetTargetRef().GetArchitecture().GetTriple(), this);
+ return m_encoding_to_type_sp;
+}
+
ObjCLanguageRuntime::ClassDescriptorSP
GNUstepObjCRuntime::GetClassDescriptorFromISA(ObjCISA isa) {
if (ClassDescriptorSP descriptor_sp =
diff --git a/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCRuntime.h b/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCRuntime.h
index 87e5179de170f..64ee7bf7f8a8e 100644
--- a/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCRuntime.h
+++ b/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCRuntime.h
@@ -143,6 +143,11 @@ class GNUstepObjCRuntime : public lldb_private::ObjCLanguageRuntime {
ClassDescriptorSP GetClassDescriptorFromISA(ObjCISA isa) override;
+ /// Realizes libobjc2's type encodings, which is what makes the ivar and
+ /// method metadata usable as types. Built on first use and kept, because
+ /// the types it returns are only valid in the AST it created them in.
+ EncodingToTypeSP GetEncodingToType() override;
+
/// Lazily-built FunctionCaller for a utility function that resolves a
/// method implementation via libobjc2's
/// `IMP objc_msg_lookup(id receiver, SEL selector)`, used by the
@@ -239,6 +244,8 @@ class GNUstepObjCRuntime : public lldb_private::ObjCLanguageRuntime {
/// `process handle`/`thread exception` to stop where an exception is
/// raised rather than where it is caught.
lldb::BreakpointSP m_objc_exception_bp_sp;
+
+ EncodingToTypeSP m_encoding_to_type_sp;
};
} // namespace lldb_private
diff --git a/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCTypeEncodingParser.cpp b/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCTypeEncodingParser.cpp
new file mode 100644
index 0000000000000..1b6b7e28b4209
--- /dev/null
+++ b/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCTypeEncodingParser.cpp
@@ -0,0 +1,396 @@
+//===-- GNUstepObjCTypeEncodingParser.cpp ---------------------------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+#include "GNUstepObjCTypeEncodingParser.h"
+
+#include "Plugins/ExpressionParser/Clang/ClangUtil.h"
+#include "Plugins/Language/ObjC/ObjCConstants.h"
+#include "Plugins/TypeSystem/Clang/TypeSystemClang.h"
+
+#include "lldb/Symbol/CompilerType.h"
+#include "lldb/Symbol/DeclVendor.h"
+#include "lldb/Utility/ConstString.h"
+#include "lldb/Utility/LLDBLog.h"
+#include "lldb/Utility/Log.h"
+
+#include "llvm/ADT/ScopeExit.h"
+#include "llvm/ADT/StringExtras.h"
+
+#include <limits>
+#include <optional>
+#include <vector>
+
+using namespace lldb_private;
+
+// Encodings ObjCConstants.h does not name. _C_COMPLEX is a libobjc2
+// extension (objc/runtime.h); long double has no macro there at all, and is
+// a real type on x86-64 ELF, so a method mentioning one would otherwise be
+// dropped wholesale.
+static constexpr char _C_COMPLEX = 'j';
+static constexpr char _C_LNG_DBL = 'D';
+
+// Method-argument qualifiers, from libobjc2's objc_skip_type_qualifiers
+// (encoding2.c). `r` is _C_CONST and is deliberately absent: it is a real
+// qualifier that changes the type, and BuildType handles it.
+static constexpr llvm::StringLiteral g_argument_qualifiers = "nNoORVA";
+
+// An encoding nested more deeply than this is not something a compiler
+// emitted; refusing it keeps a malformed string from recursing without bound.
+static constexpr unsigned g_max_depth = 64;
+
+static char popChar(llvm::StringRef &str) {
+ const char c = str.front();
+ str = str.drop_front();
+ return c;
+}
+
+/// Reads a decimal count, consuming the digits. Returns nullopt when the value
+/// does not fit in 32 bits: the encoding comes from inferior memory, and a
+/// wrapped extent would size an array wrongly rather than fail.
+static std::optional<uint32_t> ReadNumber(llvm::StringRef &type) {
+ uint64_t total = 0;
+ while (!type.empty() && llvm::isDigit(type.front())) {
+ total = 10 * total + (popChar(type) - '0');
+ if (total > std::numeric_limits<uint32_t>::max())
+ return std::nullopt;
+ }
+ return static_cast<uint32_t>(total);
+}
+
+/// Reads up to (not including) the closing quote, which stays in \p type.
+static std::optional<std::string> ReadQuotedString(llvm::StringRef &type) {
+ std::string buffer;
+ while (!type.empty() && type.front() != '"')
+ buffer.push_back(popChar(type));
+ if (type.empty())
+ return std::nullopt;
+ return buffer;
+}
+
+/// Reads a struct or union tag, which runs up to the '='.
+static std::string ReadAggregateName(llvm::StringRef &type) {
+ std::string buffer;
+ while (!type.empty() && type.front() != _C_STRUCT_E &&
+ type.front() != _C_UNION_E && type.front() != '=')
+ buffer.push_back(popChar(type));
+ return buffer;
+}
+
+GNUstepObjCTypeEncodingParser::GNUstepObjCTypeEncodingParser(
+ const llvm::Triple &triple, ObjCLanguageRuntime *runtime)
+ : ObjCLanguageRuntime::EncodingToType(), m_runtime(runtime) {
+ m_scratch_ast_ctx_sp = std::make_shared<TypeSystemClang>(
+ "GNUstepObjCTypeEncodingParser ASTContext", triple);
+}
+
+GNUstepObjCTypeEncodingParser::StructElement
+GNUstepObjCTypeEncodingParser::ReadStructElement(TypeSystemClang &ast_ctx,
+ llvm::StringRef &type,
+ bool for_expression) {
+ StructElement retval;
+ // Quoted field names are an Apple extension that clang's GNUstep output
+ // does not produce, but consuming one costs nothing and keeps this usable
+ // against an encoding that came from somewhere else.
+ if (type.consume_front("\"")) {
+ if (auto maybe_name = ReadQuotedString(type)) {
+ retval.name = *maybe_name;
+ type = type.drop_front(); // the closing quote
+ } else {
+ return retval;
+ }
+ }
+ uint32_t bitfield_size = 0;
+ retval.type = BuildType(ast_ctx, type, for_expression, &bitfield_size);
+ retval.bitfield = bitfield_size;
+ return retval;
+}
+
+clang::QualType GNUstepObjCTypeEncodingParser::BuildAggregate(
+ TypeSystemClang &ast_ctx, llvm::StringRef &type, bool for_expression,
+ char opener, char closer, uint32_t kind) {
+ llvm::StringRef start = type;
+ if (!type.consume_front(opener))
+ return clang::QualType();
+
+ const std::string name = ReadAggregateName(type);
+
+ // Templated names are parsed for their side effect on `type` and then
+ // discarded; there is no sensible clang record to build for one.
+ const bool is_templated = name.find('<') != std::string::npos;
+
+ // An opaque aggregate - `{Foo}` with no '=' - carries no members. libobjc2
+ // spells this `{Foo=}`, but accept both rather than failing the whole
+ // encoding.
+ bool has_members = type.consume_front("=");
+
+ std::vector<StructElement> elements;
+ bool closed = false;
+ while (!type.empty()) {
+ if (type.consume_front(closer)) {
+ closed = true;
+ break;
+ }
+ if (!has_members)
+ return clang::QualType();
+ StructElement element = ReadStructElement(ast_ctx, type, for_expression);
+ if (element.type.isNull())
+ return clang::QualType();
+ elements.push_back(std::move(element));
+ }
+ if (!closed || is_templated)
+ return clang::QualType();
+
+ // Key the cache on the exact text this record was built from, so that two
+ // structurally different types sharing a tag - `{Foo=}` and `{Foo=ii}` -
+ // do not collide, and so a repeated struct is laid out once.
+ const llvm::StringRef encoding = start.take_front(start.size() - type.size());
+ llvm::StringMap<clang::QualType> &cache = m_record_cache[&ast_ctx];
+ auto cached = cache.find(encoding);
+ if (cached != cache.end())
+ return cached->second;
+
+ CompilerType record_type(ast_ctx.CreateRecordType(
+ nullptr, OptionalClangModuleID(), name, kind, lldb::eLanguageTypeC));
+ if (!record_type)
+ return clang::QualType();
+
+ TypeSystemClang::StartTagDeclarationDefinition(record_type);
+ unsigned count = 0;
+ for (StructElement &element : elements) {
+ if (element.name.empty())
+ element.name = ("__unnamed_" + llvm::Twine(count)).str();
+ TypeSystemClang::AddFieldToRecordType(record_type, element.name.c_str(),
+ ast_ctx.GetType(element.type),
+ element.bitfield);
+ ++count;
+ }
+ TypeSystemClang::CompleteTagDeclarationDefinition(record_type);
+
+ clang::QualType qual_type = ClangUtil::GetQualType(record_type);
+ cache[encoding] = qual_type;
+ return qual_type;
+}
+
+clang::QualType GNUstepObjCTypeEncodingParser::BuildArray(
+ TypeSystemClang &ast_ctx, llvm::StringRef &type, bool for_expression) {
+ if (!type.consume_front(_C_ARY_B))
+ return clang::QualType();
+
+ const std::optional<uint32_t> size = ReadNumber(type);
+ if (!size)
+ return clang::QualType();
+ clang::QualType element_type(BuildType(ast_ctx, type, for_expression));
+ if (element_type.isNull())
+ return clang::QualType();
+ if (!type.consume_front(_C_ARY_E))
+ return clang::QualType();
+
+ CompilerType array_type(ast_ctx.CreateArrayType(
+ CompilerType(ast_ctx.weak_from_this(), element_type.getAsOpaquePtr()),
+ *size, /*is_vector=*/false));
+ return ClangUtil::GetQualType(array_type);
+}
+
+clang::QualType GNUstepObjCTypeEncodingParser::BuildObjCObjectPointerType(
+ TypeSystemClang &clang_ast_ctx, llvm::StringRef &type,
+ bool for_expression) {
+ if (!type.consume_front(_C_ID))
+ return clang::QualType();
+
+ clang::ASTContext &ast_ctx = clang_ast_ctx.getASTContext();
+
+ // `@?` is a block. Its full signature may follow in an extended encoding,
+ // which nothing here needs, so treat it as an opaque object pointer.
+ if (type.consume_front(_C_UNDEF))
+ return ast_ctx.getObjCIdType();
+
+ std::string name;
+ if (type.consume_front("\"")) {
+ // Unlike Apple's runtime, clang's GNUstep output never emits quoted field
+ // names inside a struct, so a quoted string after '@' is unambiguously a
+ // class name. Apple peeks at the next character to tell the two apart;
+ // doing that here would mis-parse `{Foo=@"NSString"i}`, whose `i` is the
+ // next field rather than a hint that "NSString" was a field name.
+ if (auto maybe_name = ReadQuotedString(type)) {
+ name = *maybe_name;
+ type = type.drop_front(); // the closing quote
+ } else {
+ return clang::QualType();
+ }
+ }
+
+ if (!for_expression || name.empty())
+ return ast_ctx.getObjCIdType();
+
+ // Protocol qualifiers carry no type information here: `<Proto>` alone is
+ // just `id`, and `NSFoo<Proto>` is an NSFoo.
+ const size_t less_than_pos = name.find('<');
+ if (less_than_pos == 0)
+ return ast_ctx.getObjCIdType();
+ if (less_than_pos != std::string::npos)
+ name.erase(less_than_pos);
+
+ DeclVendor *decl_vendor = m_runtime ? m_runtime->GetDeclVendor() : nullptr;
+ if (!decl_vendor)
+ return ast_ctx.getObjCIdType();
+
+ auto types = decl_vendor->FindTypes(ConstString(name), /*max_matches=*/1);
+ if (types.empty()) {
+ // Naming a class the runtime has not realized is expected, not a bug: an
+ // encoding outlives the class it mentions.
+ LLDB_LOG(GetLog(LLDBLog::Types),
+ "GNUstep type encoding names an unknown class: {0}", name);
+ return ast_ctx.getObjCIdType();
+ }
+ return ClangUtil::GetQualType(types.front().GetPointerType());
+}
+
+clang::QualType GNUstepObjCTypeEncodingParser::BuildType(
+ TypeSystemClang &clang_ast_ctx, llvm::StringRef &type, bool for_expression,
+ uint32_t *bitfield_bit_size) {
+ if (type.empty())
+ return clang::QualType();
+
+ // Every nesting construct - pointers, arrays, aggregates, const - recurses
+ // here, and the encoding is inferior data. Bound the depth so a malformed
+ // one cannot exhaust the stack.
+ if (m_depth >= g_max_depth)
+ return clang::QualType();
+ ++m_depth;
+ llvm::scope_exit depth_guard([this] { --m_depth; });
+
+ // Skip any argument qualifiers. clang emits these for declarations such as
+ // `- (oneway void)release`, and leaving them in place would drop the whole
+ // method.
+ while (!type.empty() && g_argument_qualifiers.contains(type.front()))
+ type = type.drop_front();
+ if (type.empty())
+ return clang::QualType();
+
+ clang::ASTContext &ast_ctx = clang_ast_ctx.getASTContext();
+
+ switch (type.front()) {
+ default:
+ break;
+ case _C_STRUCT_B:
+ return BuildAggregate(clang_ast_ctx, type, for_expression, _C_STRUCT_B,
+ _C_STRUCT_E,
+ llvm::to_underlying(clang::TagTypeKind::Struct));
+ case _C_UNION_B:
+ return BuildAggregate(clang_ast_ctx, type, for_expression, _C_UNION_B,
+ _C_UNION_E,
+ llvm::to_underlying(clang::TagTypeKind::Union));
+ case _C_ARY_B:
+ return BuildArray(clang_ast_ctx, type, for_expression);
+ case _C_ID:
+ return BuildObjCObjectPointerType(clang_ast_ctx, type, for_expression);
+ }
+
+ // Save a copy so an unrecognized encoding can be left untouched for the
+ // caller to notice.
+ llvm::StringRef backup = type;
+
+ switch (popChar(type)) {
+ default:
+ type = backup;
+ return clang::QualType();
+ case _C_CHR:
+ return ast_ctx.CharTy;
+ case _C_INT:
+ return ast_ctx.IntTy;
+ case _C_SHT:
+ return ast_ctx.ShortTy;
+ case _C_LNG:
+ // clang only emits 'l' where `long` is 32 bits and 'q' otherwise
+ // (ASTContext::getObjCEncodingForPrimitiveType), so this is right on
+ // LP64, LLP64 and ILP32 alike.
+ return ast_ctx.getIntTypeForBitwidth(32, /*Signed=*/true);
+ case _C_LNG_LNG:
+ return ast_ctx.LongLongTy;
+ case _C_UCHR:
+ return ast_ctx.UnsignedCharTy;
+ case _C_UINT:
+ return ast_ctx.UnsignedIntTy;
+ case _C_USHT:
+ return ast_ctx.UnsignedShortTy;
+ case _C_ULNG:
+ return ast_ctx.getIntTypeForBitwidth(32, /*Signed=*/false);
+ case _C_ULNG_LNG:
+ return ast_ctx.UnsignedLongLongTy;
+ case _C_FLT:
+ return ast_ctx.FloatTy;
+ case _C_DBL:
+ return ast_ctx.DoubleTy;
+ case _C_LNG_DBL:
+ return ast_ctx.LongDoubleTy;
+ case _C_BOOL:
+ return ast_ctx.BoolTy;
+ case _C_VOID:
+ return ast_ctx.VoidTy;
+ case _C_CHARPTR:
+ case _C_ATOM:
+ // _C_ATOM is a char* whose contents are interned.
+ return ast_ctx.getPointerType(ast_ctx.CharTy);
+ case _C_CLASS:
+ return ast_ctx.getObjCClassType();
+ case _C_SEL:
+ return ast_ctx.getObjCSelType();
+ case _C_COMPLEX: {
+ clang::QualType element_type =
+ BuildType(clang_ast_ctx, type, for_expression);
+ if (element_type.isNull())
+ return clang::QualType();
+ return ast_ctx.getComplexType(element_type);
+ }
+ case _C_VECTOR:
+ // The published grammar gives no way to recover a vector's width, so
+ // there is nothing to build. Consume it and report failure rather than
+ // silently producing the element type.
+ return clang::QualType();
+ case _C_BFLD: {
+ const std::optional<uint32_t> size = ReadNumber(type);
+ if (!size || !bitfield_bit_size)
+ return clang::QualType();
+ *bitfield_bit_size = *size;
+ return ast_ctx.UnsignedIntTy;
+ }
+ case _C_CONST: {
+ clang::QualType target_type =
+ BuildType(clang_ast_ctx, type, for_expression);
+ if (target_type.isNull())
+ return clang::QualType();
+ if (target_type == ast_ctx.UnknownAnyTy)
+ return ast_ctx.UnknownAnyTy;
+ return ast_ctx.getConstType(target_type);
+ }
+ case _C_PTR: {
+ if (!for_expression && type.consume_front(_C_UNDEF)) {
+ // A pointer to something unrepresentable is more useful as void* than
+ // as nothing at all, when the expression parser is not involved.
+ return ast_ctx.VoidPtrTy;
+ }
+ clang::QualType target_type =
+ BuildType(clang_ast_ctx, type, for_expression);
+ if (target_type.isNull())
+ return clang::QualType();
+ if (target_type == ast_ctx.UnknownAnyTy)
+ return ast_ctx.UnknownAnyTy;
+ return ast_ctx.getPointerType(target_type);
+ }
+ case _C_UNDEF:
+ return for_expression ? ast_ctx.UnknownAnyTy : clang::QualType();
+ }
+}
+
+CompilerType GNUstepObjCTypeEncodingParser::RealizeType(
+ TypeSystemClang &ast_ctx, const char *name, bool for_expression) {
+ if (!name || !name[0])
+ return CompilerType();
+ llvm::StringRef lexer(name);
+ return ast_ctx.GetType(BuildType(ast_ctx, lexer, for_expression));
+}
diff --git a/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCTypeEncodingParser.h b/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCTypeEncodingParser.h
new file mode 100644
index 0000000000000..2d3fa4ab7d200
--- /dev/null
+++ b/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCTypeEncodingParser.h
@@ -0,0 +1,103 @@
+//===-- GNUstepObjCTypeEncodingParser.h -------------------------*- C++ -*-===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+#ifndef LLDB_SOURCE_PLUGINS_LANGUAGERUNTIME_OBJC_GNUSTEPOBJCRUNTIME_GNUSTEPOBJCTYPEENCODINGPARSER_H
+#define LLDB_SOURCE_PLUGINS_LANGUAGERUNTIME_OBJC_GNUSTEPOBJCRUNTIME_GNUSTEPOBJCTYPEENCODINGPARSER_H
+
+#include "Plugins/LanguageRuntime/ObjC/ObjCLanguageRuntime.h"
+#include "lldb/lldb-private.h"
+
+#include "llvm/ADT/DenseMap.h"
+#include "llvm/ADT/StringMap.h"
+#include "llvm/ADT/StringRef.h"
+#include "llvm/TargetParser/Triple.h"
+
+#include "clang/AST/ASTContext.h"
+
+#include <string>
+
+namespace lldb_private {
+
+/// Turns an Objective-C type encoding into a CompilerType, for the encodings
+/// libobjc2 stores alongside its ivars, methods and properties.
+///
+/// The grammar is the standard one (documented normatively in libobjc2's
+/// ABIDoc/abi.tex), so this is close to AppleObjCTypeEncodingParser. It
+/// differs in four ways that matter for libobjc2 input:
+///
+/// - Leading method qualifiers ("nNoORVA", libobjc2's encoding2.c) are
+/// skipped. `A` for _Atomic is a libobjc2 addition. `r` is deliberately
+/// not in that set: it is _C_CONST, and is handled as a real qualifier.
+/// - `@"Name"` is always a class name. Apple has to disambiguate it from a
+/// field named "Name", because its runtime emits quoted field names
+/// inside struct encodings; clang's GNUstep output never does, so the
+/// disambiguation would actively mis-parse `{Foo=@"NSString"i}`.
+/// - A class name the runtime cannot resolve yields `id` rather than
+/// tripping an assertion. libobjc2 encodings routinely name classes that
+/// are not realized.
+/// - Record types are cached, so a struct appearing in many encodings does
+/// not mint a fresh (and separately laid out) decl each time.
+///
+/// The parser holds no target state, so it can be constructed from a triple
+/// alone; a runtime is only needed to resolve `@"Name"` for expressions.
+class GNUstepObjCTypeEncodingParser
+ : public ObjCLanguageRuntime::EncodingToType {
+public:
+ /// \param triple selects the data model, which decides how wide `long`
+ /// and friends are.
+ /// \param runtime resolves `@"Name"` when realizing types for expressions;
+ /// without one such a name degrades to `id`.
+ explicit GNUstepObjCTypeEncodingParser(
+ const llvm::Triple &triple, ObjCLanguageRuntime *runtime = nullptr);
+
+ ~GNUstepObjCTypeEncodingParser() override = default;
+
+ CompilerType RealizeType(TypeSystemClang &ast_ctx, const char *name,
+ bool for_expression) override;
+
+private:
+ struct StructElement {
+ std::string name;
+ clang::QualType type;
+ uint32_t bitfield = 0;
+ };
+
+ clang::QualType BuildType(TypeSystemClang &ast_ctx, llvm::StringRef &type,
+ bool for_expression,
+ uint32_t *bitfield_bit_size = nullptr);
+
+ clang::QualType BuildAggregate(TypeSystemClang &ast_ctx,
+ llvm::StringRef &type, bool for_expression,
+ char opener, char closer, uint32_t kind);
+
+ clang::QualType BuildArray(TypeSystemClang &ast_ctx, llvm::StringRef &type,
+ bool for_expression);
+
+ clang::QualType BuildObjCObjectPointerType(TypeSystemClang &ast_ctx,
+ llvm::StringRef &type,
+ bool for_expression);
+
+ StructElement ReadStructElement(TypeSystemClang &ast_ctx,
+ llvm::StringRef &type, bool for_expression);
+
+ ObjCLanguageRuntime *m_runtime;
+
+ /// Recursion depth of the current BuildType call chain.
+ unsigned m_depth = 0;
+
+ /// Records already built, keyed by the AST they belong to and then by the
+ /// exact encoding text they were built from. Both halves are needed:
+ /// RealizeType is called with different ASTContexts, and a QualType is only
+ /// valid in the one that created it.
+ llvm::DenseMap<TypeSystemClang *, llvm::StringMap<clang::QualType>>
+ m_record_cache;
+};
+
+} // namespace lldb_private
+
+#endif // LLDB_SOURCE_PLUGINS_LANGUAGERUNTIME_OBJC_GNUSTEPOBJCRUNTIME_GNUSTEPOBJCTYPEENCODINGPARSER_H
diff --git a/lldb/unittests/LanguageRuntime/ObjC/GNUstepObjCRuntime/CMakeLists.txt b/lldb/unittests/LanguageRuntime/ObjC/GNUstepObjCRuntime/CMakeLists.txt
index 39b22ebd2816e..db7a4ca17b0b4 100644
--- a/lldb/unittests/LanguageRuntime/ObjC/GNUstepObjCRuntime/CMakeLists.txt
+++ b/lldb/unittests/LanguageRuntime/ObjC/GNUstepObjCRuntime/CMakeLists.txt
@@ -1,5 +1,6 @@
add_lldb_unittest(LanguageRuntimeObjCGNUstepTests
GNUstepObjCClassDescriptorTest.cpp
+ GNUstepObjCTypeEncodingParserTest.cpp
LINK_COMPONENTS
Support
@@ -10,6 +11,8 @@ add_lldb_unittest(LanguageRuntimeObjCGNUstepTests
lldbTarget
lldbUtility
lldbPluginGNUstepObjCRuntime
+ lldbPluginExpressionParserClang
+ lldbPluginTypeSystemClang
lldbPluginPlatformLinux
lldbPluginPlatformWindows
)
diff --git a/lldb/unittests/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCTypeEncodingParserTest.cpp b/lldb/unittests/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCTypeEncodingParserTest.cpp
new file mode 100644
index 0000000000000..43e7639d3f42f
--- /dev/null
+++ b/lldb/unittests/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCTypeEncodingParserTest.cpp
@@ -0,0 +1,263 @@
+//===-- GNUstepObjCTypeEncodingParserTest.cpp -----------------------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+#include "Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCTypeEncodingParser.h"
+
+#include "Plugins/TypeSystem/Clang/TypeSystemClang.h"
+#include "TestingSupport/SubsystemRAII.h"
+#include "lldb/Host/FileSystem.h"
+#include "lldb/Host/HostInfo.h"
+#include "lldb/Symbol/CompilerType.h"
+
+#include "gtest/gtest.h"
+
+#include <memory>
+#include <string>
+
+using namespace lldb_private;
+
+namespace {
+
+/// The parser needs no target state, so each data model is just a triple.
+struct DataModel {
+ const char *triple;
+ /// Width of `long` in bits, which is what differs between LP64 and LLP64.
+ unsigned long_bits;
+ /// Width of a pointer in bits, which is what drives aggregate layout.
+ unsigned pointer_bits;
+};
+
+class GNUstepTypeEncodingParserTest
+ : public ::testing::TestWithParam<DataModel> {
+public:
+ void SetUp() override {
+ m_triple = llvm::Triple(GetParam().triple);
+ m_ast_sp = std::make_shared<TypeSystemClang>("test ASTContext", m_triple);
+ // No runtime: `@"Name"` has no DeclVendor to resolve against and must
+ // degrade to `id` rather than crash.
+ m_parser = std::make_unique<GNUstepObjCTypeEncodingParser>(m_triple);
+ }
+
+ void TearDown() override {
+ m_parser.reset();
+ m_ast_sp.reset();
+ }
+
+ /// Realizes \p encoding and returns its type name, or "" if it failed.
+ std::string Realize(llvm::StringRef encoding, bool for_expression = false) {
+ CompilerType type = m_parser->RealizeType(*m_ast_sp, encoding.str().c_str(),
+ for_expression);
+ if (!type)
+ return "";
+ return type.GetTypeName().GetString();
+ }
+
+ std::optional<uint64_t> SizeOf(llvm::StringRef encoding) {
+ CompilerType type =
+ m_parser->RealizeType(*m_ast_sp, encoding.str().c_str(), false);
+ if (!type)
+ return std::nullopt;
+ llvm::Expected<uint64_t> size = type.GetByteSize(nullptr);
+ if (!size) {
+ llvm::consumeError(size.takeError());
+ return std::nullopt;
+ }
+ return *size;
+ }
+
+ SubsystemRAII<FileSystem, HostInfo> subsystems;
+ llvm::Triple m_triple;
+ std::shared_ptr<TypeSystemClang> m_ast_sp;
+ std::unique_ptr<GNUstepObjCTypeEncodingParser> m_parser;
+};
+
+TEST_P(GNUstepTypeEncodingParserTest, Scalars) {
+ EXPECT_EQ(Realize("c"), "char");
+ EXPECT_EQ(Realize("i"), "int");
+ EXPECT_EQ(Realize("s"), "short");
+ EXPECT_EQ(Realize("q"), "long long");
+ EXPECT_EQ(Realize("C"), "unsigned char");
+ EXPECT_EQ(Realize("I"), "unsigned int");
+ EXPECT_EQ(Realize("S"), "unsigned short");
+ EXPECT_EQ(Realize("Q"), "unsigned long long");
+ EXPECT_EQ(Realize("f"), "float");
+ EXPECT_EQ(Realize("d"), "double");
+ EXPECT_EQ(Realize("B"), "bool");
+ EXPECT_EQ(Realize("v"), "void");
+ EXPECT_EQ(Realize("*"), "char *");
+}
+
+// clang emits 'l' only where `long` is 32 bits and 'q' otherwise
+// (ASTContext::getObjCEncodingForPrimitiveType), so 'l' always means 32 bits
+// no matter which data model the target uses.
+TEST_P(GNUstepTypeEncodingParserTest, LongIsAlways32Bits) {
+ EXPECT_EQ(SizeOf("l"), std::optional<uint64_t>(4u));
+ EXPECT_EQ(SizeOf("L"), std::optional<uint64_t>(4u));
+ EXPECT_EQ(SizeOf("q"), std::optional<uint64_t>(8u));
+ EXPECT_EQ(SizeOf("Q"), std::optional<uint64_t>(8u));
+}
+
+TEST_P(GNUstepTypeEncodingParserTest, LongDouble) {
+ // Unhandled by Apple's parser; an encoding mentioning one would otherwise
+ // fail and take the whole method with it.
+ EXPECT_EQ(Realize("D"), "long double");
+}
+
+TEST_P(GNUstepTypeEncodingParserTest, ObjCBuiltins) {
+ EXPECT_EQ(Realize("@"), "id");
+ EXPECT_EQ(Realize("#"), "Class");
+ EXPECT_EQ(Realize(":"), "SEL");
+ // A block. The extended form carries a signature nothing here needs.
+ EXPECT_EQ(Realize("@?"), "id");
+}
+
+// Without a DeclVendor the class name cannot be resolved. That must degrade
+// to `id`, not assert - libobjc2 encodings routinely name classes that are
+// not realized.
+TEST_P(GNUstepTypeEncodingParserTest, UnknownClassNameDegradesToId) {
+ EXPECT_EQ(Realize("@\"NSString\""), "id");
+ EXPECT_EQ(Realize("@\"NSString\"", /*for_expression=*/true), "id");
+ EXPECT_EQ(Realize("@\"<NSCopying>\"", /*for_expression=*/true), "id");
+ EXPECT_EQ(Realize("@\"NSFoo<NSCopying>\"", /*for_expression=*/true), "id");
+}
+
+TEST_P(GNUstepTypeEncodingParserTest, Pointers) {
+ EXPECT_EQ(Realize("^i"), "int *");
+ EXPECT_EQ(Realize("^^v"), "void **");
+ EXPECT_EQ(Realize("^?"), "void *");
+ EXPECT_EQ(Realize("ri"), "const int");
+ EXPECT_EQ(Realize("^ri"), "const int *");
+}
+
+TEST_P(GNUstepTypeEncodingParserTest, Arrays) {
+ EXPECT_EQ(Realize("[10i]"), "int[10]");
+ EXPECT_EQ(SizeOf("[10i]"), std::optional<uint64_t>(40u));
+ EXPECT_EQ(SizeOf("[3[4f]]"), std::optional<uint64_t>(48u));
+}
+
+TEST_P(GNUstepTypeEncodingParserTest, ArrayExtentDoesNotWrap) {
+ // The encoding is inferior data, so an extent that does not fit must fail
+ // rather than wrap: 10000000000 truncated to 32 bits is 1410065408, and an
+ // array of that many ints would be accepted as if it were the real size.
+ EXPECT_EQ(Realize("[10000000000i]"), "");
+ EXPECT_EQ(Realize("[4294967296i]"), "");
+ // One below the limit still parses, so the bound is not off by one.
+ EXPECT_EQ(Realize("[4294967295i]"), "int[4294967295]");
+}
+
+TEST_P(GNUstepTypeEncodingParserTest, Structs) {
+ EXPECT_EQ(Realize("{CGPoint=dd}"), "CGPoint");
+ EXPECT_EQ(SizeOf("{CGPoint=dd}"), std::optional<uint64_t>(16u));
+ // An opaque struct, which is how libobjc2 spells a forward declaration.
+ EXPECT_EQ(Realize("{CGImage=}"), "CGImage");
+ EXPECT_EQ(Realize("^{CGImage=}"), "CGImage *");
+ EXPECT_EQ(Realize("(U=ic)"), "U");
+}
+
+// This is the encoding shape libobjc2 actually emits for a struct ivar,
+// verified against the shipped library: members are expanded positionally
+// with no field names.
+TEST_P(GNUstepTypeEncodingParserTest, StructWithoutFieldNames) {
+ EXPECT_EQ(Realize("{_NSRange=QQ}"), "_NSRange");
+ EXPECT_EQ(SizeOf("{_NSRange=QQ}"), std::optional<uint64_t>(16u));
+}
+
+// A quoted string after '@' is a class name, never a field name: unlike
+// Apple's runtime, clang's GNUstep output does not emit quoted field names,
+// so peeking to disambiguate would mis-parse the `i` here as evidence that
+// "NSString" was a field name.
+TEST_P(GNUstepTypeEncodingParserTest, ClassNameInStructIsNotAFieldName) {
+ // An object pointer followed by an int: the size follows the pointer
+ // width, so LLP64 and LP64 agree here and only ILP32 differs.
+ EXPECT_EQ(SizeOf("{Foo=@\"NSString\"i}"),
+ std::optional<uint64_t>(GetParam().pointer_bits == 32 ? 8u : 16u));
+}
+
+TEST_P(GNUstepTypeEncodingParserTest, Bitfields) {
+ // A bitfield outside a struct has nowhere to report its width.
+ EXPECT_EQ(Realize("b3"), "");
+ // Bitfields are given an unsigned int base type, so eight bits of them
+ // still occupy one int rather than one byte.
+ EXPECT_EQ(SizeOf("{Flags=b1b1b6}"), std::optional<uint64_t>(4u));
+}
+
+// clang emits these for declarations such as `- (oneway void)release`.
+// Leaving them unhandled would drop the whole method.
+TEST_P(GNUstepTypeEncodingParserTest, SkipsArgumentQualifiers) {
+ EXPECT_EQ(Realize("Vv"), "void");
+ EXPECT_EQ(Realize("ni"), "int");
+ EXPECT_EQ(Realize("Ni"), "int");
+ EXPECT_EQ(Realize("oi"), "int");
+ EXPECT_EQ(Realize("Oi"), "int");
+ EXPECT_EQ(Realize("Ri"), "int");
+ // libobjc2's addition, for _Atomic.
+ EXPECT_EQ(Realize("Ai"), "int");
+ // 'r' is const, not an argument qualifier, and must keep its meaning.
+ EXPECT_NE(Realize("ri"), "int");
+}
+
+// The same struct appearing twice must yield the same type rather than two
+// separately laid out decls sharing a name.
+TEST_P(GNUstepTypeEncodingParserTest, RecordsAreCached) {
+ CompilerType first = m_parser->RealizeType(*m_ast_sp, "{CGPoint=dd}", false);
+ CompilerType second = m_parser->RealizeType(*m_ast_sp, "{CGPoint=dd}", false);
+ ASSERT_TRUE(first);
+ EXPECT_EQ(first.GetOpaqueQualType(), second.GetOpaqueQualType());
+}
+
+// Two different types can legitimately share a tag - an opaque declaration
+// and its definition - so the cache must not collapse them.
+TEST_P(GNUstepTypeEncodingParserTest, SameTagDifferentBodyIsNotCached) {
+ CompilerType opaque = m_parser->RealizeType(*m_ast_sp, "{Foo=}", false);
+ CompilerType defined = m_parser->RealizeType(*m_ast_sp, "{Foo=ii}", false);
+ ASSERT_TRUE(opaque);
+ ASSERT_TRUE(defined);
+ EXPECT_NE(opaque.GetOpaqueQualType(), defined.GetOpaqueQualType());
+}
+
+// Nothing here may crash, hang, or recurse without bound. The encoding comes
+// from inferior memory, so it cannot be assumed well-formed.
+TEST_P(GNUstepTypeEncodingParserTest, MalformedInputIsRejected) {
+ for (llvm::StringRef bad :
+ {"", "{", "{Foo=", "{Foo=ii", "[10", "[10i", "^", "b", "@\"", "(U=ic",
+ "{Foo=@\"NSString", "]", ")", "}", "="}) {
+ // No assertion on the result beyond "it returned"; the point is that it
+ // terminates and does not trip an assertion inside clang.
+ Realize(bad);
+ }
+}
+
+TEST_P(GNUstepTypeEncodingParserTest, DeeplyNestedInputTerminates) {
+ EXPECT_EQ(Realize(std::string(4096, '^') + "v"), "");
+ EXPECT_EQ(Realize(std::string(1000, '{')), "");
+}
+
+// Truncating a valid encoding at every length is the cheapest way to reach
+// the partial states a hand-written parser gets wrong.
+TEST_P(GNUstepTypeEncodingParserTest, EveryTruncationTerminates) {
+ const std::string full =
+ "{Outer=@\"NSString\"^{Inner=ii}[4f]b3q{_NSRange=QQ}}";
+ for (size_t n = 1; n <= full.size(); ++n)
+ Realize(llvm::StringRef(full).take_front(n));
+}
+
+INSTANTIATE_TEST_SUITE_P(
+ DataModels, GNUstepTypeEncodingParserTest,
+ ::testing::Values(DataModel{"x86_64-pc-linux", 64, 64},
+ // Windows is LLP64: pointers are 64 bits, long is 32.
+ DataModel{"x86_64-pc-windows-msvc", 32, 64},
+ DataModel{"i386-pc-linux", 32, 32}),
+ [](const ::testing::TestParamInfo<DataModel> &info) {
+ std::string name = info.param.triple;
+ for (char &c : name)
+ if (!std::isalnum(static_cast<unsigned char>(c)))
+ c = '_';
+ return name;
+ });
+
+} // namespace
>From 79e432f88a7ccb0a3e3a8b4dbd6dea5eb5ca21fd Mon Sep 17 00:00:00 2001
From: Rob <robk at robk.dev>
Date: Tue, 18 Aug 2026 09:33:05 +0100
Subject: [PATCH 31/49] [lldb][GNUstep] Read ivars from libobjc2 metadata
Implements GetNumIVars/GetIVarAtIndex by walking libobjc2's objc_ivar_list
(ivar.h), which is what makes a class's ivars reachable without debug info.
Types come from the encoding parser; name, offset and size are useful on
their own, so an encoding that does not realize leaves the ivar in place
with an empty type rather than dropping it.
The metadata is inferior data and cannot be assumed well-formed, so the
whole list is rejected rather than partially trusted when anything about it
does not add up. Two of the guards are not obvious:
- objc_resolve_class sets objc_class_flag_resolved *before* it calls
objc_compute_ivar_offsets (class_table.c), so the flag alone does not mean
the offsets are absolute. Stop anywhere in that window - including a +load
breakpoint while a sibling class is resolving - and the stored offsets are
still relative to the start of this class's own ivars.
objc_compute_ivar_offsets only runs while instance_size is non-positive
and leaves it positive (ivar.c), so requiring a positive instance_size is
what closes that window.
- libobjc2 stores a *pointer* to each offset so it can rewrite it in place,
but between class_addIvar and objc_registerClassPair the field holds the
offset inline, as a small integer masquerading as a pointer (runtime.c).
The element stride is read from the list rather than assumed, because that
is how the runtime walks the array.
GetTypeBitSize is overridden at the same time, and deliberately in the same
commit: the inherited implementation derives a class's size from the ivar
list as the end of the last ivar, which becomes reachable the moment ivars
are readable. That is wrong for libobjc2 twice over - it ignores trailing
padding, and objc_class::ivars holds only the class's own ivars, so a class
declaring none would report no size at all while its sibling reported one.
instance_size is the runtime's own answer, so use it and decline when it is
not yet available. Note that with debug info present nothing currently
consults this path; the override is what keeps the inherited computation
from answering once it becomes reachable.
IsKVO is also implemented, over objc_class_flag_hidden_class. That is
libobjc2's analogue of an Apple KVO subclass - a class object_getClass()
walks past (runtime.c) - so the inherited GetNonKVOClassDescriptor does the
rest without new machinery.
33 tests across the three data models, covering the accepted shape and each
rejection: a declared stride that is honoured and one that is too small, the
resolved-but-not-laid-out window, an implausible count, an ivar that does
not fit in the object, and an inline offset.
Assisted-by: Claude Opus 5
---
.../GNUstepObjCClassDescriptor.cpp | 187 ++++++++++++++
.../GNUstepObjCClassDescriptor.h | 62 +++++
.../GNUstepObjCRuntime/GNUstepObjCRuntime.cpp | 31 ++-
.../GNUstepObjCRuntime/GNUstepObjCRuntime.h | 12 +
.../GNUstepObjCClassDescriptorTest.cpp | 229 ++++++++++++++++++
5 files changed, 516 insertions(+), 5 deletions(-)
diff --git a/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCClassDescriptor.cpp b/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCClassDescriptor.cpp
index 061320b590f86..78031e0ca604e 100644
--- a/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCClassDescriptor.cpp
+++ b/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCClassDescriptor.cpp
@@ -38,6 +38,18 @@ using namespace lldb_private;
// Flags from libobjc2's `enum objc_class_flags` (class.h).
static constexpr uint64_t g_class_flag_meta = 1ULL << 0;
static constexpr uint64_t g_class_flag_resolved = 1ULL << 9;
+static constexpr uint64_t g_class_flag_hidden = 1ULL << 12;
+
+// Bounds on what a well-formed ivar list can say about itself. These are not
+// tuning knobs: the values come from inferior memory, so a corrupted or
+// misidentified structure must not be able to drive an unbounded read.
+static constexpr uint32_t g_max_ivars = 4096;
+static constexpr size_t g_max_type_encoding_length = 1024;
+
+// Element strides are read from the metadata rather than assumed, because
+// that is how the runtime walks these arrays - but a stride this large is
+// not something a compiler emitted.
+static constexpr uint64_t g_max_element_stride = 1024;
// An upper bound for plausible class names. A string that does not terminate
// within this many bytes is not a class name, and stopping there keeps a
@@ -55,6 +67,8 @@ struct ClassLayout {
uint64_t name_offset;
uint64_t info_offset;
uint64_t instance_size_offset;
+ uint64_t ivars_offset;
+ uint64_t methods_offset;
};
ClassLayout GetClassLayout(Process &process) {
@@ -70,6 +84,13 @@ ClassLayout GetClassLayout(Process &process) {
layout.name_offset = 2 * layout.pointer_size;
layout.info_offset = 3 * layout.pointer_size + layout.long_size;
layout.instance_size_offset = 3 * layout.pointer_size + 2 * layout.long_size;
+ // `ivars` is the first field after the three `long`s, so it picks up
+ // whatever tail padding the target's alignment requires. Cross-check: two
+ // pointers further on is `dtable`, which libobjc2 pins per data model in
+ // asmconstants.h and asserts in dtable.c.
+ layout.ivars_offset = llvm::alignTo(
+ 3 * layout.pointer_size + 3 * layout.long_size, layout.pointer_size);
+ layout.methods_offset = layout.ivars_offset + layout.pointer_size;
return layout;
}
} // namespace
@@ -123,6 +144,7 @@ void GNUstepObjCClassDescriptor::Read() {
// both directions is what keeps an arbitrary readable address from being
// accepted as a class.
const bool is_meta = (info & g_class_flag_meta) != 0;
+ m_is_hidden = (info & g_class_flag_hidden) != 0;
if (!is_meta) {
const uint64_t metaclass_info = process_sp->ReadUnsignedIntegerFromMemory(
metaclass + layout.info_offset, layout.long_size, 0, error);
@@ -133,6 +155,7 @@ void GNUstepObjCClassDescriptor::Read() {
// Only a resolved class has a real superclass pointer and instance size;
// see the class documentation.
const bool resolved = (info & g_class_flag_resolved) != 0;
+ m_resolved = resolved;
if (resolved) {
const int64_t instance_size = process_sp->ReadSignedIntegerFromMemory(
m_isa + layout.instance_size_offset, layout.long_size, 0, error);
@@ -149,6 +172,170 @@ void GNUstepObjCClassDescriptor::Read() {
m_valid = true;
}
+std::vector<GNUstepObjCClassDescriptor::RawIvar>
+GNUstepObjCClassDescriptor::ReadIvarList() const {
+ std::vector<RawIvar> ivars;
+ ProcessSP process_sp = m_process_wp.lock();
+ if (!process_sp || !m_valid || m_is_meta)
+ return ivars;
+
+ // See the header: the resolved flag is set before the offsets are computed,
+ // and a positive instance_size is what says the computation finished.
+ if (!m_resolved || m_instance_size == 0)
+ return ivars;
+
+ const ClassLayout layout = GetClassLayout(*process_sp);
+ const uint32_t ptr_size = layout.pointer_size;
+
+ Status error;
+ const addr_t list_addr =
+ process_sp->ReadPointerFromMemory(m_isa + layout.ivars_offset, error);
+ if (error.Fail() || list_addr == 0 || list_addr == LLDB_INVALID_ADDRESS ||
+ list_addr % ptr_size != 0)
+ return ivars;
+
+ // struct objc_ivar_list { int count; size_t size; struct objc_ivar[]; }
+ // (ivar.h). `size` is the element stride and is read rather than assumed,
+ // because the runtime uses it to walk the array.
+ const uint64_t count_offset = 0;
+ const uint64_t size_offset = llvm::alignTo(sizeof(uint32_t), ptr_size);
+ const uint64_t entries_offset = size_offset + ptr_size;
+
+ const int64_t count = process_sp->ReadSignedIntegerFromMemory(
+ list_addr + count_offset, sizeof(uint32_t), 0, error);
+ if (error.Fail() || count <= 0 || count > g_max_ivars)
+ return ivars;
+
+ const uint64_t stride = process_sp->ReadUnsignedIntegerFromMemory(
+ list_addr + size_offset, ptr_size, 0, error);
+ // An element smaller than the struct this code knows how to read would make
+ // every field after the first come from the wrong place.
+ const uint64_t min_stride = 3 * ptr_size + 2 * sizeof(uint32_t);
+ if (error.Fail() || stride < min_stride || stride > g_max_element_stride)
+ return ivars;
+
+ Log *log = GetLog(LLDBLog::Types);
+ ivars.reserve(count);
+ for (int64_t i = 0; i < count; ++i) {
+ // struct objc_ivar { const char *name; const char *type; int *offset;
+ // uint32_t size; uint32_t flags; }
+ const addr_t entry = list_addr + entries_offset + i * stride;
+
+ const addr_t name_ptr = process_sp->ReadPointerFromMemory(entry, error);
+ if (error.Fail() || name_ptr == 0 || name_ptr == LLDB_INVALID_ADDRESS)
+ return {};
+ const addr_t type_ptr =
+ process_sp->ReadPointerFromMemory(entry + ptr_size, error);
+ if (error.Fail() || type_ptr == 0 || type_ptr == LLDB_INVALID_ADDRESS)
+ return {};
+ const addr_t offset_ptr =
+ process_sp->ReadPointerFromMemory(entry + 2 * ptr_size, error);
+ // libobjc2 stores a pointer to the offset, not the offset itself, so that
+ // the runtime can rewrite it in place. Between class_addIvar and
+ // objc_registerClassPair (runtime.c) the field briefly holds a small
+ // integer masquerading as a pointer, which this alignment check rejects.
+ if (error.Fail() || offset_ptr == 0 || offset_ptr == LLDB_INVALID_ADDRESS ||
+ offset_ptr % sizeof(int32_t) != 0)
+ return {};
+ const uint32_t ivar_size = process_sp->ReadUnsignedIntegerFromMemory(
+ entry + 3 * ptr_size, sizeof(uint32_t), 0, error);
+ if (error.Fail())
+ return {};
+
+ // The offset is a 32-bit signed int wherever libobjc2 runs.
+ const int64_t offset = process_sp->ReadSignedIntegerFromMemory(
+ offset_ptr, sizeof(int32_t), 0, error);
+ if (error.Fail())
+ return {};
+
+ char name_buffer[g_max_class_name_length];
+ const size_t name_length = process_sp->ReadCStringFromMemory(
+ name_ptr, name_buffer, sizeof(name_buffer), error);
+ if (error.Fail() || name_length == 0 ||
+ name_length >= sizeof(name_buffer) - 1)
+ return {};
+
+ char type_buffer[g_max_type_encoding_length];
+ const size_t type_length = process_sp->ReadCStringFromMemory(
+ type_ptr, type_buffer, sizeof(type_buffer), error);
+ if (error.Fail() || type_length >= sizeof(type_buffer) - 1)
+ return {};
+
+ // An ivar that does not fit inside the object is evidence that this is
+ // not really an ivar list, so discard the whole thing rather than
+ // reporting a plausible-looking subset.
+ if (offset < 0 ||
+ static_cast<uint64_t>(offset) + ivar_size > m_instance_size) {
+ LLDB_LOG(log,
+ "GNUstep ivar {0} of {1} lies outside the object "
+ "(offset {2}, size {3}, instance size {4}); ignoring the list",
+ name_buffer, m_name, offset, ivar_size, m_instance_size);
+ return {};
+ }
+
+ RawIvar ivar;
+ ivar.name = ConstString(name_buffer);
+ ivar.type_encoding.assign(type_buffer, type_length);
+ ivar.offset = static_cast<int32_t>(offset);
+ ivar.size = ivar_size;
+ ivars.push_back(std::move(ivar));
+ }
+ return ivars;
+}
+
+void GNUstepObjCClassDescriptor::GetIVarInformation() {
+ if (m_ivars_filled)
+ return;
+ std::lock_guard<std::recursive_mutex> guard(m_ivars_mutex);
+
+ std::vector<RawIvar> raw = ReadIvarList();
+
+ // A descriptor is kept in the runtime's ISA map for the life of the
+ // process, so an answer latched here is permanent. Latch only a positive
+ // one: every failure inside ReadIvarList - a class caught mid-resolution,
+ // a list briefly dangling while another thread grows it - also yields an
+ // empty vector, and caching that would report "this class has no ivars"
+ // for the rest of the session. A class that genuinely declares none is
+ // cheap to re-read.
+ if (raw.empty())
+ return;
+ m_ivars_filled = true;
+
+ ProcessSP process_sp = m_process_wp.lock();
+ ObjCLanguageRuntime::EncodingToTypeSP encoding_to_type_sp;
+ if (process_sp) {
+ if (ObjCLanguageRuntime *runtime = ObjCLanguageRuntime::Get(*process_sp))
+ encoding_to_type_sp = runtime->GetEncodingToType();
+ }
+
+ m_ivars.reserve(raw.size());
+ for (const RawIvar &ivar : raw) {
+ iVarDescriptor descriptor;
+ descriptor.m_name = ivar.name;
+ descriptor.m_size = ivar.size;
+ descriptor.m_offset = ivar.offset;
+ // The name, offset and size are useful on their own, so an encoding that
+ // does not realize leaves the ivar in place with an empty type rather
+ // than dropping it.
+ if (encoding_to_type_sp)
+ descriptor.m_type = encoding_to_type_sp->RealizeType(
+ ivar.type_encoding.c_str(), /*for_expression=*/false);
+ m_ivars.push_back(std::move(descriptor));
+ }
+}
+
+size_t GNUstepObjCClassDescriptor::GetNumIVars() {
+ GetIVarInformation();
+ return m_ivars.size();
+}
+
+ObjCLanguageRuntime::ClassDescriptor::iVarDescriptor
+GNUstepObjCClassDescriptor::GetIVarAtIndex(size_t idx) {
+ if (idx >= GetNumIVars())
+ return iVarDescriptor();
+ return m_ivars[idx];
+}
+
ObjCLanguageRuntime::ClassDescriptorSP
GNUstepObjCClassDescriptor::GetSuperclass() {
if (!m_valid || m_superclass_isa == 0)
diff --git a/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCClassDescriptor.h b/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCClassDescriptor.h
index 25ef25c22400b..19b51856e0f21 100644
--- a/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCClassDescriptor.h
+++ b/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCClassDescriptor.h
@@ -14,7 +14,11 @@
#include "lldb/lldb-forward.h"
#include "lldb/lldb-types.h"
+#include <functional>
+#include <mutex>
#include <optional>
+#include <string>
+#include <vector>
namespace lldb_private {
@@ -80,7 +84,54 @@ class GNUstepObjCClassDescriptor : public ObjCLanguageRuntime::ClassDescriptor {
/// that resolves to one is a Class, not an object.
bool IsMetaclass() const { return m_is_meta; }
+ /// True once libobjc2 has resolved the class: before that its superclass
+ /// pointer may still hold a name, its instance size is the negated size of
+ /// its own ivars, and its ivar offsets are not yet absolute. A descriptor
+ /// built in that state is a snapshot of it, so callers that cache
+ /// descriptors must not cache an unresolved one.
+ bool IsResolved() const { return m_resolved; }
+
+ /// libobjc2's analogue of an Apple KVO subclass: a class the runtime hides
+ /// from object_getClass(), which walks past it to the first visible
+ /// superclass (runtime.c). Associated-object classes are the in-tree
+ /// producer (associate.m).
+ bool IsKVO() override { return m_is_hidden; }
+
+ size_t GetNumIVars() override;
+
+ iVarDescriptor GetIVarAtIndex(size_t idx) override;
+
protected:
+ /// One entry of libobjc2's `objc_ivar_list`, as read from memory. The type
+ /// is kept as its encoding here; turning it into a CompilerType needs the
+ /// runtime, which this class deliberately does not depend on.
+ struct RawIvar {
+ ConstString name;
+ std::string type_encoding;
+ int32_t offset = 0;
+ uint32_t size = 0;
+ };
+
+ /// Reads this class's own ivars, or an empty list if the class has none or
+ /// its metadata is not yet trustworthy. Pure memory reads.
+ ///
+ /// The offsets libobjc2 stores are only meaningful once the runtime has
+ /// computed them, and `objc_class_flag_resolved` alone does not say that:
+ /// objc_resolve_class sets the flag *before* calling
+ /// objc_compute_ivar_offsets (class_table.c), so a stop anywhere in that
+ /// window - including a +load breakpoint while a sibling class is being
+ /// resolved - would otherwise yield offsets relative to the start of this
+ /// class's own ivars rather than to the object. objc_compute_ivar_offsets
+ /// only runs while instance_size is non-positive and leaves it positive
+ /// (ivar.c), so requiring a positive instance_size closes that window.
+ std::vector<RawIvar> ReadIvarList() const;
+
+ /// Realized ivars, filled on first use. Not cached while the class is
+ /// unresolved: a descriptor lives in the runtime's ISA map for the life of
+ /// the process, so caching an unresolved class's ivars would pin
+ /// class-relative offsets forever.
+ void GetIVarInformation();
+
/// Parse `struct objc_class` at m_isa. Called from the constructor; sets
/// m_valid only if the structure passes the consistency checks that keep a
/// stray pointer into readable memory from being reported as a class.
@@ -93,7 +144,18 @@ class GNUstepObjCClassDescriptor : public ObjCLanguageRuntime::ClassDescriptor {
ObjCLanguageRuntime::ObjCISA m_metaclass_isa = 0;
uint64_t m_instance_size = 0;
bool m_is_meta = false;
+ bool m_is_hidden = false;
+ bool m_resolved = false;
bool m_valid = false;
+
+ std::vector<iVarDescriptor> m_ivars;
+ bool m_ivars_filled = false;
+ /// Guards the two above while they are filled, as
+ /// AppleObjCClassDescriptorV2::iVarsStorage does. Recursive for the same
+ /// reason: realizing an ivar's type can re-enter this runtime. Note that
+ /// neither implementation protects a re-entrant *reader* - both return
+ /// early on the filled flag - so this is about concurrent access.
+ std::recursive_mutex m_ivars_mutex;
};
/// Class descriptor for libobjc2 "small objects" (tagged pointers). The
diff --git a/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCRuntime.cpp b/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCRuntime.cpp
index 2008e542720d8..e03130c9e1856 100644
--- a/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCRuntime.cpp
+++ b/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCRuntime.cpp
@@ -1131,7 +1131,13 @@ void GNUstepObjCRuntime::AddClassesFromModule(const ModuleSP &module_sp) {
name.consume_front(prefix);
auto descriptor_sp = std::make_shared<GNUstepObjCClassDescriptor>(
m_process->shared_from_this(), isa);
- if (descriptor_sp->IsValid())
+ // A descriptor is a snapshot taken when it was built, and the map holds
+ // it for the life of the process. Before the runtime resolves a class its
+ // superclass pointer and instance size are not yet meaningful, and this
+ // sweep runs at the loader's rendezvous stop - which for a dlopen'd image
+ // is *before* its __objc_load. Caching one then would pin an empty class
+ // permanently, so leave it to be rebuilt on demand.
+ if (descriptor_sp->IsValid() && descriptor_sp->IsResolved())
AddClass(isa, descriptor_sp, name.str().c_str());
}
}
@@ -1157,6 +1163,18 @@ GNUstepObjCRuntime::GetClassDescriptor(ValueObject &in_value) {
return ObjCLanguageRuntime::GetClassDescriptor(in_value);
}
+std::optional<uint64_t>
+GNUstepObjCRuntime::GetTypeBitSize(const CompilerType &compiler_type) {
+ ClassDescriptorSP descriptor_sp =
+ GetClassDescriptorFromClassName(compiler_type.GetTypeName());
+ if (!descriptor_sp || !descriptor_sp->IsValid())
+ return std::nullopt;
+ const uint64_t instance_size = descriptor_sp->GetInstanceSize();
+ if (instance_size == 0)
+ return std::nullopt;
+ return instance_size * 8;
+}
+
ObjCLanguageRuntime::EncodingToTypeSP
GNUstepObjCRuntime::GetEncodingToType() {
if (!m_encoding_to_type_sp)
@@ -1186,10 +1204,13 @@ GNUstepObjCRuntime::GetClassDescriptorFromISA(ObjCISA isa) {
// class methods look like instance methods and which has no ivars. Cache
// metaclasses by ISA all the same, so they are not re-parsed on every
// lookup.
- if (descriptor_sp->IsMetaclass())
- AddClass(isa, descriptor_sp);
- else
- AddClass(isa, descriptor_sp, descriptor_sp->GetClassName().GetCString());
+ // Only a resolved class is safe to cache; see AddClassesFromModule.
+ if (descriptor_sp->IsResolved()) {
+ if (descriptor_sp->IsMetaclass())
+ AddClass(isa, descriptor_sp);
+ else
+ AddClass(isa, descriptor_sp, descriptor_sp->GetClassName().GetCString());
+ }
return descriptor_sp;
}
diff --git a/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCRuntime.h b/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCRuntime.h
index 64ee7bf7f8a8e..a4354a22979b6 100644
--- a/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCRuntime.h
+++ b/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCRuntime.h
@@ -148,6 +148,18 @@ class GNUstepObjCRuntime : public lldb_private::ObjCLanguageRuntime {
/// the types it returns are only valid in the AST it created them in.
EncodingToTypeSP GetEncodingToType() override;
+ /// Size of an Objective-C class, in bits.
+ ///
+ /// The inherited implementation derives this from the ivar list, as the end
+ /// of the last ivar. That is wrong for libobjc2 twice over: it ignores
+ /// trailing padding, and `objc_class::ivars` holds only the class's *own*
+ /// ivars, so a class that declares none would report nothing at all while
+ /// its sibling reported a size. The runtime already knows the answer -
+ /// `instance_size` is the true body size once the class is resolved - so
+ /// use that, and decline rather than guess when it is not yet available.
+ std::optional<uint64_t>
+ GetTypeBitSize(const CompilerType &compiler_type) override;
+
/// Lazily-built FunctionCaller for a utility function that resolves a
/// method implementation via libobjc2's
/// `IMP objc_msg_lookup(id receiver, SEL selector)`, used by the
diff --git a/lldb/unittests/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCClassDescriptorTest.cpp b/lldb/unittests/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCClassDescriptorTest.cpp
index c19b783a57632..7e567b4207dd4 100644
--- a/lldb/unittests/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCClassDescriptorTest.cpp
+++ b/lldb/unittests/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCClassDescriptorTest.cpp
@@ -395,4 +395,233 @@ INSTANTIATE_TEST_SUITE_P(
return name;
});
+// --- Ivar list tests -------------------------------------------------------
+//
+// The addresses below sit in the same fake block as the class structures.
+constexpr addr_t g_ivar_list_addr = FakeProcess::g_base_addr + 0x600;
+constexpr addr_t g_ivar_names_addr = FakeProcess::g_base_addr + 0x700;
+constexpr addr_t g_ivar_offsets_addr = FakeProcess::g_base_addr + 0x800;
+
+/// One ivar to lay out. `offset` is the value the offset *variable* holds,
+/// which is what libobjc2 points at rather than storing inline.
+struct IvarSpec {
+ const char *name;
+ const char *type_encoding;
+ int32_t offset;
+ uint32_t size;
+};
+
+class GNUstepIvarTest : public GNUstepClassDescriptorTest {
+public:
+ /// sizeof(struct objc_ivar): three pointers then two uint32_t.
+ uint64_t IvarStride() const {
+ return 3 * PointerSize() + 2 * sizeof(uint32_t);
+ }
+
+ /// Offset of the first element of an objc_ivar_list: {int count; size_t
+ /// size;} with the size_t at its natural alignment.
+ uint64_t IvarListHeaderSize() const {
+ return AlignUp(sizeof(uint32_t), PointerSize()) + PointerSize();
+ }
+
+ /// Writes an objc_ivar_list plus the strings and offset variables it
+ /// points at, and returns its address.
+ addr_t WriteIvarList(llvm::ArrayRef<IvarSpec> ivars, uint32_t count_override,
+ uint64_t stride_override,
+ uint64_t inline_offset_value = 0) {
+ FakeProcess &process = GetProcess();
+ const uint64_t stride = stride_override ? stride_override : IvarStride();
+ process.WriteInteger(g_ivar_list_addr,
+ count_override ? count_override : ivars.size(),
+ sizeof(uint32_t));
+ process.WriteInteger(g_ivar_list_addr +
+ AlignUp(sizeof(uint32_t), PointerSize()),
+ stride, PointerSize());
+
+ addr_t name_addr = g_ivar_names_addr;
+ addr_t offset_var = g_ivar_offsets_addr;
+ for (size_t i = 0; i < ivars.size(); ++i) {
+ const addr_t entry = g_ivar_list_addr + IvarListHeaderSize() + i * stride;
+ process.WriteCString(name_addr, ivars[i].name);
+ const addr_t type_addr = name_addr + 0x20;
+ process.WriteCString(type_addr, ivars[i].type_encoding);
+ process.WriteInteger(offset_var, static_cast<uint64_t>(ivars[i].offset),
+ sizeof(uint32_t));
+
+ process.WriteInteger(entry, name_addr, PointerSize());
+ process.WriteInteger(entry + PointerSize(), type_addr, PointerSize());
+ process.WriteInteger(entry + 2 * PointerSize(),
+ inline_offset_value ? inline_offset_value
+ : offset_var,
+ PointerSize());
+ process.WriteInteger(entry + 3 * PointerSize(), ivars[i].size,
+ sizeof(uint32_t));
+ process.WriteInteger(entry + 3 * PointerSize() + sizeof(uint32_t), 0,
+ sizeof(uint32_t));
+
+ name_addr += 0x40;
+ offset_var += sizeof(uint32_t);
+ }
+ return g_ivar_list_addr;
+ }
+
+ /// Lays out a resolved class with the given ivars and returns a descriptor.
+ GNUstepObjCClassDescriptor
+ MakeClassWithIvars(llvm::ArrayRef<IvarSpec> ivars, uint64_t instance_size,
+ uint64_t info = g_flag_resolved,
+ uint32_t count_override = 0, uint64_t stride_override = 0,
+ uint64_t inline_offset_value = 0) {
+ FakeProcess &process = GetProcess();
+ process.WriteCString(g_name_addr, "Widget");
+ WriteClass(g_metaclass_addr, g_metaclass_addr, 0, g_name_addr,
+ g_flag_meta | g_flag_resolved, 0);
+ WriteClass(g_class_addr, g_metaclass_addr, 0, g_name_addr, info,
+ instance_size);
+ const addr_t list = WriteIvarList(ivars, count_override, stride_override,
+ inline_offset_value);
+ process.WriteInteger(g_class_addr + IvarsOffset(), list, PointerSize());
+ return GNUstepObjCClassDescriptor(m_process_sp, g_class_addr);
+ }
+};
+
+TEST_P(GNUstepIvarTest, ReadsIvars) {
+ // Deliberately not pointer-aligned sizes, so a stride computed from the
+ // wrong data model lands on the wrong entry.
+ const IvarSpec ivars[] = {
+ {"_count", "i", 0, 4},
+ {"_name", "@", static_cast<int32_t>(PointerSize()), PointerSize()},
+ {"_flag", "c", static_cast<int32_t>(2 * PointerSize()), 1},
+ };
+ GNUstepObjCClassDescriptor descriptor =
+ MakeClassWithIvars(ivars, 3 * PointerSize());
+
+ ASSERT_EQ(descriptor.GetNumIVars(), 3u);
+ EXPECT_EQ(descriptor.GetIVarAtIndex(0).m_name, ConstString("_count"));
+ EXPECT_EQ(descriptor.GetIVarAtIndex(0).m_offset, 0);
+ EXPECT_EQ(descriptor.GetIVarAtIndex(0).m_size, 4u);
+ EXPECT_EQ(descriptor.GetIVarAtIndex(1).m_name, ConstString("_name"));
+ EXPECT_EQ(descriptor.GetIVarAtIndex(1).m_offset,
+ static_cast<int32_t>(PointerSize()));
+ EXPECT_EQ(descriptor.GetIVarAtIndex(2).m_name, ConstString("_flag"));
+ EXPECT_EQ(descriptor.GetIVarAtIndex(2).m_offset,
+ static_cast<int32_t>(2 * PointerSize()));
+}
+
+// An out-of-range index must be inert rather than reading past the vector.
+TEST_P(GNUstepIvarTest, RejectsOutOfRangeIndex) {
+ const IvarSpec ivars[] = {{"_count", "i", 0, 4}};
+ GNUstepObjCClassDescriptor descriptor = MakeClassWithIvars(ivars, 8);
+ ASSERT_EQ(descriptor.GetNumIVars(), 1u);
+ EXPECT_FALSE(descriptor.GetIVarAtIndex(1).m_name);
+ EXPECT_FALSE(descriptor.GetIVarAtIndex(1000).m_name);
+}
+
+// The runtime walks the array by the stride the list declares, so this code
+// must too rather than assuming sizeof(objc_ivar).
+TEST_P(GNUstepIvarTest, HonorsDeclaredStride) {
+ const IvarSpec ivars[] = {{"_a", "i", 0, 4}, {"_b", "i", 4, 4}};
+ GNUstepObjCClassDescriptor descriptor =
+ MakeClassWithIvars(ivars, 16, g_flag_resolved, 0, IvarStride() + 8);
+ ASSERT_EQ(descriptor.GetNumIVars(), 2u);
+ EXPECT_EQ(descriptor.GetIVarAtIndex(1).m_name, ConstString("_b"));
+ EXPECT_EQ(descriptor.GetIVarAtIndex(1).m_offset, 4);
+}
+
+// A stride smaller than the struct would make every field after the first
+// come from the wrong place.
+TEST_P(GNUstepIvarTest, RejectsUndersizedStride) {
+ const IvarSpec ivars[] = {{"_a", "i", 0, 4}};
+ GNUstepObjCClassDescriptor descriptor =
+ MakeClassWithIvars(ivars, 16, g_flag_resolved, 0, PointerSize());
+ EXPECT_EQ(descriptor.GetNumIVars(), 0u);
+}
+
+// objc_resolve_class sets objc_class_flag_resolved *before* it calls
+// objc_compute_ivar_offsets, so the flag alone does not mean the offsets are
+// absolute yet. A non-positive instance_size is what marks that window, and
+// reporting ivars inside it would give offsets relative to the start of this
+// class's own ivars.
+TEST_P(GNUstepIvarTest, RejectsClassResolvedButNotYetLaidOut) {
+ const IvarSpec ivars[] = {{"_a", "i", 0, 4}};
+ GNUstepObjCClassDescriptor descriptor =
+ MakeClassWithIvars(ivars, 0, g_flag_resolved);
+ EXPECT_EQ(descriptor.GetNumIVars(), 0u);
+}
+
+TEST_P(GNUstepIvarTest, RejectsUnresolvedClass) {
+ const IvarSpec ivars[] = {{"_a", "i", 0, 4}};
+ GNUstepObjCClassDescriptor descriptor = MakeClassWithIvars(ivars, 16, 0);
+ EXPECT_EQ(descriptor.GetNumIVars(), 0u);
+}
+
+// A count the structure could not possibly hold is evidence this is not an
+// ivar list at all.
+TEST_P(GNUstepIvarTest, RejectsImplausibleCount) {
+ const IvarSpec ivars[] = {{"_a", "i", 0, 4}};
+ GNUstepObjCClassDescriptor huge =
+ MakeClassWithIvars(ivars, 16, g_flag_resolved, 0x10000);
+ EXPECT_EQ(huge.GetNumIVars(), 0u);
+
+ GNUstepObjCClassDescriptor negative =
+ MakeClassWithIvars(ivars, 16, g_flag_resolved, static_cast<uint32_t>(-1));
+ EXPECT_EQ(negative.GetNumIVars(), 0u);
+}
+
+// An ivar that does not fit inside the object means the list was
+// misidentified, so none of it can be trusted - not just that entry.
+TEST_P(GNUstepIvarTest, RejectsIvarOutsideTheObject) {
+ const IvarSpec ivars[] = {{"_a", "i", 0, 4}, {"_b", "i", 4096, 4}};
+ GNUstepObjCClassDescriptor descriptor = MakeClassWithIvars(ivars, 16);
+ EXPECT_EQ(descriptor.GetNumIVars(), 0u);
+}
+
+// Between class_addIvar and objc_registerClassPair, libobjc2 stores the
+// offset inline in the pointer field (runtime.c), so it is a small integer
+// masquerading as a pointer rather than something safe to dereference.
+TEST_P(GNUstepIvarTest, RejectsInlineOffsetMasqueradingAsPointer) {
+ // Only one class is laid out per test: Process caches the memory it reads,
+ // so writing a second variant over the same addresses would be masked by
+ // the first descriptor's reads. ReadsIvars covers the accepted shape.
+ const IvarSpec ivars[] = {{"_a", "i", 0, 4}};
+ GNUstepObjCClassDescriptor broken =
+ MakeClassWithIvars(ivars, 16, g_flag_resolved, 0, 0,
+ /*inline_offset_value=*/3);
+ EXPECT_EQ(broken.GetNumIVars(), 0u);
+}
+
+TEST_P(GNUstepIvarTest, ClassWithNoIvarListHasNone) {
+ FakeProcess &process = GetProcess();
+ process.WriteCString(g_name_addr, "Widget");
+ WriteClass(g_metaclass_addr, g_metaclass_addr, 0, g_name_addr,
+ g_flag_meta | g_flag_resolved, 0);
+ WriteClass(g_class_addr, g_metaclass_addr, 0, g_name_addr, g_flag_resolved,
+ 16);
+ process.WriteInteger(g_class_addr + IvarsOffset(), 0, PointerSize());
+ GNUstepObjCClassDescriptor descriptor(m_process_sp, g_class_addr);
+ EXPECT_EQ(descriptor.GetNumIVars(), 0u);
+}
+
+// A metaclass describes the class object, which has no ivars of its own.
+TEST_P(GNUstepIvarTest, MetaclassHasNoIvars) {
+ const IvarSpec ivars[] = {{"_a", "i", 0, 4}};
+ MakeClassWithIvars(ivars, 16);
+ GetProcess().WriteInteger(g_metaclass_addr + IvarsOffset(), g_ivar_list_addr,
+ PointerSize());
+ GNUstepObjCClassDescriptor metaclass(m_process_sp, g_metaclass_addr);
+ EXPECT_EQ(metaclass.GetNumIVars(), 0u);
+}
+
+INSTANTIATE_TEST_SUITE_P(DataModels, GNUstepIvarTest,
+ ::testing::Values(DataModel{"x86_64-pc-linux", 8, 8},
+ DataModel{"x86_64-pc-windows-msvc",
+ 8, 4},
+ DataModel{"i386-pc-linux", 4, 4}),
+ [](const ::testing::TestParamInfo<DataModel> &info) {
+ std::string name = info.param.triple;
+ for (char &c : name)
+ if (!std::isalnum(static_cast<unsigned char>(c)))
+ c = '_';
+ return name;
+ });
+
} // namespace
>From 95009b8381457fc6ebf55f3ebe8775e6e963db89 Mon Sep 17 00:00:00 2001
From: Rob <robk at robk.dev>
Date: Tue, 18 Aug 2026 09:39:07 +0100
Subject: [PATCH 32/49] [lldb][GNUstep] Fall back to runtime metadata for
ivars, and format NSURL
Formatters reached ivars only through debug info. gnustep-base hides several
classes' ivars behind GS_EXPOSE, so a build against the installed headers
sees an @interface with no ivars at all, and a stripped build has none
either - which is why NSURL was previously left uncovered.
GNUstepGetIvar now falls back to libobjc2's own ivar metadata when debug
info has nothing. Debug info stays the preferred source, because it is the
only one that describes members *inside* a struct-typed ivar: libobjc2's
encodings expand struct members positionally with no field names
(`{_NSRange=QQ}`), so `map.nodeCount` and friends remain reachable only
through DWARF. Flat ivars - which is what the string, number, array, data
and date formatters read - are now reachable either way.
With that in place, NSURL formats. Reaching _urlString and _baseURL by name
also avoids the fixed offsets Apple's provider computes, which is what makes
this correct across data models without a per-platform table.
Assisted-by: Claude Opus 5
---
.../Language/ObjC/GNUstepFormatters.cpp | 140 +++++++++++++++++-
.../Plugins/Language/ObjC/GNUstepFormatters.h | 40 ++++-
.../Plugins/Language/ObjC/GNUstepNSString.cpp | 31 +++-
.../TestGNUstepDataFormatters.py | 6 +
.../Shell/Expr/objc-gnustep-string-flags.m | 68 +++++++++
.../Language/ObjC/GNUstepFormattersTest.cpp | 17 +++
.../GNUstepObjCClassDescriptorTest.cpp | 17 +++
7 files changed, 303 insertions(+), 16 deletions(-)
create mode 100644 lldb/test/Shell/Expr/objc-gnustep-string-flags.m
diff --git a/lldb/source/Plugins/Language/ObjC/GNUstepFormatters.cpp b/lldb/source/Plugins/Language/ObjC/GNUstepFormatters.cpp
index 7609c6b5de8da..b0d620f55cf2b 100644
--- a/lldb/source/Plugins/Language/ObjC/GNUstepFormatters.cpp
+++ b/lldb/source/Plugins/Language/ObjC/GNUstepFormatters.cpp
@@ -53,7 +53,78 @@ ValueObjectSP lldb_private::formatters::GNUstepGetIvar(ValueObject &valobj,
if (ValueObjectSP dynamic_sp =
object_sp->GetDynamicValue(lldb::eDynamicDontRunTarget))
object_sp = dynamic_sp;
- return object_sp->GetChildMemberWithName(name);
+ if (ValueObjectSP child_sp = object_sp->GetChildMemberWithName(name))
+ return child_sp;
+
+ // Debug info is the better source - it knows about members inside a
+ // struct-typed ivar, which the runtime's encodings cannot express - but it
+ // is not always there. gnustep-base hides several classes' ivars behind
+ // GS_EXPOSE, so a consumer of the installed headers sees an @interface
+ // with no ivars at all, and a stripped build has none either. The runtime
+ // metadata still describes them, so fall back to it.
+ return GNUstepGetIvarFromRuntime(*object_sp, name);
+}
+
+namespace {
+struct FoundIvar {
+ ObjCLanguageRuntime::ClassDescriptor::iVarDescriptor ivar;
+ addr_t object_addr;
+};
+} // namespace
+
+static std::optional<FoundIvar> FindIvarInRuntime(ValueObject &valobj,
+ llvm::StringRef name) {
+ ProcessSP process_sp = valobj.GetProcessSP();
+ if (!process_sp)
+ return std::nullopt;
+ auto *runtime = llvm::dyn_cast_or_null<GNUstepObjCRuntime>(
+ ObjCLanguageRuntime::Get(*process_sp));
+ if (!runtime)
+ return std::nullopt;
+
+ const addr_t object_addr = valobj.GetValueAsUnsigned(LLDB_INVALID_ADDRESS);
+ if (object_addr == 0 || object_addr == LLDB_INVALID_ADDRESS)
+ return std::nullopt;
+
+ // An ivar declared by a superclass is just as much a part of the object, so
+ // walk up until it is found. Offsets are absolute, so no adjustment is
+ // needed at each step.
+ ObjCLanguageRuntime::ClassDescriptorSP descriptor_sp =
+ runtime->GetClassDescriptor(valobj);
+ // The chain comes from inferior memory, so bound the walk rather than
+ // trusting it to terminate.
+ static constexpr uint32_t g_max_superclass_depth = 256;
+ const ConstString ivar_name(name);
+ for (uint32_t depth = 0; descriptor_sp && depth < g_max_superclass_depth;
+ ++depth, descriptor_sp = descriptor_sp->GetSuperclass()) {
+ const size_t num_ivars = descriptor_sp->GetNumIVars();
+ for (size_t i = 0; i < num_ivars; ++i) {
+ const auto &ivar = descriptor_sp->GetIVarAtIndex(i);
+ if (ivar.m_name == ivar_name)
+ return FoundIvar{ivar, object_addr};
+ }
+ }
+ return std::nullopt;
+}
+
+ValueObjectSP
+lldb_private::formatters::GNUstepGetIvarFromRuntime(ValueObject &valobj,
+ llvm::StringRef name) {
+ std::optional<FoundIvar> found = FindIvarInRuntime(valobj, name);
+ if (!found || !found->ivar.m_type)
+ return {};
+ return valobj.GetSyntheticChildAtOffset(
+ found->ivar.m_offset, found->ivar.m_type,
+ /*can_create=*/true, ConstString(name));
+}
+
+std::optional<addr_t>
+lldb_private::formatters::GNUstepGetIvarAddress(ValueObject &valobj,
+ llvm::StringRef name) {
+ std::optional<FoundIvar> found = FindIvarInRuntime(valobj, name);
+ if (!found)
+ return std::nullopt;
+ return found->object_addr + found->ivar.m_offset;
}
std::optional<double>
@@ -72,6 +143,11 @@ lldb_private::formatters::GNUstepGetFloatValue(ValueObject &valobj) {
// --- Small object decoding -------------------------------------------------
+bool lldb_private::formatters::GNUstepDecodeWideFlag(
+ uint8_t byte, lldb::ByteOrder byte_order) {
+ return (byte & (byte_order == lldb::eByteOrderBig ? 0x80 : 0x01)) != 0;
+}
+
std::optional<std::string>
lldb_private::formatters::GNUstepDecodeTinyString(uint64_t ptr) {
if ((ptr & g_gnustep_small_object_mask) != 4)
@@ -128,8 +204,57 @@ double lldb_private::formatters::GNUstepDecodeSmallDate(uint64_t ptr) {
return value;
}
+// A URL relative to a base that is itself relative is real, but a chain
+// longer than this is corrupt or circular.
+static constexpr uint32_t g_max_url_base_depth = 16;
+
// --- Small providers -------------------------------------------------------
+// Recursion is kept out of the registered provider, which must match the
+// summary-callback signature exactly.
+static bool GNUstepNSURLSummary(ValueObject &valobj, Stream &stream,
+ const TypeSummaryOptions &options,
+ uint32_t depth) {
+ if (!IsGNUstepObjCRuntime(valobj))
+ return false;
+ // gnustep-base keeps NSURL's ivars behind GS_EXPOSE(NSURL), so a build
+ // against the installed headers has no debug info for them. Reaching them
+ // by name through the runtime's own metadata avoids the fixed offsets
+ // Apple's provider has to use.
+ ValueObjectSP url_sp = GNUstepGetIvar(valobj, "_urlString");
+ if (!url_sp || url_sp->GetValueAsUnsigned(0) == 0)
+ return false;
+
+ // A relative URL keeps the base it was resolved against, which is how
+ // -absoluteString presents it too. The chain is inferior data and a URL
+ // whose base is itself would otherwise recurse until the stack runs out,
+ // and this runs whenever the value is merely displayed.
+ StreamString base_summary;
+ if (depth < g_max_url_base_depth) {
+ ValueObjectSP base_sp = GNUstepGetIvar(valobj, "_baseURL");
+ if (base_sp && base_sp->GetValueAsUnsigned(0) != 0)
+ if (!GNUstepNSURLSummary(*base_sp, base_summary, options, depth + 1))
+ base_summary.Clear();
+ }
+
+ if (base_summary.Empty())
+ return GNUstepNSStringSummaryProvider(*url_sp, stream, options);
+
+ StreamString url_summary;
+ if (!GNUstepNSStringSummaryProvider(*url_sp, url_summary, options))
+ return false;
+ llvm::StringRef url_text = url_summary.GetString();
+ llvm::StringRef base_text = base_summary.GetString();
+ // Both arrive quoted as @"...", and the pair reads better as one string.
+ url_text.consume_front("@\"");
+ url_text.consume_back("\"");
+ base_text.consume_front("@\"");
+ base_text.consume_back("\"");
+ stream.Printf("@\"%s -- %s\"", url_text.str().c_str(),
+ base_text.str().c_str());
+ return true;
+}
+
bool lldb_private::formatters::GNUstepNSNullSummaryProvider(
ValueObject &valobj, Stream &stream, const TypeSummaryOptions &options) {
if (!IsGNUstepObjCRuntime(valobj))
@@ -284,7 +409,14 @@ void lldb_private::formatters::LoadGNUstepFormatters(
AddCXXSummary(objc_category_sp, GNUstepNSNullSummaryProvider,
"GNUstep NSNull summary provider", "NSNull", summary_flags);
- // Not covered: NSURL. gnustep-base declares its ivars behind
- // GS_EXPOSE(NSURL), so they are in neither the debug info nor the
- // __objc_ivar_offset symbols of a normal build; `po` still describes it.
+ AddCXXSummary(objc_category_sp, GNUstepNSURLSummaryProvider,
+ "GNUstep NSURL summary provider", "NSURL", summary_flags);
+}
+
+bool lldb_private::formatters::GNUstepNSURLSummaryProvider(
+ ValueObject &valobj, Stream &stream, const TypeSummaryOptions &options) {
+ // Shared type name with Apple's provider - see GNUstepNSDateSummaryProvider.
+ if (!IsGNUstepObjCRuntime(valobj))
+ return NSURLSummaryProvider(valobj, stream, options);
+ return GNUstepNSURLSummary(valobj, stream, options, /*depth=*/0);
}
diff --git a/lldb/source/Plugins/Language/ObjC/GNUstepFormatters.h b/lldb/source/Plugins/Language/ObjC/GNUstepFormatters.h
index 2ceda0aca08f7..70da89a51b8e6 100644
--- a/lldb/source/Plugins/Language/ObjC/GNUstepFormatters.h
+++ b/lldb/source/Plugins/Language/ObjC/GNUstepFormatters.h
@@ -12,9 +12,10 @@
// Like the Apple formatters these never run code in the inferior. Unlike them
// they do not hardcode ivar offsets: libobjc2 packs instance sizes and the
// widths of `long`-typed ivars differ between LP64 and LLP64, so ivars are
-// looked up by name through the debug info attached to the value's dynamic
-// type. Small objects (libobjc2's tagged pointers) are decoded from the
-// pointer bits alone.
+// looked up by name - through the debug info attached to the value's dynamic
+// type where there is any, and through libobjc2's own metadata otherwise.
+// Small objects (libobjc2's tagged pointers) are decoded from the pointer
+// bits alone.
//
//===----------------------------------------------------------------------===//
@@ -42,11 +43,30 @@ bool IsGNUstepObjCRuntime(ValueObject &valobj);
/// runtime-reported class name of a value.
void LoadGNUstepFormatters(lldb::TypeCategoryImplSP objc_category_sp);
-/// Finds the ivar \p name of the object \p valobj points at, using the debug
-/// info of its dynamic type. Returns an empty pointer when the ivar is not
-/// visible, which the callers treat as "cannot format".
+/// Returns the ivar named \p name on \p valobj, or an empty pointer if it
+/// cannot be reached, which the callers treat as "cannot format".
+///
+/// Debug info is preferred, because it is the only source that describes
+/// members *inside* a struct-typed ivar. Where it has nothing - gnustep-base
+/// hides several classes' ivars behind GS_EXPOSE, and a stripped build has
+/// none at all - libobjc2's own metadata is used instead.
lldb::ValueObjectSP GNUstepGetIvar(ValueObject &valobj, llvm::StringRef name);
+/// Finds an ivar using only libobjc2's runtime metadata, for classes whose
+/// ivars debug info does not describe - gnustep-base hides several behind
+/// GS_EXPOSE, and a stripped build has none. Only reaches an ivar the class
+/// or one of its superclasses declares directly: the runtime's encodings
+/// expand struct members positionally, with no field names, so a member
+/// *inside* a struct-typed ivar is not addressable this way.
+lldb::ValueObjectSP GNUstepGetIvarFromRuntime(ValueObject &valobj,
+ llvm::StringRef name);
+
+/// Load address of ivar \p name, from the runtime's metadata. Unlike
+/// GNUstepGetIvarFromRuntime this does not need the ivar's type to be
+/// expressible, which libobjc2's encodings cannot manage for a bitfield.
+std::optional<lldb::addr_t> GNUstepGetIvarAddress(ValueObject &valobj,
+ llvm::StringRef name);
+
/// The value of a floating-point ValueObject as a double, or nullopt if it
/// cannot be read as one.
std::optional<double> GNUstepGetFloatValue(ValueObject &valobj);
@@ -59,6 +79,11 @@ constexpr uint64_t g_gnustep_small_object_mask = 7;
/// GSTinyString packs up to eight 7-bit characters and a 5-bit length into
/// the pointer (gnustep-base Source/GSString.m, clang CGObjCGNU.cpp).
+/// Whether gnustep-base's `wide` string flag is set in the byte holding it.
+/// It is the first bitfield of its storage unit, so the low bit
+/// little-endian and the high bit big-endian.
+bool GNUstepDecodeWideFlag(uint8_t byte, lldb::ByteOrder byte_order);
+
std::optional<std::string> GNUstepDecodeTinyString(uint64_t ptr);
/// NSSmallInt stores an arithmetically shifted integer (Source/NSNumber.m).
int64_t GNUstepDecodeSmallInt(uint64_t ptr);
@@ -85,6 +110,9 @@ bool GNUstepNSSetSummaryProvider(ValueObject &valobj, Stream &stream,
const TypeSummaryOptions &options);
bool GNUstepNSDataSummaryProvider(ValueObject &valobj, Stream &stream,
const TypeSummaryOptions &options);
+bool GNUstepNSURLSummaryProvider(ValueObject &valobj, Stream &stream,
+ const TypeSummaryOptions &options);
+
bool GNUstepNSNullSummaryProvider(ValueObject &valobj, Stream &stream,
const TypeSummaryOptions &options);
diff --git a/lldb/source/Plugins/Language/ObjC/GNUstepNSString.cpp b/lldb/source/Plugins/Language/ObjC/GNUstepNSString.cpp
index 20f7ab889e9de..65f33ebb98b14 100644
--- a/lldb/source/Plugins/Language/ObjC/GNUstepNSString.cpp
+++ b/lldb/source/Plugins/Language/ObjC/GNUstepNSString.cpp
@@ -92,6 +92,26 @@ std::optional<StringContents> ReadConstantString(ValueObject &valobj) {
return contents;
}
+/// `_flags` is a bitfield struct, which libobjc2's encodings cannot describe,
+/// so without debug info read `wide` out of the byte it occupies.
+std::optional<bool> IsWide(ValueObject &valobj) {
+ if (ValueObjectSP flags_sp = GNUstepGetIvar(valobj, "_flags"))
+ if (ValueObjectSP wide_sp = flags_sp->GetChildMemberWithName("wide"))
+ return wide_sp->GetValueAsUnsigned(0) != 0;
+
+ std::optional<addr_t> flags_addr = GNUstepGetIvarAddress(valobj, "_flags");
+ ProcessSP process_sp = valobj.GetProcessSP();
+ if (!flags_addr || !process_sp)
+ return std::nullopt;
+ Status error;
+ uint8_t byte = 0;
+ if (process_sp->ReadMemory(*flags_addr, &byte, sizeof(byte), error) !=
+ sizeof(byte) ||
+ error.Fail())
+ return std::nullopt;
+ return GNUstepDecodeWideFlag(byte, process_sp->GetByteOrder());
+}
+
/// GSString and everything derived from it, plus GSMutableString: the buffer
/// pointer `_contents`, the character count `_count`, and `_flags` whose bit
/// 0 (`wide`) selects 16-bit characters (Source/GSPrivate.h). The buffer is
@@ -99,18 +119,17 @@ std::optional<StringContents> ReadConstantString(ValueObject &valobj) {
std::optional<StringContents> ReadGSString(ValueObject &valobj) {
ValueObjectSP contents_sp = GNUstepGetIvar(valobj, "_contents");
ValueObjectSP count_sp = GNUstepGetIvar(valobj, "_count");
- ValueObjectSP flags_sp = GNUstepGetIvar(valobj, "_flags");
- if (!contents_sp || !count_sp || !flags_sp)
+ if (!contents_sp || !count_sp)
return std::nullopt;
StringContents contents;
contents.address = GetPointerValue(*contents_sp);
if (contents.address == LLDB_INVALID_ADDRESS)
return std::nullopt;
contents.count = count_sp->GetValueAsUnsigned(0);
- bool wide = false;
- if (ValueObjectSP wide_sp = flags_sp->GetChildMemberWithName("wide"))
- wide = wide_sp->GetValueAsUnsigned(0) != 0;
- contents.encoding = wide ? Encoding::UTF16 : Encoding::Latin1;
+ std::optional<bool> wide = IsWide(valobj);
+ if (!wide)
+ return std::nullopt;
+ contents.encoding = *wide ? Encoding::UTF16 : Encoding::Latin1;
return contents;
}
diff --git a/lldb/test/API/lang/objc-gnustep/data-formatters/TestGNUstepDataFormatters.py b/lldb/test/API/lang/objc-gnustep/data-formatters/TestGNUstepDataFormatters.py
index 6c71fa24ed360..791ae3d877ad9 100644
--- a/lldb/test/API/lang/objc-gnustep/data-formatters/TestGNUstepDataFormatters.py
+++ b/lldb/test/API/lang/objc-gnustep/data-formatters/TestGNUstepDataFormatters.py
@@ -125,6 +125,12 @@ def test_others(self):
"frame variable -d run-target someDate", substrs=["2023-11-14 22:13:20 UTC"]
)
self.expect("frame variable -d run-target null", substrs=["<null>"])
+ # NSURL keeps its ivars behind GS_EXPOSE, so nothing here has debug
+ # info for them; this only works by reading libobjc2's own metadata.
+ self.expect(
+ "frame variable -d run-target url",
+ substrs=['@"https://www.gnustep.org/resources"'],
+ )
self.expect("frame variable -d run-target nilObject", substrs=["nil"])
# A custom class: dynamic type plus formatted ivars, and its class
# object is not itself presented as an instance.
diff --git a/lldb/test/Shell/Expr/objc-gnustep-string-flags.m b/lldb/test/Shell/Expr/objc-gnustep-string-flags.m
new file mode 100644
index 0000000000000..aa76cb3442da3
--- /dev/null
+++ b/lldb/test/Shell/Expr/objc-gnustep-string-flags.m
@@ -0,0 +1,68 @@
+// REQUIRES: objc-gnustep
+//
+// RUN: %build %inferior_target %s --compiler=clang --objc-gnustep --output=%t
+
+#import "objc/runtime.h"
+
+#ifdef __has_attribute
+#if __has_attribute(objc_root_class)
+__attribute__((objc_root_class))
+#endif
+#endif
+ at interface NSObject {
+ id isa;
+ int refcount;
+}
++ (id)new;
+ at end
+ at implementation NSObject
++ (id)new {
+ return class_createInstance(self, 0);
+}
+ at end
+
+// gnustep-base spells the wide flag as a one-bit field of an anonymous struct
+// (`struct { unsigned wide:1; ... } _flags`, Source/GSPrivate.h), which
+// libobjc2's type encodings cannot describe - so where the class has no debug
+// info the summary provider has no `wide` member to read and falls back to
+// reading the bit out of memory. Declaring it as a plain word here leaves the
+// member missing in the same way, which is what takes that path.
+//
+// The name is what matters: formatters are matched on the class name the
+// runtime reports, so this stands in for gnustep-base's own GSCInlineString.
+ at interface GSCInlineString : NSObject {
+ at public
+ char *_contents;
+ unsigned _count;
+ unsigned _flags;
+}
+ at end
+ at implementation GSCInlineString
+ at end
+
+int main() {
+ GSCInlineString *narrow = [GSCInlineString new];
+ narrow->_contents = "hi";
+ narrow->_count = 2;
+ narrow->_flags = 0;
+
+ // Bit 0 is `wide`. Set it and the same bytes are read as UTF-16, which is
+ // what proves the fallback read the flag rather than defaulting to narrow.
+ GSCInlineString *wide = [GSCInlineString new];
+ wide->_contents = "h\0i\0";
+ wide->_count = 2;
+ wide->_flags = 1;
+
+ return 0; // break here
+}
+
+// RUN: %lldb %inferior_abi -b -o "b objc-gnustep-string-flags.m:56" -o "run" \
+// RUN: -o "frame variable -d run-target narrow" \
+// RUN: -o "frame variable -d run-target wide" \
+// RUN: -- %t | FileCheck %s
+//
+// CHECK: (lldb) frame variable -d run-target narrow
+// CHECK: @"hi"
+//
+// CHECK: (lldb) frame variable -d run-target wide
+// CHECK: @"hi"
diff --git a/lldb/unittests/Language/ObjC/GNUstepFormattersTest.cpp b/lldb/unittests/Language/ObjC/GNUstepFormattersTest.cpp
index 3fa1a31e40bd5..35bfb5896957c 100644
--- a/lldb/unittests/Language/ObjC/GNUstepFormattersTest.cpp
+++ b/lldb/unittests/Language/ObjC/GNUstepFormattersTest.cpp
@@ -18,6 +18,23 @@ using namespace lldb_private::formatters;
// bits) and gnustep-base's encodings; the constants are what the compiler and
// runtime actually produce, checked against a live process.
+// gnustep-base's `wide` is the first bitfield of its storage unit, so which
+// end of the byte it occupies depends on the target's byte order. No test
+// configuration runs big-endian, so this is the only cover it gets.
+TEST(GNUstepFormattersTest, WideFlagFollowsByteOrder) {
+ EXPECT_FALSE(GNUstepDecodeWideFlag(0x00, lldb::eByteOrderLittle));
+ EXPECT_TRUE(GNUstepDecodeWideFlag(0x01, lldb::eByteOrderLittle));
+ EXPECT_FALSE(GNUstepDecodeWideFlag(0x80, lldb::eByteOrderLittle));
+
+ EXPECT_FALSE(GNUstepDecodeWideFlag(0x00, lldb::eByteOrderBig));
+ EXPECT_TRUE(GNUstepDecodeWideFlag(0x80, lldb::eByteOrderBig));
+ EXPECT_FALSE(GNUstepDecodeWideFlag(0x01, lldb::eByteOrderBig));
+
+ // The neighbouring `owned` bit must not be mistaken for it either way.
+ EXPECT_FALSE(GNUstepDecodeWideFlag(0x02, lldb::eByteOrderLittle));
+ EXPECT_FALSE(GNUstepDecodeWideFlag(0x40, lldb::eByteOrderBig));
+}
+
TEST(GNUstepFormattersTest, TinyStringDecodesClangEmittedLiteral) {
// clang emits @"Hello" for the gnustep-2.x ABI as this constant
// (CGObjCGNU.cpp: 7-bit characters from the top, 5-bit length, tag 4).
diff --git a/lldb/unittests/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCClassDescriptorTest.cpp b/lldb/unittests/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCClassDescriptorTest.cpp
index 7e567b4207dd4..8bc7453885be8 100644
--- a/lldb/unittests/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCClassDescriptorTest.cpp
+++ b/lldb/unittests/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCClassDescriptorTest.cpp
@@ -507,6 +507,23 @@ TEST_P(GNUstepIvarTest, ReadsIvars) {
static_cast<int32_t>(2 * PointerSize()));
}
+// An ivar whose encoding yields no type (libobjc2's bitfield structs) must
+// still be locatable, so a formatter can read it from memory.
+TEST_P(GNUstepIvarTest, LocatesIvarWithUnrepresentableType) {
+ const IvarSpec ivars[] = {
+ {"_contents", "^v", 0, PointerSize()},
+ {"_flags", "{?=b1b1b2b28}", static_cast<int32_t>(PointerSize()), 4},
+ };
+ GNUstepObjCClassDescriptor descriptor =
+ MakeClassWithIvars(ivars, PointerSize() + 4);
+
+ ASSERT_EQ(descriptor.GetNumIVars(), 2u);
+ EXPECT_EQ(descriptor.GetIVarAtIndex(1).m_name, ConstString("_flags"));
+ EXPECT_EQ(descriptor.GetIVarAtIndex(1).m_offset,
+ static_cast<int32_t>(PointerSize()));
+ EXPECT_EQ(descriptor.GetIVarAtIndex(1).m_size, 4u);
+}
+
// An out-of-range index must be inert rather than reading past the vector.
TEST_P(GNUstepIvarTest, RejectsOutOfRangeIndex) {
const IvarSpec ivars[] = {{"_count", "i", 0, 4}};
>From e9371a4610a3ad48a538032edc40563e5bdd423b Mon Sep 17 00:00:00 2001
From: Rob <robk at robk.dev>
Date: Tue, 18 Aug 2026 09:48:06 +0100
Subject: [PATCH 33/49] [lldb] Fix a crash listing a frame recognizer with no
module
A recognizer meant to match in any module is registered with an empty module
name. ConstString::GetCString() returns null for an empty string, and
StackFrameRecognizerManager::ForEach passes it straight into a std::string
parameter, so `frame recognizer list` crashed in strlen.
Nothing hit this before because every in-tree recognizer names a module, and
`frame recognizer add` rejects a missing -s, so the case is only reachable
through the API. It comes up for a language runtime whose library is named
differently on each platform, which is why the existing NullModuleRegex test
covers the regex path but not this one.
Assisted-by: Claude Opus 5
---
lldb/source/Target/StackFrameRecognizer.cpp | 7 +++--
.../Target/StackFrameRecognizerTest.cpp | 29 +++++++++++++++++++
2 files changed, 34 insertions(+), 2 deletions(-)
diff --git a/lldb/source/Target/StackFrameRecognizer.cpp b/lldb/source/Target/StackFrameRecognizer.cpp
index 99ef837ec580b..65b3e525522a1 100644
--- a/lldb/source/Target/StackFrameRecognizer.cpp
+++ b/lldb/source/Target/StackFrameRecognizer.cpp
@@ -139,9 +139,12 @@ void StackFrameRecognizerManager::ForEach(
module_name, llvm::ArrayRef(ConstString(symbol_name)),
entry.symbol_mangling, true);
} else {
+ // A recognizer that matches in any module has no module name, and
+ // ConstString::GetCString() is null for an empty string, which the
+ // std::string parameter cannot be constructed from.
callback(entry.recognizer_id, entry.enabled, entry.recognizer->GetName(),
- entry.module.GetCString(), entry.symbols, entry.symbol_mangling,
- false);
+ entry.module.GetStringRef().str(), entry.symbols,
+ entry.symbol_mangling, false);
}
}
}
diff --git a/lldb/unittests/Target/StackFrameRecognizerTest.cpp b/lldb/unittests/Target/StackFrameRecognizerTest.cpp
index b356cd87d54ac..9b5b269bc9e40 100644
--- a/lldb/unittests/Target/StackFrameRecognizerTest.cpp
+++ b/lldb/unittests/Target/StackFrameRecognizerTest.cpp
@@ -76,3 +76,32 @@ TEST_F(StackFrameRecognizerTest, NullModuleRegex) {
EXPECT_TRUE(any_printed);
}
+
+// A recognizer that should match in any module is registered with an empty
+// module name. ConstString::GetCString() is null for an empty string, and
+// ForEach hands that to a std::string parameter, so this used to crash. The
+// command layer requires a module, so only a recognizer registered through
+// the API - as the GNUstep ObjC runtime's does, because the runtime library
+// is named differently on each platform - can reach it.
+TEST_F(StackFrameRecognizerTest, NullModuleName) {
+ DebuggerSP debugger_sp = Debugger::CreateInstance();
+ ASSERT_TRUE(debugger_sp);
+
+ StackFrameRecognizerManager manager;
+ std::vector<ConstString> symbols = {ConstString("boom")};
+ manager.AddRecognizer(std::make_shared<DummyStackFrameRecognizer>(),
+ ConstString(), symbols,
+ Mangled::NamePreference::ePreferDemangled, false);
+
+ bool any_printed = false;
+ std::string reported_module = "unset";
+ manager.ForEach([&](uint32_t recognizer_id, bool enabled, std::string name,
+ std::string module, llvm::ArrayRef<ConstString> symbols,
+ Mangled::NamePreference symbol_mangling, bool regexp) {
+ any_printed = true;
+ reported_module = module;
+ });
+
+ EXPECT_TRUE(any_printed);
+ EXPECT_EQ(reported_module, "");
+}
>From 94e03f1699b8e57dea7b67c9e733ec204c9150f1 Mon Sep 17 00:00:00 2001
From: Rob <robk at robk.dev>
Date: Tue, 18 Aug 2026 09:48:08 +0100
Subject: [PATCH 34/49] [lldb][GNUstep] Format NSException, and match the throw
frame by mangled name
Adds an NSException summary provider. gnustep-base's NSException has three
ivars - _e_name, _e_reason and _reserved, not the four Apple's does - and
they sit behind GS_EXPOSE(NSException), so this works only through the
runtime-metadata fallback. userInfo lives inside _reserved and is not
reachable by name, so it is not reported.
Also fixes the exception frame recognizer, which did not fire against any
libobjc2 carrying debug info. It was registered to match on the demangled
name, following AppleObjCRuntimeV2. libobjc2's Windows exception back-end is
C++ (eh_win32_msvc.cc), so with debug info present the demangled name of
this extern "C" function is `::objc_exception_throw(id)` rather than the
bare symbol, and the comparison failed. Preferring the mangled name gives
`objc_exception_throw` there, and falls back to the same string through the
symbol when there is no debug info.
The Shell test did not catch this: on Windows it stopped on the executable's
import thunk, which has no debug info, so it exercised the fallback rather
than the path a real runtime takes. The new API test runs against
gnustep-base, where the throw happens inside the runtime library itself.
Assisted-by: Claude Opus 5
---
.../Language/ObjC/GNUstepFormatters.cpp | 40 ++++++++--
.../Plugins/Language/ObjC/GNUstepFormatters.h | 3 +
.../GNUstepObjCRuntime/GNUstepObjCRuntime.cpp | 41 +++++++++-
.../API/lang/objc-gnustep/exceptions/Makefile | 3 +
.../exceptions/TestGNUstepExceptions.py | 79 +++++++++++++++++++
.../lang/objc-gnustep/exceptions/categories | 1 +
.../API/lang/objc-gnustep/exceptions/main.m | 20 +++++
7 files changed, 176 insertions(+), 11 deletions(-)
create mode 100644 lldb/test/API/lang/objc-gnustep/exceptions/Makefile
create mode 100644 lldb/test/API/lang/objc-gnustep/exceptions/TestGNUstepExceptions.py
create mode 100644 lldb/test/API/lang/objc-gnustep/exceptions/categories
create mode 100644 lldb/test/API/lang/objc-gnustep/exceptions/main.m
diff --git a/lldb/source/Plugins/Language/ObjC/GNUstepFormatters.cpp b/lldb/source/Plugins/Language/ObjC/GNUstepFormatters.cpp
index b0d620f55cf2b..3ed7992a75397 100644
--- a/lldb/source/Plugins/Language/ObjC/GNUstepFormatters.cpp
+++ b/lldb/source/Plugins/Language/ObjC/GNUstepFormatters.cpp
@@ -255,6 +255,34 @@ static bool GNUstepNSURLSummary(ValueObject &valobj, Stream &stream,
return true;
}
+bool lldb_private::formatters::GNUstepNSURLSummaryProvider(
+ ValueObject &valobj, Stream &stream, const TypeSummaryOptions &options) {
+ // Shared type name with Apple's provider - see GNUstepNSDateSummaryProvider.
+ if (!IsGNUstepObjCRuntime(valobj))
+ return NSURLSummaryProvider(valobj, stream, options);
+ return GNUstepNSURLSummary(valobj, stream, options, /*depth=*/0);
+}
+
+bool lldb_private::formatters::GNUstepNSExceptionSummaryProvider(
+ ValueObject &valobj, Stream &stream, const TypeSummaryOptions &options) {
+ // Shared type name with Apple's provider - see GNUstepNSDateSummaryProvider.
+ if (!IsGNUstepObjCRuntime(valobj))
+ return NSException_SummaryProvider(valobj, stream, options);
+ // gnustep-base's NSException has three ivars - _e_name, _e_reason and
+ // _reserved - not the four Apple's does, and they sit behind
+ // GS_EXPOSE(NSException), so this depends on the runtime-metadata
+ // fallback. userInfo lives inside _reserved and is not reachable by name.
+ ValueObjectSP reason_sp = GNUstepGetIvar(valobj, "_e_reason");
+ if (!reason_sp || reason_sp->GetValueAsUnsigned(0) == 0) {
+ // A raised exception always has a name even when it carries no reason.
+ ValueObjectSP name_sp = GNUstepGetIvar(valobj, "_e_name");
+ if (!name_sp || name_sp->GetValueAsUnsigned(0) == 0)
+ return false;
+ return GNUstepNSStringSummaryProvider(*name_sp, stream, options);
+ }
+ return GNUstepNSStringSummaryProvider(*reason_sp, stream, options);
+}
+
bool lldb_private::formatters::GNUstepNSNullSummaryProvider(
ValueObject &valobj, Stream &stream, const TypeSummaryOptions &options) {
if (!IsGNUstepObjCRuntime(valobj))
@@ -409,14 +437,10 @@ void lldb_private::formatters::LoadGNUstepFormatters(
AddCXXSummary(objc_category_sp, GNUstepNSNullSummaryProvider,
"GNUstep NSNull summary provider", "NSNull", summary_flags);
+ AddCXXSummary(objc_category_sp, GNUstepNSExceptionSummaryProvider,
+ "GNUstep NSException summary provider", "NSException",
+ summary_flags);
+
AddCXXSummary(objc_category_sp, GNUstepNSURLSummaryProvider,
"GNUstep NSURL summary provider", "NSURL", summary_flags);
}
-
-bool lldb_private::formatters::GNUstepNSURLSummaryProvider(
- ValueObject &valobj, Stream &stream, const TypeSummaryOptions &options) {
- // Shared type name with Apple's provider - see GNUstepNSDateSummaryProvider.
- if (!IsGNUstepObjCRuntime(valobj))
- return NSURLSummaryProvider(valobj, stream, options);
- return GNUstepNSURLSummary(valobj, stream, options, /*depth=*/0);
-}
diff --git a/lldb/source/Plugins/Language/ObjC/GNUstepFormatters.h b/lldb/source/Plugins/Language/ObjC/GNUstepFormatters.h
index 70da89a51b8e6..c0f7e23757232 100644
--- a/lldb/source/Plugins/Language/ObjC/GNUstepFormatters.h
+++ b/lldb/source/Plugins/Language/ObjC/GNUstepFormatters.h
@@ -113,6 +113,9 @@ bool GNUstepNSDataSummaryProvider(ValueObject &valobj, Stream &stream,
bool GNUstepNSURLSummaryProvider(ValueObject &valobj, Stream &stream,
const TypeSummaryOptions &options);
+bool GNUstepNSExceptionSummaryProvider(ValueObject &valobj, Stream &stream,
+ const TypeSummaryOptions &options);
+
bool GNUstepNSNullSummaryProvider(ValueObject &valobj, Stream &stream,
const TypeSummaryOptions &options);
diff --git a/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCRuntime.cpp b/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCRuntime.cpp
index e03130c9e1856..932ad65eadd0f 100644
--- a/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCRuntime.cpp
+++ b/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCRuntime.cpp
@@ -58,6 +58,12 @@ using namespace lldb_private;
LLDB_PLUGIN_DEFINE(GNUstepObjCRuntime)
+// A type encoding longer than this, or an ISA chain deeper than this, is not
+// something a compiler produced. Both come from inferior memory, so they are
+// bounded rather than trusted.
+static constexpr size_t g_max_type_encoding_length = 1024;
+static constexpr uint32_t g_max_superclass_depth = 256;
+
namespace {
/// Registers the Objective-C selectors of a JIT'd expression module with the
/// libobjc2 runtime.
@@ -217,6 +223,7 @@ class GNUstepObjCExceptionRecognizedStackFrame : public RecognizedStackFrame {
};
class GNUstepObjCExceptionThrowFrameRecognizer : public StackFrameRecognizer {
+public:
RecognizedStackFrameSP RecognizeFrame(StackFrameSP frame) override {
return std::make_shared<GNUstepObjCExceptionRecognizedStackFrame>(frame);
}
@@ -229,12 +236,37 @@ class GNUstepObjCExceptionThrowFrameRecognizer : public StackFrameRecognizer {
/// objc.dll, ...), so the recognizer is registered without a module filter
/// and matched on the symbol alone; an empty module ConstString matches any
/// module (StackFrameRecognizer.cpp).
+///
+/// The mangled name is what has to match. libobjc2's Windows exception
+/// back-end is C++ (eh_win32_msvc.cc), so where the runtime carries debug
+/// info the demangled name of this extern "C" function is
+/// `::objc_exception_throw(id)` rather than the bare symbol. Preferring the
+/// mangled name gives `objc_exception_throw` there, and falls back to the
+/// same string through the symbol when there is no debug info at all.
void RegisterGNUstepObjCExceptionRecognizer(Process *process) {
static const std::vector<ConstString> g_symbols = {
ConstString("objc_exception_throw")};
- process->GetTarget().GetFrameRecognizerManager().AddRecognizer(
+ static const std::string g_name =
+ GNUstepObjCExceptionThrowFrameRecognizer().GetName();
+
+ // A runtime is created per Process but recognizers live on the Target, so
+ // re-running the same target would otherwise stack up a copy per run, each
+ // consulted for every frame.
+ StackFrameRecognizerManager &manager =
+ process->GetTarget().GetFrameRecognizerManager();
+ bool already_registered = false;
+ manager.ForEach([&](uint32_t, bool, std::string name, std::string,
+ llvm::ArrayRef<ConstString>, Mangled::NamePreference,
+ bool) {
+ if (name == g_name)
+ already_registered = true;
+ });
+ if (already_registered)
+ return;
+
+ manager.AddRecognizer(
std::make_shared<GNUstepObjCExceptionThrowFrameRecognizer>(),
- ConstString(), g_symbols, Mangled::NamePreference::ePreferDemangled,
+ ConstString(), g_symbols, Mangled::NamePreference::ePreferMangled,
/*first_instruction_only=*/true);
}
} // namespace
@@ -799,8 +831,11 @@ GNUstepObjCRuntime::GetExceptionObjectForThread(ThreadSP thread_sp) {
if (!descriptor_sp || !descriptor_sp->IsValid())
return {};
+ // The chain comes from inferior memory, so bound the walk rather than
+ // trusting it to terminate.
static const ConstString g_NSException("NSException");
- for (; descriptor_sp; descriptor_sp = descriptor_sp->GetSuperclass()) {
+ for (uint32_t depth = 0; descriptor_sp && depth < g_max_superclass_depth;
+ ++depth, descriptor_sp = descriptor_sp->GetSuperclass()) {
if (descriptor_sp->GetClassName() == g_NSException)
return cpp_exception_sp;
}
diff --git a/lldb/test/API/lang/objc-gnustep/exceptions/Makefile b/lldb/test/API/lang/objc-gnustep/exceptions/Makefile
new file mode 100644
index 0000000000000..845553d5e3f2f
--- /dev/null
+++ b/lldb/test/API/lang/objc-gnustep/exceptions/Makefile
@@ -0,0 +1,3 @@
+OBJC_SOURCES := main.m
+
+include Makefile.rules
diff --git a/lldb/test/API/lang/objc-gnustep/exceptions/TestGNUstepExceptions.py b/lldb/test/API/lang/objc-gnustep/exceptions/TestGNUstepExceptions.py
new file mode 100644
index 0000000000000..01e36bae73ca8
--- /dev/null
+++ b/lldb/test/API/lang/objc-gnustep/exceptions/TestGNUstepExceptions.py
@@ -0,0 +1,79 @@
+"""
+Test Objective-C exception support against gnustep-base's NSException.
+"""
+
+import lldb
+from lldbsuite.test.decorators import *
+from lldbsuite.test.lldbtest import *
+from lldbsuite.test import lldbutil
+
+
+class TestGNUstepExceptions(TestBase):
+ def test_formatter_in_handler(self):
+ """An NSException formats by its reason, from runtime metadata.
+
+ gnustep-base declares _e_name and _e_reason behind
+ GS_EXPOSE(NSException), so nothing here has debug info for them.
+ """
+ self.build()
+ lldbutil.run_to_source_breakpoint(
+ self, "// break in handler", lldb.SBFileSpec("main.m")
+ )
+ self.runCmd("settings set target.prefer-dynamic-value run-target")
+ self.expect(
+ "frame variable -d run-target caught",
+ substrs=['@"the bad thing happened"'],
+ )
+ self.expect("po caught", substrs=["the bad thing happened"])
+
+ def test_stops_at_throw(self):
+ """`breakpoint set -E objc` stops where the exception is raised, and
+ the frame recognizer presents the thrown object."""
+ self.build()
+ target = self.dbg.CreateTarget(self.getBuildArtifact("a.out"))
+ self.assertTrue(target, VALID_TARGET)
+ self.runCmd("breakpoint set -E objc")
+
+ process = target.LaunchSimple(None, None, self.get_process_working_directory())
+ self.assertTrue(process, PROCESS_IS_VALID)
+ self.assertEqual(process.GetState(), lldb.eStateStopped)
+
+ thread = lldbutil.get_stopped_thread(process, lldb.eStopReasonBreakpoint)
+ self.assertIsNotNone(thread, "stopped on the exception breakpoint")
+ # The recognizer's description surfaces through the command layer
+ # rather than through SBThread::GetStopDescription.
+ self.expect(
+ "thread list",
+ substrs=["stopped", "stop reason = hit Objective-C exception"],
+ )
+
+ # The recognizer synthesizes `exception` as an argument, so it is
+ # visible even though objc_exception_throw has no debug info.
+ exception = thread.GetCurrentException()
+ self.assertTrue(exception.IsValid(), "thread has a current exception")
+ self.assertIn("NSException", exception.GetTypeName())
+
+ def test_exception_is_recognized_argument(self):
+ """The thrown object is exposed as a recognized argument, which is
+ what lldb-dap's exception view reads."""
+ self.build()
+ target = self.dbg.CreateTarget(self.getBuildArtifact("a.out"))
+ self.assertTrue(target, VALID_TARGET)
+ self.runCmd("breakpoint set -E objc")
+ process = target.LaunchSimple(None, None, self.get_process_working_directory())
+ self.assertTrue(process, PROCESS_IS_VALID)
+ thread = lldbutil.get_stopped_thread(process, lldb.eStopReasonBreakpoint)
+ self.assertIsNotNone(thread)
+
+ options = lldb.SBVariablesOptions()
+ options.SetIncludeArguments(False)
+ options.SetIncludeRecognizedArguments(True)
+ options.SetIncludeLocals(False)
+ options.SetIncludeStatics(False)
+ variables = thread.GetFrameAtIndex(0).GetVariables(options)
+ self.assertEqual(variables.GetSize(), 1)
+ self.assertEqual(variables.GetValueAtIndex(0).GetName(), "exception")
+ self.assertEqual(
+ variables.GetValueAtIndex(0).GetValueType(),
+ lldb.eValueTypeVariableArgument,
+ )
diff --git a/lldb/test/API/lang/objc-gnustep/exceptions/categories b/lldb/test/API/lang/objc-gnustep/exceptions/categories
new file mode 100644
index 0000000000000..70b14bf34d6cc
--- /dev/null
+++ b/lldb/test/API/lang/objc-gnustep/exceptions/categories
@@ -0,0 +1 @@
+objc-gnustep-base
diff --git a/lldb/test/API/lang/objc-gnustep/exceptions/main.m b/lldb/test/API/lang/objc-gnustep/exceptions/main.m
new file mode 100644
index 0000000000000..4c0099df1c6bc
--- /dev/null
+++ b/lldb/test/API/lang/objc-gnustep/exceptions/main.m
@@ -0,0 +1,20 @@
+#import <Foundation/Foundation.h>
+
+static void raiser(void) {
+ [[NSException exceptionWithName:@"BadThingException"
+ reason:@"the bad thing happened"
+ userInfo:nil] raise];
+}
+
+int main(int argc, const char **argv) {
+ @autoreleasepool {
+ NSException *caught = nil;
+ @try {
+ raiser();
+ } @catch (NSException *e) {
+ caught = e;
+ NSLog(@"%@", [caught reason]); // break in handler
+ }
+ return caught == nil;
+ }
+}
>From c37b63d5b34385c1ca39dfa4a7414b302b552236 Mon Sep 17 00:00:00 2001
From: Rob <robk at robk.dev>
Date: Tue, 18 Aug 2026 10:07:28 +0100
Subject: [PATCH 35/49] [lldb][test] Add --no-debug-info to the Shell build
helper
`-g` was appended unconditionally, so a Shell test had no way to produce a
translation unit without debug info. That is needed to test anything that
has to work from a source other than debug info - a language runtime's own
metadata, for instance - and stripping afterwards is a poorer substitute,
because -g0 never emits the sections at all.
Assisted-by: Claude Opus 5
---
.../Shell/BuildScript/toolchain-clang.test | 6 ++++++
lldb/test/Shell/helper/build.py | 18 +++++++++++++++---
2 files changed, 21 insertions(+), 3 deletions(-)
diff --git a/lldb/test/Shell/BuildScript/toolchain-clang.test b/lldb/test/Shell/BuildScript/toolchain-clang.test
index 04a4551506783..175958661757e 100644
--- a/lldb/test/Shell/BuildScript/toolchain-clang.test
+++ b/lldb/test/Shell/BuildScript/toolchain-clang.test
@@ -12,3 +12,9 @@ CHECK-64: {{.*}}clang{{(\.EXE)?}} -m64 -g -O0 -c {{.*}}-o {{.*}}foo.exe-foobar.o
CHECK: linking foo.exe-foobar.o -> foo.exe
CHECK-32: {{.*}}clang++{{(\.EXE)?}} -m32 {{(-L.* )?(-Wl,-rpath,.* )?}}-o {{.*}}foo.exe {{.*}}foo.exe-foobar.o
CHECK-64: {{.*}}clang++{{(\.EXE)?}} -m64 {{(-L.* )?(-Wl,-rpath,.* )?}}-o {{.*}}foo.exe {{.*}}foo.exe-foobar.o
+
+RUN: %build -n --verbose --arch=64 --compiler=clang --no-debug-info --mode=compile-and-link -o %t/foo.exe foobar.c \
+RUN: | FileCheck --check-prefix=NODEBUG %s
+
+NODEBUG: compiling foobar.c -> foo.exe-foobar.o
+NODEBUG: {{.*}}clang{{(\.EXE)?}} -m64 -g0 -O0 -c {{.*}}-o {{.*}}foo.exe-foobar.o {{.*}}foobar.c
diff --git a/lldb/test/Shell/helper/build.py b/lldb/test/Shell/helper/build.py
index d09b28c9bc17b..b21bf6e3bf45c 100755
--- a/lldb/test/Shell/helper/build.py
+++ b/lldb/test/Shell/helper/build.py
@@ -113,6 +113,14 @@
help="When specified, the resulting image should not link against system libraries or include system headers. Useful when writing cross-targeting tests.",
)
+parser.add_argument(
+ "--no-debug-info",
+ dest="no_debug_info",
+ action="store_true",
+ default=False,
+ help="Compile without debug information. Useful for testing behaviour that has to work from something other than debug info, such as a language runtime's own metadata.",
+)
+
parser.add_argument(
"--opt",
dest="opt",
@@ -307,6 +315,7 @@ def __init__(self, toolchain_type, args, obj_ext):
self.output = args.output
self.mode = args.mode
self.nodefaultlib = args.nodefaultlib
+ self.no_debug_info = args.no_debug_info
self.verbose = args.verbose
self.obj_ext = obj_ext
self.lib_paths = args.libs_dir
@@ -694,7 +703,8 @@ def _get_compilation_command(self, source, obj):
if self.nodefaultlib:
args.append("/GS-")
args.append("/GR-")
- args.append("/Z7")
+ if not self.no_debug_info:
+ args.append("/Z7")
if self.toolchain_type == "clang-cl":
args.append("-Xclang")
args.append("-fkeep-static-consts")
@@ -764,7 +774,7 @@ def _get_compilation_command(self, source, obj):
args = [self.compiler_for_file(source)]
args = self._add_m_option_if_needed(args)
- args.append("-g")
+ args.append("-g0" if self.no_debug_info else "-g")
if self.opt == "none":
args.append("-O0")
elif self.opt == "basic":
@@ -785,7 +795,9 @@ def _get_compilation_command(self, source, obj):
# CodeView cannot represent Objective-C types, so force
# DWARF even though the target is MSVC. The debugger needs
# it to recognize classes and resolve dynamic types.
- args.extend(["-gdwarf", "-Xclang", "--dependent-lib=msvcrtd"])
+ if not self.no_debug_info:
+ args.append("-gdwarf")
+ args.extend(["-Xclang", "--dependent-lib=msvcrtd"])
elif self.sysroot:
args.extend(["--sysroot", self.sysroot])
>From 43705e9d043570f3b0b350cb82dca2a3b483400a Mon Sep 17 00:00:00 2001
From: Rob <robk at robk.dev>
Date: Tue, 18 Aug 2026 10:07:30 +0100
Subject: [PATCH 36/49] [lldb][GNUstep] Synthesize interfaces from runtime
metadata
Adds a DeclVendor, so a class the debug info does not describe is still
usable: `type lookup` prints it, `frame variable` expands it, and an
expression can name its type and reach its ivars. Until now
GetDeclVendor() returned nullptr, so ClangASTSource::FindDeclInObjCRuntime
returned silently and the class was a dead end.
Interfaces are built lazily - FindDecls returns a forward declaration and
the ivars are filled in only when clang asks for the definition - which
keeps a lookup for one class from dragging in its whole superclass chain.
FinishDecl starts the definition and clears the external-storage bits
*before* running the callbacks, because the superclass leg is eager and a
class reaching itself through its isa chain would otherwise recurse without
bound.
Methods are not vended yet. After __objc_load a selector's name field holds
a numeric dispatch index rather than a string (selector_table.cc), so
recovering names needs either symbols for the method functions or a call
into the runtime; ivars are what make a class inspectable, and an interface
completed from them alone is well formed.
GetRuntimeType is overridden at the same time, and necessarily so. The
inherited implementation consults debug info through
LookupInCompleteClassCache, which keys on an eSymbolTypeObjCClass symbol
that only Mach-O produces - so for gnustep-2.x it always misses and the
synthesized interface would win by default. That is a downgrade wherever
debug info exists, because the runtime's encodings cannot describe members
inside a struct-typed ivar: without this, the NSString and NSDictionary
formatters lose `_flags.wide` and the GSIMapTable walk, which the
data-formatters test caught.
Assisted-by: Claude Opus 5
---
lldb/include/lldb/Symbol/DeclVendor.h | 1 +
.../ObjC/GNUstepObjCRuntime/CMakeLists.txt | 1 +
.../GNUstepObjCClassDescriptor.cpp | 28 ++
.../GNUstepObjCClassDescriptor.h | 19 ++
.../GNUstepObjCDeclVendor.cpp | 255 ++++++++++++++++++
.../GNUstepObjCDeclVendor.h | 77 ++++++
.../GNUstepObjCRuntime/GNUstepObjCRuntime.cpp | 43 ++-
.../GNUstepObjCRuntime/GNUstepObjCRuntime.h | 22 ++
.../Expr/Inputs/objc-gnustep-hidden-class.m | 44 +++
.../Shell/Expr/objc-gnustep-decl-vendor.m | 57 ++++
10 files changed, 546 insertions(+), 1 deletion(-)
create mode 100644 lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCDeclVendor.cpp
create mode 100644 lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCDeclVendor.h
create mode 100644 lldb/test/Shell/Expr/Inputs/objc-gnustep-hidden-class.m
create mode 100644 lldb/test/Shell/Expr/objc-gnustep-decl-vendor.m
diff --git a/lldb/include/lldb/Symbol/DeclVendor.h b/lldb/include/lldb/Symbol/DeclVendor.h
index 5b0cbf9a15357..375308781288c 100644
--- a/lldb/include/lldb/Symbol/DeclVendor.h
+++ b/lldb/include/lldb/Symbol/DeclVendor.h
@@ -22,6 +22,7 @@ class DeclVendor {
enum DeclVendorKind {
eClangModuleDeclVendor,
eAppleObjCDeclVendor,
+ eGNUstepObjCDeclVendor,
eLastClangDeclVendor,
};
// Constructors and Destructors
diff --git a/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/CMakeLists.txt b/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/CMakeLists.txt
index 46cf4ef300797..1a906f5fa8ab0 100644
--- a/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/CMakeLists.txt
+++ b/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/CMakeLists.txt
@@ -1,5 +1,6 @@
add_lldb_library(lldbPluginGNUstepObjCRuntime PLUGIN
GNUstepObjCClassDescriptor.cpp
+ GNUstepObjCDeclVendor.cpp
GNUstepObjCTypeEncodingParser.cpp
GNUstepObjCRuntime.cpp
GNUstepThreadPlanStepThroughObjCTrampoline.cpp
diff --git a/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCClassDescriptor.cpp b/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCClassDescriptor.cpp
index 78031e0ca604e..b21670616a3af 100644
--- a/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCClassDescriptor.cpp
+++ b/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCClassDescriptor.cpp
@@ -172,6 +172,34 @@ void GNUstepObjCClassDescriptor::Read() {
m_valid = true;
}
+bool GNUstepObjCClassDescriptor::Describe(
+ std::function<void(ObjCLanguageRuntime::ObjCISA)> const &superclass_func,
+ std::function<bool(const char *, const char *)> const &instance_method_func,
+ std::function<bool(const char *, const char *)> const &class_method_func,
+ std::function<bool(const char *, const char *, lldb::addr_t, uint64_t)> const
+ &ivar_func) const {
+ if (!m_valid)
+ return false;
+
+ // A root class has no superclass to report; libobjc2 leaves the field null
+ // once the class is resolved.
+ if (superclass_func && m_superclass_isa != 0)
+ superclass_func(m_superclass_isa);
+
+ if (ivar_func) {
+ for (const RawIvar &ivar : ReadIvarList()) {
+ // libobjc2 stores a pointer to the offset so it can rewrite it in
+ // place, and that pointer is what Apple's runtime reports here, so
+ // pass the already-resolved offset as the address. Consumers that only
+ // want the value - which is all of them in tree - are unaffected.
+ if (ivar_func(ivar.name.GetCString(), ivar.type_encoding.c_str(),
+ static_cast<lldb::addr_t>(ivar.offset), ivar.size))
+ break;
+ }
+ }
+ return true;
+}
+
std::vector<GNUstepObjCClassDescriptor::RawIvar>
GNUstepObjCClassDescriptor::ReadIvarList() const {
std::vector<RawIvar> ivars;
diff --git a/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCClassDescriptor.h b/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCClassDescriptor.h
index 19b51856e0f21..9b3b095e88d27 100644
--- a/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCClassDescriptor.h
+++ b/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCClassDescriptor.h
@@ -97,6 +97,25 @@ class GNUstepObjCClassDescriptor : public ObjCLanguageRuntime::ClassDescriptor {
/// producer (associate.m).
bool IsKVO() override { return m_is_hidden; }
+ /// Reports this class's superclass and ivars to \p superclass_func and
+ /// \p ivar_func. Any callback may be null.
+ ///
+ /// Methods are not reported yet: after __objc_load, a selector's name field
+ /// holds a numeric dispatch index rather than a string (selector_table.cc),
+ /// so recovering names needs either symbols for the method functions or a
+ /// call into the runtime, and neither belongs in a descriptor that promises
+ /// not to execute code. Returning true with no methods is well defined -
+ /// the callbacks are optional - and lets the interface be completed from
+ /// its ivars alone.
+ bool Describe(std::function<void(ObjCLanguageRuntime::ObjCISA)> const
+ &superclass_func,
+ std::function<bool(const char *, const char *)> const
+ &instance_method_func,
+ std::function<bool(const char *, const char *)> const
+ &class_method_func,
+ std::function<bool(const char *, const char *, lldb::addr_t,
+ uint64_t)> const &ivar_func) const override;
+
size_t GetNumIVars() override;
iVarDescriptor GetIVarAtIndex(size_t idx) override;
diff --git a/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCDeclVendor.cpp b/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCDeclVendor.cpp
new file mode 100644
index 0000000000000..329fb9ea7c5bf
--- /dev/null
+++ b/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCDeclVendor.cpp
@@ -0,0 +1,255 @@
+//===-- GNUstepObjCDeclVendor.cpp -----------------------------------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+#include "GNUstepObjCDeclVendor.h"
+
+#include "Plugins/ExpressionParser/Clang/ClangUtil.h"
+#include "Plugins/TypeSystem/Clang/TypeSystemClang.h"
+
+#include "lldb/Core/Module.h"
+#include "lldb/Target/Process.h"
+#include "lldb/Target/Target.h"
+#include "lldb/Utility/LLDBLog.h"
+#include "lldb/Utility/Log.h"
+
+#include "clang/AST/ASTContext.h"
+#include "clang/AST/DeclObjC.h"
+#include "clang/AST/ExternalASTSource.h"
+
+using namespace lldb;
+using namespace lldb_private;
+
+namespace lldb_private {
+
+/// Completes an interface the moment clang needs its members, rather than
+/// when the class was first named. Mirrors AppleObjCExternalASTSource.
+class GNUstepObjCExternalASTSource : public clang::ExternalASTSource {
+public:
+ explicit GNUstepObjCExternalASTSource(GNUstepObjCDeclVendor &decl_vendor)
+ : m_decl_vendor(decl_vendor) {}
+
+ bool FindExternalVisibleDeclsByName(
+ const clang::DeclContext *decl_ctx, clang::DeclarationName name,
+ const clang::DeclContext *original_dc) override {
+ auto *interface_decl = llvm::dyn_cast<clang::ObjCInterfaceDecl>(decl_ctx);
+ if (!interface_decl) {
+ SetNoExternalVisibleDeclsForName(decl_ctx, name);
+ return false;
+ }
+ if (!m_decl_vendor.FinishDecl(
+ const_cast<clang::ObjCInterfaceDecl *>(interface_decl)))
+ return false;
+ return !interface_decl->lookup(name).empty();
+ }
+
+ void CompleteType(clang::TagDecl *tag_decl) override {}
+
+ void CompleteType(clang::ObjCInterfaceDecl *interface_decl) override {
+ m_decl_vendor.FinishDecl(interface_decl);
+ }
+
+ bool layoutRecordType(
+ const clang::RecordDecl *Record, uint64_t &Size, uint64_t &Alignment,
+ llvm::DenseMap<const clang::FieldDecl *, uint64_t> &FieldOffsets,
+ llvm::DenseMap<const clang::CXXRecordDecl *, clang::CharUnits>
+ &BaseOffsets,
+ llvm::DenseMap<const clang::CXXRecordDecl *, clang::CharUnits>
+ &VirtualBaseOffsets) override {
+ return false;
+ }
+
+ void StartTranslationUnit(clang::ASTConsumer *Consumer) override {
+ clang::TranslationUnitDecl *tu_decl =
+ m_decl_vendor.m_ast_ctx_sp->getASTContext().getTranslationUnitDecl();
+ tu_decl->setHasExternalVisibleStorage();
+ tu_decl->setHasExternalLexicalStorage();
+ }
+
+private:
+ GNUstepObjCDeclVendor &m_decl_vendor;
+};
+
+} // namespace lldb_private
+
+GNUstepObjCDeclVendor::GNUstepObjCDeclVendor(ObjCLanguageRuntime &runtime)
+ : DeclVendor(eGNUstepObjCDeclVendor), m_runtime(runtime) {
+ m_ast_ctx_sp = std::make_shared<TypeSystemClang>(
+ "GNUstepObjCDeclVendor AST",
+ runtime.GetProcess()->GetTarget().GetArchitecture().GetTriple());
+ auto external_source_owning_ptr =
+ llvm::makeIntrusiveRefCnt<GNUstepObjCExternalASTSource>(*this);
+ m_external_source = external_source_owning_ptr.get();
+ m_ast_ctx_sp->getASTContext().setExternalSource(external_source_owning_ptr);
+}
+
+clang::ObjCInterfaceDecl *
+GNUstepObjCDeclVendor::GetDeclForISA(ObjCLanguageRuntime::ObjCISA isa) {
+ auto iter = m_isa_to_interface.find(isa);
+ if (iter != m_isa_to_interface.end())
+ return iter->second;
+
+ ObjCLanguageRuntime::ClassDescriptorSP descriptor =
+ m_runtime.GetClassDescriptorFromISA(isa);
+ if (!descriptor)
+ return nullptr;
+ ConstString name(descriptor->GetClassName());
+ if (!name)
+ return nullptr;
+
+ clang::ASTContext &ast_ctx = m_ast_ctx_sp->getASTContext();
+ clang::IdentifierInfo &identifier_info =
+ ast_ctx.Idents.get(name.GetStringRef());
+
+ clang::ObjCInterfaceDecl *new_iface_decl = clang::ObjCInterfaceDecl::Create(
+ ast_ctx, ast_ctx.getTranslationUnitDecl(), clang::SourceLocation(),
+ &identifier_info, /*typeParamList=*/nullptr, /*PrevDecl=*/nullptr);
+
+ // The ISA is how FinishDecl gets back to the runtime from a bare decl.
+ ClangASTMetadata meta_data;
+ meta_data.SetISAPtr(isa);
+ m_ast_ctx_sp->SetMetadata(new_iface_decl, meta_data);
+
+ new_iface_decl->setHasExternalVisibleStorage();
+ new_iface_decl->setHasExternalLexicalStorage();
+ ast_ctx.getTranslationUnitDecl()->addDecl(new_iface_decl);
+
+ m_isa_to_interface[isa] = new_iface_decl;
+ return new_iface_decl;
+}
+
+bool GNUstepObjCDeclVendor::FinishDecl(
+ clang::ObjCInterfaceDecl *interface_decl) {
+ if (!interface_decl)
+ return false;
+
+ std::optional<ClangASTMetadata> meta_data =
+ m_ast_ctx_sp->GetMetadata(interface_decl);
+ if (!meta_data)
+ return false;
+ const ObjCLanguageRuntime::ObjCISA isa = meta_data->GetISAPtr();
+ if (isa == 0)
+ return false;
+
+ // Already done. This also terminates the superclass recursion below: a
+ // cycle in the isa graph re-enters here and stops.
+ if (!interface_decl->hasExternalVisibleStorage())
+ return true;
+
+ // The definition has to be started, and the external-storage bits cleared,
+ // *before* the callbacks run - otherwise a class that reaches itself
+ // through its superclass chain recurses without bound.
+ interface_decl->startDefinition();
+ interface_decl->setHasExternalVisibleStorage(false);
+ interface_decl->setHasExternalLexicalStorage(false);
+
+ ObjCLanguageRuntime::ClassDescriptorSP descriptor =
+ m_runtime.GetClassDescriptorFromISA(isa);
+ if (!descriptor)
+ return false;
+
+ ObjCLanguageRuntime::EncodingToTypeSP encoding_to_type_sp =
+ m_runtime.GetEncodingToType();
+ Log *log = GetLog(LLDBLog::Types);
+
+ auto superclass_func = [this,
+ interface_decl](ObjCLanguageRuntime::ObjCISA super) {
+ clang::ObjCInterfaceDecl *superclass_decl = GetDeclForISA(super);
+ if (!superclass_decl)
+ return;
+ // A superclass has to be complete before it can be attached, so this is
+ // eager where everything else here is lazy.
+ FinishDecl(superclass_decl);
+ clang::ASTContext &ast_ctx = m_ast_ctx_sp->getASTContext();
+ interface_decl->setSuperClass(ast_ctx.getTrivialTypeSourceInfo(
+ ast_ctx.getObjCInterfaceType(superclass_decl)));
+ };
+
+ auto ivar_func = [this, interface_decl, &encoding_to_type_sp,
+ log](const char *name, const char *type,
+ lldb::addr_t offset, uint64_t size) -> bool {
+ if (!name || !type || !encoding_to_type_sp)
+ return false;
+ // Deliberately not for_expression: an ivar's precise Objective-C type
+ // would send the parser back through this vendor while it is mid-
+ // FinishDecl, and dynamic typing resolves it anyway.
+ CompilerType ivar_type = encoding_to_type_sp->RealizeType(
+ *m_ast_ctx_sp, type, /*for_expression=*/false);
+ if (!ivar_type) {
+ // The ivar still occupies space even when its encoding cannot be
+ // realized. Dropping it would leave clang to lay the interface out from
+ // the ivars that remain, silently shifting the offset of every ivar
+ // after it - so the debugger would report wrong values rather than
+ // decline to answer. Stand in an opaque block of the right size.
+ LLDB_LOG(log,
+ "GNUstep ivar {0} has an unrealizable type {1}; substituting "
+ "an opaque {2}-byte placeholder",
+ name, type, size);
+ if (size == 0)
+ return false;
+ ivar_type = m_ast_ctx_sp->CreateArrayType(
+ m_ast_ctx_sp->GetBasicType(lldb::eBasicTypeChar), size,
+ /*is_vector=*/false);
+ if (!ivar_type)
+ return false;
+ }
+ clang::ASTContext &ast_ctx = m_ast_ctx_sp->getASTContext();
+ clang::ObjCIvarDecl *ivar_decl = clang::ObjCIvarDecl::Create(
+ ast_ctx, interface_decl, clang::SourceLocation(),
+ clang::SourceLocation(), &ast_ctx.Idents.get(name),
+ ClangUtil::GetQualType(ivar_type), /*TInfo=*/nullptr,
+ clang::ObjCIvarDecl::Public, /*BW=*/nullptr, /*synthesized=*/false);
+ if (ivar_decl)
+ interface_decl->addDecl(ivar_decl);
+ return false;
+ };
+
+ if (!descriptor->Describe(superclass_func, /*instance_method_func=*/nullptr,
+ /*class_method_func=*/nullptr, ivar_func)) {
+ LLDB_LOG(log, "GNUstep runtime could not describe class {0}",
+ descriptor->GetClassName());
+ return false;
+ }
+ return true;
+}
+
+uint32_t GNUstepObjCDeclVendor::FindDecls(ConstString name, bool append,
+ uint32_t max_matches,
+ std::vector<CompilerDecl> &decls) {
+ if (!append)
+ decls.clear();
+ if (!name || max_matches == 0)
+ return 0;
+
+ clang::ASTContext &ast_ctx = m_ast_ctx_sp->getASTContext();
+ clang::IdentifierInfo &identifier_info =
+ ast_ctx.Idents.get(name.GetStringRef());
+ clang::DeclarationName decl_name =
+ ast_ctx.DeclarationNames.getIdentifier(&identifier_info);
+
+ // Anything already vended stays vended, so repeated lookups of the same
+ // name return the same decl rather than a fresh one.
+ for (clang::NamedDecl *candidate :
+ ast_ctx.getTranslationUnitDecl()->lookup(decl_name)) {
+ if (auto *result_iface_decl =
+ llvm::dyn_cast<clang::ObjCInterfaceDecl>(candidate)) {
+ decls.push_back(m_ast_ctx_sp->GetCompilerDecl(result_iface_decl));
+ return 1;
+ }
+ }
+
+ const ObjCLanguageRuntime::ObjCISA isa = m_runtime.GetISA(name);
+ if (isa == 0)
+ return 0;
+
+ clang::ObjCInterfaceDecl *iface_decl = GetDeclForISA(isa);
+ if (!iface_decl)
+ return 0;
+
+ decls.push_back(m_ast_ctx_sp->GetCompilerDecl(iface_decl));
+ return 1;
+}
diff --git a/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCDeclVendor.h b/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCDeclVendor.h
new file mode 100644
index 0000000000000..13ca60e03e788
--- /dev/null
+++ b/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCDeclVendor.h
@@ -0,0 +1,77 @@
+//===-- GNUstepObjCDeclVendor.h ---------------------------------*- C++ -*-===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+#ifndef LLDB_SOURCE_PLUGINS_LANGUAGERUNTIME_OBJC_GNUSTEPOBJCRUNTIME_GNUSTEPOBJCDECLVENDOR_H
+#define LLDB_SOURCE_PLUGINS_LANGUAGERUNTIME_OBJC_GNUSTEPOBJCRUNTIME_GNUSTEPOBJCDECLVENDOR_H
+
+#include "Plugins/LanguageRuntime/ObjC/ObjCLanguageRuntime.h"
+#include "lldb/Symbol/DeclVendor.h"
+#include "lldb/lldb-private.h"
+
+#include "llvm/ADT/DenseMap.h"
+
+#include "clang/AST/DeclObjC.h"
+
+namespace lldb_private {
+
+class GNUstepObjCExternalASTSource;
+
+/// Builds Objective-C interface declarations out of libobjc2's runtime
+/// metadata, so that a class the debug info does not describe is still
+/// usable: `frame variable` can show its ivars, `expr` can name its type,
+/// and `type lookup` can print it.
+///
+/// Declarations are created lazily. FindDecls returns a forward declaration;
+/// the ivars are filled in only when clang asks for the definition, through
+/// an ExternalASTSource. That laziness is what keeps a lookup for one class
+/// from dragging in its whole superclass chain and every type they mention.
+class GNUstepObjCDeclVendor : public DeclVendor {
+public:
+ explicit GNUstepObjCDeclVendor(ObjCLanguageRuntime &runtime);
+
+ ~GNUstepObjCDeclVendor() override = default;
+
+ static bool classof(const DeclVendor *vendor) {
+ return vendor->GetKind() == eGNUstepObjCDeclVendor;
+ }
+
+ uint32_t FindDecls(ConstString name, bool append, uint32_t max_matches,
+ std::vector<CompilerDecl> &decls) override;
+
+ /// Fills in \p interface_decl's superclass and ivars. Called by the AST
+ /// source when clang first needs the definition, and directly for a
+ /// superclass, which has to be complete before it can be attached.
+ ///
+ /// Returns false if the runtime could not describe the class, in which
+ /// case the interface is left as a definition with no members.
+ bool FinishDecl(clang::ObjCInterfaceDecl *interface_decl);
+
+private:
+ /// Returns a forward declaration for \p isa, creating it if needed.
+ clang::ObjCInterfaceDecl *GetDeclForISA(ObjCLanguageRuntime::ObjCISA isa);
+
+ ObjCLanguageRuntime &m_runtime;
+
+ /// The vendor owns its own AST. Decls are copied into the expression
+ /// parser's AST by ClangASTImporter when they are used, so they must not
+ /// be created in a context that could outlive or conflict with it.
+ std::shared_ptr<TypeSystemClang> m_ast_ctx_sp;
+
+ /// Not owned; the AST context holds the reference that keeps it alive.
+ GNUstepObjCExternalASTSource *m_external_source = nullptr;
+
+ llvm::DenseMap<ObjCLanguageRuntime::ObjCISA, clang::ObjCInterfaceDecl *>
+ m_isa_to_interface;
+
+ // The AST source completes decls on demand and needs the vendor's AST.
+ friend class GNUstepObjCExternalASTSource;
+};
+
+} // namespace lldb_private
+
+#endif // LLDB_SOURCE_PLUGINS_LANGUAGERUNTIME_OBJC_GNUSTEPOBJCRUNTIME_GNUSTEPOBJCDECLVENDOR_H
diff --git a/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCRuntime.cpp b/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCRuntime.cpp
index 932ad65eadd0f..8f0eff094b8fe 100644
--- a/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCRuntime.cpp
+++ b/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCRuntime.cpp
@@ -8,6 +8,7 @@
#include "GNUstepObjCRuntime.h"
#include "GNUstepObjCClassDescriptor.h"
+#include "GNUstepObjCDeclVendor.h"
#include "GNUstepObjCTypeEncodingParser.h"
#include "GNUstepThreadPlanStepThroughObjCTrampoline.h"
@@ -354,7 +355,15 @@ LanguageRuntime *GNUstepObjCRuntime::CreateInstance(Process *process,
return new GNUstepObjCRuntime(process);
}
-GNUstepObjCRuntime::~GNUstepObjCRuntime() = default;
+GNUstepObjCRuntime::~GNUstepObjCRuntime() {
+ // The encoding parser caches clang::QualTypes keyed by the TypeSystemClang
+ // that minted them, and one of those ASTs belongs to the decl vendor. A
+ // QualType is a tagged pointer and dropping the cache never dereferences
+ // one, so this is ordering hygiene rather than a live hazard - but state it
+ // here instead of leaving it to depend on member declaration order.
+ m_encoding_to_type_sp.reset();
+ m_decl_vendor_up.reset();
+}
GNUstepObjCRuntime::GNUstepObjCRuntime(Process *process)
: ObjCLanguageRuntime(process), m_objc_module_sp(nullptr),
@@ -1210,6 +1219,38 @@ GNUstepObjCRuntime::GetTypeBitSize(const CompilerType &compiler_type) {
return instance_size * 8;
}
+std::optional<CompilerType>
+GNUstepObjCRuntime::GetRuntimeType(CompilerType base_type) {
+ CompilerType class_type;
+ bool is_pointer_type = false;
+ if (TypeSystemClang::IsObjCObjectPointerType(base_type, &class_type))
+ is_pointer_type = true;
+ else if (TypeSystemClang::IsObjCObjectOrInterfaceType(base_type))
+ class_type = base_type;
+ else
+ return std::nullopt;
+ if (!class_type)
+ return std::nullopt;
+
+ ConstString class_name(class_type.GetTypeName());
+ if (!class_name)
+ return std::nullopt;
+
+ if (TypeSP type_sp = LookupClassTypeInDebugInfo(class_name)) {
+ if (CompilerType complete_type = type_sp->GetFullCompilerType();
+ complete_type.GetCompleteType())
+ return is_pointer_type ? complete_type.GetPointerType() : complete_type;
+ }
+
+ return ObjCLanguageRuntime::GetRuntimeType(base_type);
+}
+
+DeclVendor *GNUstepObjCRuntime::GetDeclVendor() {
+ if (!m_decl_vendor_up)
+ m_decl_vendor_up = std::make_unique<GNUstepObjCDeclVendor>(*this);
+ return m_decl_vendor_up.get();
+}
+
ObjCLanguageRuntime::EncodingToTypeSP
GNUstepObjCRuntime::GetEncodingToType() {
if (!m_encoding_to_type_sp)
diff --git a/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCRuntime.h b/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCRuntime.h
index a4354a22979b6..1817b41f9ee9d 100644
--- a/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCRuntime.h
+++ b/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCRuntime.h
@@ -27,6 +27,7 @@
namespace lldb_private {
class GNUstepTaggedPointerVendor;
+class GNUstepObjCDeclVendor;
class GNUstepObjCRuntime : public lldb_private::ObjCLanguageRuntime {
public:
@@ -148,6 +149,23 @@ class GNUstepObjCRuntime : public lldb_private::ObjCLanguageRuntime {
/// the types it returns are only valid in the AST it created them in.
EncodingToTypeSP GetEncodingToType() override;
+ /// Synthesizes Objective-C interface declarations from runtime metadata,
+ /// so a class the debug info does not describe is still usable. Built on
+ /// first use; the decls it owns are copied into the expression parser's
+ /// AST as they are needed.
+ DeclVendor *GetDeclVendor() override;
+
+ /// Resolves \p base_type to the most complete description available.
+ ///
+ /// The inherited implementation consults debug info through
+ /// LookupInCompleteClassCache, which keys on an eSymbolTypeObjCClass
+ /// symbol that only Mach-O produces - so for gnustep-2.x it always misses
+ /// and the runtime-synthesized interface wins by default. That is a
+ /// downgrade wherever debug info exists, because the runtime's metadata
+ /// cannot describe members inside a struct-typed ivar. Prefer debug info
+ /// explicitly, and fall back to the inherited behaviour otherwise.
+ std::optional<CompilerType> GetRuntimeType(CompilerType base_type) override;
+
/// Size of an Objective-C class, in bits.
///
/// The inherited implementation derives this from the ivar list, as the end
@@ -257,7 +275,11 @@ class GNUstepObjCRuntime : public lldb_private::ObjCLanguageRuntime {
/// raised rather than where it is caught.
lldb::BreakpointSP m_objc_exception_bp_sp;
+ /// Torn down in an explicit order by the destructor, which see: the parser
+ /// caches types belonging to the vendor's AST.
EncodingToTypeSP m_encoding_to_type_sp;
+
+ std::unique_ptr<GNUstepObjCDeclVendor> m_decl_vendor_up;
};
} // namespace lldb_private
diff --git a/lldb/test/Shell/Expr/Inputs/objc-gnustep-hidden-class.m b/lldb/test/Shell/Expr/Inputs/objc-gnustep-hidden-class.m
new file mode 100644
index 0000000000000..3ef3731afc5da
--- /dev/null
+++ b/lldb/test/Shell/Expr/Inputs/objc-gnustep-hidden-class.m
@@ -0,0 +1,44 @@
+// Compiled without debug information, so the only description of Hidden
+// that reaches the debugger is libobjc2's own runtime metadata. The header
+// this file's users see declares nothing but the class name.
+
+#import "objc/runtime.h"
+
+ at protocol NSCoding
+ at end
+
+#ifdef __has_attribute
+#if __has_attribute(objc_root_class)
+__attribute__((objc_root_class))
+#endif
+#endif
+ at interface NSObject <NSCoding> {
+ id isa;
+}
++ (id)new;
+ at end
+ at implementation NSObject
++ (id)new {
+ return class_createInstance(self, 0);
+}
+ at end
+
+ at interface Hidden : NSObject {
+ at public
+ int _int;
+ float _float;
+ char _char;
+ void *_ptr;
+}
+ at end
+ at implementation Hidden
+ at end
+
+id MakeHidden(void) {
+ Hidden *hidden = [Hidden new];
+ hidden->_int = 1;
+ hidden->_float = 2.0f;
+ hidden->_char = '\3';
+ hidden->_ptr = (void *)4;
+ return hidden;
+}
diff --git a/lldb/test/Shell/Expr/objc-gnustep-decl-vendor.m b/lldb/test/Shell/Expr/objc-gnustep-decl-vendor.m
new file mode 100644
index 0000000000000..d436dc8df40c8
--- /dev/null
+++ b/lldb/test/Shell/Expr/objc-gnustep-decl-vendor.m
@@ -0,0 +1,57 @@
+// REQUIRES: objc-gnustep
+//
+// A class whose only description is libobjc2's runtime metadata: the other
+// half of this program is compiled with -g0, and nothing here declares
+// anything about Hidden beyond its name.
+//
+// RUN: %build %S/Inputs/objc-gnustep-hidden-class.m --compiler=clang --objc-gnustep \
+// RUN: --no-debug-info --mode=compile --output=%t-hidden.o
+// RUN: %build %s --compiler=clang --objc-gnustep --mode=compile --output=%t-main.o
+// RUN: %build %t-hidden.o %t-main.o --compiler=clang --objc-gnustep --mode=link --output=%t
+
+ at class Hidden;
+id MakeHidden(void);
+
+int main() {
+ Hidden *hidden = (Hidden *)MakeHidden();
+ return hidden != 0; // break here
+}
+
+// The premise: the debug info really does not describe this class, so
+// `image lookup -t` finds nothing and exits non-zero. If this ever starts
+// matching, the rest of the test proves nothing.
+//
+// RUN: not %lldb -b -o "image lookup -t Hidden" -- %t \
+// RUN: | FileCheck %s --check-prefix=NODWARF
+//
+// NODWARF-NOT: name = "Hidden"
+
+// The interface is synthesized from the runtime instead, so the type can be
+// named and its ivars read.
+//
+// RUN: %lldb -b -o "b objc-gnustep-decl-vendor.m:17" -o "run" \
+// RUN: -o "type lookup Hidden" \
+// RUN: -o "expr -- ((Hidden *)hidden)->_int" \
+// RUN: -o "expr -- ((Hidden *)hidden)->_float" \
+// RUN: -o "frame variable -d run-target *hidden" \
+// RUN: -- %t | FileCheck %s --check-prefix=VENDOR
+//
+// VENDOR: (lldb) type lookup Hidden
+// VENDOR: @interface Hidden
+// VENDOR-DAG: int _int;
+// VENDOR-DAG: float _float;
+// VENDOR-DAG: char _char;
+//
+// VENDOR: (lldb) expr -- ((Hidden *)hidden)->_int
+// VENDOR: (int) $0 = 1
+//
+// VENDOR: (lldb) expr -- ((Hidden *)hidden)->_float
+// VENDOR: (float) $1 = 2
+//
+// And the object expands in full, which is what a debugger UI shows.
+//
+// VENDOR: (lldb) frame variable -d run-target *hidden
+// VENDOR-DAG: _int = 1
+// VENDOR-DAG: _float = 2
+// VENDOR-DAG: _char = '{{.*}}3'
+// VENDOR-DAG: _ptr = 0x{{0*}}4
>From fedab12797ddb4362561dbe4964b58fc394fd7d6 Mon Sep 17 00:00:00 2001
From: Rob <robk at robk.dev>
Date: Tue, 18 Aug 2026 10:15:52 +0100
Subject: [PATCH 37/49] [lldb][GNUstep] Vend methods from runtime metadata
Completes the DeclVendor: a message send to a class with no debug info now
type-checks and runs, where before only its ivars were reachable.
Recovering a method's name is the awkward part. After __objc_load a
selector's name field holds a numeric dispatch index rather than a string
(selector_table.cc), so it cannot be read back from memory. It is recovered
from the symbol clang emits for the selector instead,
".objc_selector_<name>_<mangled types>" (CGObjCGNU.cpp). A selector name can
contain both '_' and ':', so the boundary is not findable by scanning; the
suffix is subtracted instead, using the selector's own `types` string, which
survives registration untouched. No code is run in the inferior to do this.
Two details worth naming. The first selector in a section shares an address
with the section's start sentinel, so every symbol at the address is
considered rather than whichever an exact lookup returns. And a class's
class methods are its metaclass's instance methods, as they are in Apple's
runtime, so Describe recurses into the metaclass for them.
The same selector legitimately appears more than once - categories prepend
their lists and may override a method - so methods are deduplicated by
selector before being turned into decls.
Assisted-by: Claude Opus 5
---
.../GNUstepObjCClassDescriptor.cpp | 120 +++++++++++-
.../GNUstepObjCClassDescriptor.h | 48 +++--
.../GNUstepObjCDeclVendor.cpp | 180 +++++++++++++++++-
.../GNUstepObjCDeclVendor.h | 7 +
.../GNUstepObjCRuntime/GNUstepObjCRuntime.cpp | 75 +++++++-
.../GNUstepObjCRuntime/GNUstepObjCRuntime.h | 9 +
.../Expr/Inputs/objc-gnustep-hidden-class.m | 12 ++
.../Shell/Expr/objc-gnustep-decl-vendor.m | 19 ++
8 files changed, 441 insertions(+), 29 deletions(-)
diff --git a/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCClassDescriptor.cpp b/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCClassDescriptor.cpp
index b21670616a3af..382a5fa142c8b 100644
--- a/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCClassDescriptor.cpp
+++ b/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCClassDescriptor.cpp
@@ -7,6 +7,7 @@
//===----------------------------------------------------------------------===//
#include "GNUstepObjCClassDescriptor.h"
+#include "GNUstepObjCRuntime.h"
#include "lldb/Core/Module.h"
#include "lldb/Core/ModuleList.h"
@@ -50,6 +51,8 @@ static constexpr size_t g_max_type_encoding_length = 1024;
// that is how the runtime walks these arrays - but a stride this large is
// not something a compiler emitted.
static constexpr uint64_t g_max_element_stride = 1024;
+static constexpr uint32_t g_max_methods = 8192;
+static constexpr uint32_t g_max_method_lists = 512;
// An upper bound for plausible class names. A string that does not terminate
// within this many bytes is not a class name, and stopping there keeps a
@@ -176,8 +179,8 @@ bool GNUstepObjCClassDescriptor::Describe(
std::function<void(ObjCLanguageRuntime::ObjCISA)> const &superclass_func,
std::function<bool(const char *, const char *)> const &instance_method_func,
std::function<bool(const char *, const char *)> const &class_method_func,
- std::function<bool(const char *, const char *, lldb::addr_t, uint64_t)> const
- &ivar_func) const {
+ std::function<bool(const char *, const char *, lldb::addr_t,
+ uint64_t)> const &ivar_func) const {
if (!m_valid)
return false;
@@ -186,6 +189,40 @@ bool GNUstepObjCClassDescriptor::Describe(
if (superclass_func && m_superclass_isa != 0)
superclass_func(m_superclass_isa);
+ if (instance_method_func || class_method_func) {
+ ProcessSP process_sp = m_process_wp.lock();
+ auto *runtime = process_sp ? llvm::dyn_cast_or_null<GNUstepObjCRuntime>(
+ ObjCLanguageRuntime::Get(*process_sp))
+ : nullptr;
+ // A method list read off a class object is always that object's own
+ // instance methods, whether or not the object happens to be a metaclass.
+ // Which of the caller's two callbacks that corresponds to is the caller's
+ // business - see the metaclass hop below.
+ const auto &method_func = instance_method_func;
+ if (runtime && method_func) {
+ // The same selector legitimately appears more than once: categories
+ // prepend their lists, and a category may override a method.
+ llvm::DenseSet<ConstString> seen;
+ for (const RawMethod &method : ReadMethodList()) {
+ ConstString name = runtime->GetSelectorName(method.selector);
+ if (!name || !seen.insert(name).second)
+ continue;
+ if (method_func(name.GetCString(), method.types.c_str()))
+ break;
+ }
+ }
+
+ // libobjc2 stores a class's *class* methods on its metaclass, as that
+ // metaclass's instance methods, so they are collected by asking the
+ // metaclass for its instance methods and reporting them as ours.
+ if (!m_is_meta && class_method_func) {
+ if (std::unique_ptr<ObjCLanguageRuntime::ClassDescriptor> metaclass =
+ GetMetaclass())
+ metaclass->Describe(nullptr, /*instance_method_func=*/class_method_func,
+ nullptr, nullptr);
+ }
+ }
+
if (ivar_func) {
for (const RawIvar &ivar : ReadIvarList()) {
// libobjc2 stores a pointer to the offset so it can rewrite it in
@@ -200,6 +237,85 @@ bool GNUstepObjCClassDescriptor::Describe(
return true;
}
+std::vector<GNUstepObjCClassDescriptor::RawMethod>
+GNUstepObjCClassDescriptor::ReadMethodList() const {
+ std::vector<RawMethod> methods;
+ ProcessSP process_sp = m_process_wp.lock();
+ if (!process_sp || !m_valid)
+ return methods;
+
+ const ClassLayout layout = GetClassLayout(*process_sp);
+ const uint32_t ptr_size = layout.pointer_size;
+
+ Status error;
+ addr_t list_addr =
+ process_sp->ReadPointerFromMemory(m_isa + layout.methods_offset, error);
+ if (error.Fail())
+ return methods;
+
+ // struct objc_method_list { objc_method_list *next; int count; size_t size;
+ // objc_method methods[]; } (method.h)
+ const uint64_t count_offset = ptr_size;
+ const uint64_t size_offset =
+ llvm::alignTo(ptr_size + sizeof(uint32_t), ptr_size);
+ const uint64_t entries_offset = size_offset + ptr_size;
+ const uint64_t min_stride = 3 * ptr_size;
+
+ // A class's own methods are followed by one list per category. The chain
+ // and its contents both come from inferior memory, so bound the number of
+ // lists, the total number of methods, and revisiting: a `next` pointing
+ // back into the chain would otherwise multiply out to millions of entries
+ // even with each list individually bounded.
+ llvm::SmallPtrSet<const void *, 16> visited;
+ for (uint32_t list = 0; list < g_max_method_lists; ++list) {
+ if (list_addr == 0 || list_addr == LLDB_INVALID_ADDRESS ||
+ list_addr % ptr_size != 0)
+ break;
+ if (!visited.insert(reinterpret_cast<const void *>(list_addr)).second)
+ break;
+ if (methods.size() >= g_max_methods)
+ break;
+
+ const int64_t count = process_sp->ReadSignedIntegerFromMemory(
+ list_addr + count_offset, sizeof(uint32_t), 0, error);
+ if (error.Fail() || count < 0 || count > g_max_methods)
+ break;
+ const uint64_t stride = process_sp->ReadUnsignedIntegerFromMemory(
+ list_addr + size_offset, ptr_size, 0, error);
+ if (error.Fail() || stride < min_stride || stride > g_max_element_stride)
+ break;
+
+ for (int64_t i = 0; i < count && methods.size() < g_max_methods; ++i) {
+ // struct objc_method { IMP imp; SEL selector; const char *types; }
+ const addr_t entry = list_addr + entries_offset + i * stride;
+ const addr_t selector =
+ process_sp->ReadPointerFromMemory(entry + ptr_size, error);
+ if (error.Fail() || selector == 0 || selector == LLDB_INVALID_ADDRESS)
+ continue;
+ const addr_t types_ptr =
+ process_sp->ReadPointerFromMemory(entry + 2 * ptr_size, error);
+ if (error.Fail())
+ continue;
+
+ RawMethod method;
+ method.selector = selector;
+ if (types_ptr != 0 && types_ptr != LLDB_INVALID_ADDRESS) {
+ char buffer[g_max_type_encoding_length];
+ const size_t length = process_sp->ReadCStringFromMemory(
+ types_ptr, buffer, sizeof(buffer), error);
+ if (error.Success() && length < sizeof(buffer) - 1)
+ method.types.assign(buffer, length);
+ }
+ methods.push_back(std::move(method));
+ }
+
+ list_addr = process_sp->ReadPointerFromMemory(list_addr, error);
+ if (error.Fail())
+ break;
+ }
+ return methods;
+}
+
std::vector<GNUstepObjCClassDescriptor::RawIvar>
GNUstepObjCClassDescriptor::ReadIvarList() const {
std::vector<RawIvar> ivars;
diff --git a/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCClassDescriptor.h b/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCClassDescriptor.h
index 9b3b095e88d27..8741c07d677a6 100644
--- a/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCClassDescriptor.h
+++ b/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCClassDescriptor.h
@@ -97,24 +97,26 @@ class GNUstepObjCClassDescriptor : public ObjCLanguageRuntime::ClassDescriptor {
/// producer (associate.m).
bool IsKVO() override { return m_is_hidden; }
- /// Reports this class's superclass and ivars to \p superclass_func and
- /// \p ivar_func. Any callback may be null.
+ /// Reports this class's superclass, methods and ivars to the matching
+ /// callbacks. Any callback may be null, and a method or ivar callback
+ /// returning true stops the iteration.
///
- /// Methods are not reported yet: after __objc_load, a selector's name field
- /// holds a numeric dispatch index rather than a string (selector_table.cc),
- /// so recovering names needs either symbols for the method functions or a
- /// call into the runtime, and neither belongs in a descriptor that promises
- /// not to execute code. Returning true with no methods is well defined -
- /// the callbacks are optional - and lets the interface be completed from
- /// its ivars alone.
- bool Describe(std::function<void(ObjCLanguageRuntime::ObjCISA)> const
- &superclass_func,
- std::function<bool(const char *, const char *)> const
- &instance_method_func,
- std::function<bool(const char *, const char *)> const
- &class_method_func,
- std::function<bool(const char *, const char *, lldb::addr_t,
- uint64_t)> const &ivar_func) const override;
+ /// Class methods are collected from the metaclass, which is where libobjc2
+ /// keeps them, as its instance methods.
+ ///
+ /// A method whose name cannot be recovered is skipped rather than reported
+ /// under a placeholder: the name lives only in the symbol clang emitted for
+ /// the selector, because __objc_load overwrites the name field in memory
+ /// with a numeric dispatch index (selector_table.cc).
+ ///
+ /// Returns true if the class could be described at all.
+ bool Describe(
+ std::function<void(ObjCLanguageRuntime::ObjCISA)> const &superclass_func,
+ std::function<bool(const char *, const char *)> const
+ &instance_method_func,
+ std::function<bool(const char *, const char *)> const &class_method_func,
+ std::function<bool(const char *, const char *, lldb::addr_t,
+ uint64_t)> const &ivar_func) const override;
size_t GetNumIVars() override;
@@ -131,6 +133,18 @@ class GNUstepObjCClassDescriptor : public ObjCLanguageRuntime::ClassDescriptor {
uint32_t size = 0;
};
+ /// One entry of libobjc2's `objc_method_list`. The selector is kept as its
+ /// address: after __objc_load its name field holds a dispatch index rather
+ /// than a string, so the name has to come from the symbol emitted for it.
+ struct RawMethod {
+ lldb::addr_t selector = 0;
+ std::string types;
+ };
+
+ /// Reads the methods this class implements, walking the whole list chain
+ /// (categories are prepended to it). Pure memory reads.
+ std::vector<RawMethod> ReadMethodList() const;
+
/// Reads this class's own ivars, or an empty list if the class has none or
/// its metadata is not yet trustworthy. Pure memory reads.
///
diff --git a/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCDeclVendor.cpp b/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCDeclVendor.cpp
index 329fb9ea7c5bf..68c0f7739703a 100644
--- a/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCDeclVendor.cpp
+++ b/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCDeclVendor.cpp
@@ -21,6 +21,8 @@
#include "clang/AST/DeclObjC.h"
#include "clang/AST/ExternalASTSource.h"
+#include "llvm/ADT/StringExtras.h"
+
using namespace lldb;
using namespace lldb_private;
@@ -122,6 +124,131 @@ GNUstepObjCDeclVendor::GetDeclForISA(ObjCLanguageRuntime::ObjCISA isa) {
return new_iface_decl;
}
+namespace {
+/// Splits a runtime method type encoding into its component types.
+///
+/// The encoding interleaves types with the byte offsets of the arguments in
+/// the (long obsolete) argument frame - "v16 at 0:8" is a void return, then
+/// `self` at 0 and `_cmd` at 8. Only the types are wanted, so the digits are
+/// dropped; digits *inside* an aggregate are part of it and are kept, which
+/// is what the depth counter is for.
+///
+/// The resulting order is fixed: [0] return, [1] self, [2] _cmd, [3...]
+/// the declared parameters.
+class MethodTypeSplitter {
+public:
+ explicit MethodTypeSplitter(llvm::StringRef types) { Parse(types); }
+
+ bool IsValid() const { return m_valid && m_types.size() >= 3; }
+ llvm::StringRef GetReturnType() const { return m_types[0]; }
+ size_t GetNumArguments() const { return m_types.size() - 3; }
+ llvm::StringRef GetArgumentType(size_t idx) const { return m_types[idx + 3]; }
+
+private:
+ void Parse(llvm::StringRef types) {
+ std::string current;
+ unsigned depth = 0;
+ for (char c : types) {
+ const bool is_digit = llvm::isDigit(c);
+ if (depth == 0 && is_digit) {
+ // An offset: it ends the type that preceded it.
+ if (!current.empty()) {
+ m_types.push_back(current);
+ current.clear();
+ }
+ continue;
+ }
+ if (c == '{' || c == '(' || c == '[')
+ ++depth;
+ else if (c == '}' || c == ')' || c == ']')
+ if (depth > 0)
+ --depth;
+ current.push_back(c);
+ // A complete aggregate ends the type too, since no offset follows it
+ // until the next argument.
+ if (depth == 0 && (c == '}' || c == ')' || c == ']')) {
+ m_types.push_back(current);
+ current.clear();
+ }
+ }
+ if (!current.empty())
+ m_types.push_back(current);
+ m_valid = !m_types.empty();
+ }
+
+ std::vector<std::string> m_types;
+ bool m_valid = false;
+};
+} // namespace
+
+clang::ObjCMethodDecl *GNUstepObjCDeclVendor::BuildMethodDecl(
+ clang::ObjCInterfaceDecl *interface_decl, llvm::StringRef name,
+ llvm::StringRef types, bool is_instance_method) {
+ MethodTypeSplitter splitter(types);
+ if (!splitter.IsValid())
+ return nullptr;
+
+ ObjCLanguageRuntime::EncodingToTypeSP encoding_to_type_sp =
+ m_runtime.GetEncodingToType();
+ if (!encoding_to_type_sp)
+ return nullptr;
+
+ clang::ASTContext &ast_ctx = m_ast_ctx_sp->getASTContext();
+
+ // Method types are realized for expressions: unlike an ivar, a return type
+ // of @"NSString" is worth resolving, and recursing back into this vendor
+ // for it is safe because the method is not part of any interface yet.
+ CompilerType return_type = encoding_to_type_sp->RealizeType(
+ *m_ast_ctx_sp, splitter.GetReturnType().str().c_str(),
+ /*for_expression=*/true);
+ if (!return_type)
+ return nullptr;
+
+ // "initWithFoo:bar:" has as many pieces as colons; a zero-argument
+ // selector is a single identifier with none.
+ llvm::SmallVector<llvm::StringRef, 4> pieces;
+ name.split(pieces, ':');
+ const bool has_colons = name.contains(':');
+ if (has_colons && !pieces.empty() && pieces.back().empty())
+ pieces.pop_back();
+ if (pieces.empty())
+ return nullptr;
+
+ llvm::SmallVector<const clang::IdentifierInfo *, 4> selector_pieces;
+ for (llvm::StringRef piece : pieces)
+ selector_pieces.push_back(&ast_ctx.Idents.get(piece));
+
+ const unsigned num_selector_args = has_colons ? selector_pieces.size() : 0;
+ clang::Selector selector =
+ ast_ctx.Selectors.getSelector(num_selector_args, selector_pieces.data());
+
+ clang::ObjCMethodDecl *method_decl = clang::ObjCMethodDecl::Create(
+ ast_ctx, clang::SourceLocation(), clang::SourceLocation(), selector,
+ ClangUtil::GetQualType(return_type), /*ReturnTInfo=*/nullptr,
+ interface_decl, is_instance_method, /*isVariadic=*/false,
+ /*isPropertyAccessor=*/false, /*isSynthesizedAccessorStub=*/false,
+ /*isImplicitlyDeclared=*/true, /*isDefined=*/false,
+ clang::ObjCImplementationControl::None,
+ /*HasRelatedResultType=*/false);
+ if (!method_decl)
+ return nullptr;
+
+ llvm::SmallVector<clang::ParmVarDecl *, 4> params;
+ for (size_t i = 0; i < splitter.GetNumArguments(); ++i) {
+ CompilerType arg_type = encoding_to_type_sp->RealizeType(
+ *m_ast_ctx_sp, splitter.GetArgumentType(i).str().c_str(),
+ /*for_expression=*/true);
+ if (!arg_type)
+ return nullptr;
+ params.push_back(clang::ParmVarDecl::Create(
+ ast_ctx, method_decl, clang::SourceLocation(), clang::SourceLocation(),
+ /*Id=*/nullptr, ClangUtil::GetQualType(arg_type), /*TInfo=*/nullptr,
+ clang::SC_None, /*DefArg=*/nullptr));
+ }
+ method_decl->setMethodParams(ast_ctx, params, {});
+ return method_decl;
+}
+
bool GNUstepObjCDeclVendor::FinishDecl(
clang::ObjCInterfaceDecl *interface_decl) {
if (!interface_decl)
@@ -140,6 +267,15 @@ bool GNUstepObjCDeclVendor::FinishDecl(
if (!interface_decl->hasExternalVisibleStorage())
return true;
+ // Ask the runtime before publishing anything. Starting the definition and
+ // then failing would leave a members-less interface that the guard above
+ // makes permanent, so a class that was momentarily unreadable would stay
+ // empty for the rest of the session.
+ ObjCLanguageRuntime::ClassDescriptorSP descriptor =
+ m_runtime.GetClassDescriptorFromISA(isa);
+ if (!descriptor)
+ return false;
+
// The definition has to be started, and the external-storage bits cleared,
// *before* the callbacks run - otherwise a class that reaches itself
// through its superclass chain recurses without bound.
@@ -147,11 +283,6 @@ bool GNUstepObjCDeclVendor::FinishDecl(
interface_decl->setHasExternalVisibleStorage(false);
interface_decl->setHasExternalLexicalStorage(false);
- ObjCLanguageRuntime::ClassDescriptorSP descriptor =
- m_runtime.GetClassDescriptorFromISA(isa);
- if (!descriptor)
- return false;
-
ObjCLanguageRuntime::EncodingToTypeSP encoding_to_type_sp =
m_runtime.GetEncodingToType();
Log *log = GetLog(LLDBLog::Types);
@@ -159,11 +290,28 @@ bool GNUstepObjCDeclVendor::FinishDecl(
auto superclass_func = [this,
interface_decl](ObjCLanguageRuntime::ObjCISA super) {
clang::ObjCInterfaceDecl *superclass_decl = GetDeclForISA(super);
- if (!superclass_decl)
+ if (!superclass_decl || superclass_decl == interface_decl)
return;
// A superclass has to be complete before it can be attached, so this is
// eager where everything else here is lazy.
FinishDecl(superclass_decl);
+
+ // The recursion latch above stops FinishDecl re-entering, but it does not
+ // stop the *edge* being written: with A's superclass B and B's superclass
+ // A, both setSuperClass calls would still run and leave a cycle in the
+ // AST that clang then walks without bound. Reject any edge that is
+ // already reachable upwards from the proposed superclass.
+ for (const clang::ObjCInterfaceDecl *ancestor = superclass_decl; ancestor;
+ ancestor = ancestor->getSuperClass()) {
+ if (ancestor == interface_decl) {
+ LLDB_LOG(GetLog(LLDBLog::Types),
+ "GNUstep class {0} would close a superclass cycle; leaving it "
+ "without a superclass",
+ interface_decl->getName());
+ return;
+ }
+ }
+
clang::ASTContext &ast_ctx = m_ast_ctx_sp->getASTContext();
interface_decl->setSuperClass(ast_ctx.getTrivialTypeSourceInfo(
ast_ctx.getObjCInterfaceType(superclass_decl)));
@@ -208,8 +356,24 @@ bool GNUstepObjCDeclVendor::FinishDecl(
return false;
};
- if (!descriptor->Describe(superclass_func, /*instance_method_func=*/nullptr,
- /*class_method_func=*/nullptr, ivar_func)) {
+ auto make_method_func = [this, interface_decl, log](bool is_instance_method) {
+ return [this, interface_decl, log,
+ is_instance_method](const char *name, const char *types) -> bool {
+ if (!name || !types)
+ return false;
+ if (clang::ObjCMethodDecl *method_decl =
+ BuildMethodDecl(interface_decl, name, types, is_instance_method))
+ interface_decl->addDecl(method_decl);
+ else
+ LLDB_LOG(log, "GNUstep method {0} has an unrealizable signature {1}",
+ name, types);
+ return false;
+ };
+ };
+
+ if (!descriptor->Describe(
+ superclass_func, make_method_func(/*is_instance_method=*/true),
+ make_method_func(/*is_instance_method=*/false), ivar_func)) {
LLDB_LOG(log, "GNUstep runtime could not describe class {0}",
descriptor->GetClassName());
return false;
diff --git a/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCDeclVendor.h b/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCDeclVendor.h
index 13ca60e03e788..71c9976a66029 100644
--- a/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCDeclVendor.h
+++ b/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCDeclVendor.h
@@ -52,6 +52,13 @@ class GNUstepObjCDeclVendor : public DeclVendor {
bool FinishDecl(clang::ObjCInterfaceDecl *interface_decl);
private:
+ /// Builds an ObjCMethodDecl from a runtime method name and type
+ /// encoding, or nullptr if the encoding cannot be realized.
+ clang::ObjCMethodDecl *
+ BuildMethodDecl(clang::ObjCInterfaceDecl *interface_decl,
+ llvm::StringRef name, llvm::StringRef types,
+ bool is_instance_method);
+
/// Returns a forward declaration for \p isa, creating it if needed.
clang::ObjCInterfaceDecl *GetDeclForISA(ObjCLanguageRuntime::ObjCISA isa);
diff --git a/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCRuntime.cpp b/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCRuntime.cpp
index 8f0eff094b8fe..831ba70209cec 100644
--- a/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCRuntime.cpp
+++ b/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCRuntime.cpp
@@ -1033,6 +1033,78 @@ GNUstepObjCRuntime::FindDispatchEntryPoint(lldb::addr_t pc) {
return std::nullopt;
}
+ConstString GNUstepObjCRuntime::GetSelectorName(lldb::addr_t sel_addr) {
+ if (!m_process || sel_addr == 0 || sel_addr == LLDB_INVALID_ADDRESS)
+ return ConstString();
+
+ // clang emits every selector as a global named
+ // ".objc_selector_<name>_<mangled types>" (CGObjCGNU.cpp). That symbol is
+ // the only place the name survives: __objc_load overwrites the selector's
+ // name field with a numeric dispatch index (selector_table.cc), so it
+ // cannot be read back from memory.
+ Address resolved;
+ if (!GetTargetRef().ResolveLoadAddress(sel_addr, resolved))
+ return ConstString();
+ ModuleSP module_sp = resolved.GetModule();
+ if (!module_sp)
+ return ConstString();
+ Symtab *symtab = module_sp->GetSymtab();
+ if (!symtab)
+ return ConstString();
+
+ static constexpr llvm::StringLiteral g_selector_prefix = ".objc_selector_";
+
+ // The first selector in a section shares its address with the section's
+ // start sentinel, so take every symbol at this address rather than
+ // whichever one an exact lookup happens to return.
+ llvm::StringRef symbol_name;
+ symtab->ForEachSymbolContainingFileAddress(
+ resolved.GetFileAddress(), [&](Symbol *symbol) -> bool {
+ llvm::StringRef name = symbol->GetName().GetStringRef();
+ if (!name.starts_with(g_selector_prefix))
+ return true; // keep looking
+ symbol_name = name;
+ return false;
+ });
+ if (symbol_name.empty())
+ return ConstString();
+
+ llvm::StringRef name = symbol_name.drop_front(g_selector_prefix.size());
+
+ // A selector name may contain both '_' and ':', so the boundary cannot be
+ // found by scanning. Subtract the suffix instead: the types half of the
+ // symbol is exactly the selector's own `types` string, mangled. That
+ // string is still readable - only the name field was overwritten.
+ const uint32_t ptr_size = m_process->GetAddressByteSize();
+ Status error;
+ const addr_t types_addr =
+ m_process->ReadPointerFromMemory(sel_addr + ptr_size, error);
+ std::string suffix("_");
+ if (error.Success() && types_addr != 0 &&
+ types_addr != LLDB_INVALID_ADDRESS) {
+ char buffer[g_max_type_encoding_length];
+ const size_t length = m_process->ReadCStringFromMemory(
+ types_addr, buffer, sizeof(buffer), error);
+ if (error.Success() && length < sizeof(buffer) - 1) {
+ // GetSymbolNameForTypeEncoding: '@' is replaced on ELF because it is
+ // reserved for symbol versioning, '=' on Windows because lld rejects
+ // it in an exported name. MinGW is isOSWindows() and not ELF, so the
+ // predicates are asked separately rather than derived from each other.
+ const llvm::Triple &triple = GetTargetRef().GetArchitecture().GetTriple();
+ std::string mangled(buffer, length);
+ if (triple.isOSBinFormatELF())
+ llvm::replace(mangled, '@', '\1');
+ if (triple.isOSWindows())
+ llvm::replace(mangled, '=', '\2');
+ suffix += mangled;
+ }
+ }
+
+ if (!name.consume_back(suffix))
+ return ConstString();
+ return ConstString(name);
+}
+
FunctionCaller *GNUstepObjCRuntime::GetMsgLookupFunctionCaller(Thread &thread) {
// Build (once) a utility function that resolves a method implementation by
// calling libobjc2's objc_msg_lookup, and a FunctionCaller to invoke it.
@@ -1251,8 +1323,7 @@ DeclVendor *GNUstepObjCRuntime::GetDeclVendor() {
return m_decl_vendor_up.get();
}
-ObjCLanguageRuntime::EncodingToTypeSP
-GNUstepObjCRuntime::GetEncodingToType() {
+ObjCLanguageRuntime::EncodingToTypeSP GNUstepObjCRuntime::GetEncodingToType() {
if (!m_encoding_to_type_sp)
m_encoding_to_type_sp = std::make_shared<GNUstepObjCTypeEncodingParser>(
GetTargetRef().GetArchitecture().GetTriple(), this);
diff --git a/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCRuntime.h b/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCRuntime.h
index 1817b41f9ee9d..d44559bb01ab3 100644
--- a/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCRuntime.h
+++ b/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCRuntime.h
@@ -178,6 +178,15 @@ class GNUstepObjCRuntime : public lldb_private::ObjCLanguageRuntime {
std::optional<uint64_t>
GetTypeBitSize(const CompilerType &compiler_type) override;
+ /// The name of the selector at \p sel_addr, recovered from the symbol
+ /// clang emits for it. Empty when there is no such symbol.
+ ///
+ /// This cannot be read from memory: __objc_load overwrites a selector's
+ /// name field with a numeric dispatch index (selector_table.cc), so the
+ /// string is gone by the time a debugger sees it. No code is run in the
+ /// inferior to find it.
+ ConstString GetSelectorName(lldb::addr_t sel_addr);
+
/// Lazily-built FunctionCaller for a utility function that resolves a
/// method implementation via libobjc2's
/// `IMP objc_msg_lookup(id receiver, SEL selector)`, used by the
diff --git a/lldb/test/Shell/Expr/Inputs/objc-gnustep-hidden-class.m b/lldb/test/Shell/Expr/Inputs/objc-gnustep-hidden-class.m
index 3ef3731afc5da..fc822fbda9354 100644
--- a/lldb/test/Shell/Expr/Inputs/objc-gnustep-hidden-class.m
+++ b/lldb/test/Shell/Expr/Inputs/objc-gnustep-hidden-class.m
@@ -30,8 +30,20 @@ @interface Hidden : NSObject {
char _char;
void *_ptr;
}
+- (int)doubled:(int)value;
+- (int)plainInt;
++ (int)classAnswer;
@end
@implementation Hidden
+- (int)doubled:(int)value {
+ return value * 2;
+}
+- (int)plainInt {
+ return _int;
+}
++ (int)classAnswer {
+ return 7;
+}
@end
id MakeHidden(void) {
diff --git a/lldb/test/Shell/Expr/objc-gnustep-decl-vendor.m b/lldb/test/Shell/Expr/objc-gnustep-decl-vendor.m
index d436dc8df40c8..993156e747da0 100644
--- a/lldb/test/Shell/Expr/objc-gnustep-decl-vendor.m
+++ b/lldb/test/Shell/Expr/objc-gnustep-decl-vendor.m
@@ -34,6 +34,9 @@ int main() {
// RUN: -o "expr -- ((Hidden *)hidden)->_int" \
// RUN: -o "expr -- ((Hidden *)hidden)->_float" \
// RUN: -o "frame variable -d run-target *hidden" \
+// RUN: -o "expr -- [(Hidden *)hidden plainInt]" \
+// RUN: -o "expr -- [(Hidden *)hidden doubled:21]" \
+// RUN: -o "expr -- [Hidden classAnswer]" \
// RUN: -- %t | FileCheck %s --check-prefix=VENDOR
//
// VENDOR: (lldb) type lookup Hidden
@@ -55,3 +58,19 @@ int main() {
// VENDOR-DAG: _float = 2
// VENDOR-DAG: _char = '{{.*}}3'
// VENDOR-DAG: _ptr = 0x{{0*}}4
+//
+// Methods are synthesized too, so a message send type-checks and runs. The
+// selector's name comes from the symbol clang emits for it: after
+// __objc_load the name field in memory holds a dispatch index instead.
+//
+// VENDOR: (lldb) expr -- [(Hidden *)hidden plainInt]
+// VENDOR: (int) $2 = 1
+//
+// VENDOR: (lldb) expr -- [(Hidden *)hidden doubled:21]
+// VENDOR: (int) $3 = 42
+//
+// Class methods come from the metaclass, where libobjc2 keeps them as its
+// instance methods.
+//
+// VENDOR: (lldb) expr -- [Hidden classAnswer]
+// VENDOR: (int) $4 = 7
>From ce2cef39c12d79404f7e9ffd428a75b0c3a925c0 Mon Sep 17 00:00:00 2001
From: Rob <robk at robk.dev>
Date: Tue, 18 Aug 2026 22:50:47 +0100
Subject: [PATCH 38/49] [lldb][test] Apply the GNUstep build flags only when a
test asks for them
build.py derived the libobjc2 include and library paths from
--objc-gnustep-dir alone, but the lit configuration passes that flag to every
%build invocation whenever a libobjc2 is configured. So the Objective-C
runtime's flags - and, on Windows, -fuse-ld=lld-link -Wl,/debug:dwarf - were
applied to tests that have nothing to do with Objective-C, and the --sysroot
branches were unreachable on any GNUstep-configured build.
Nothing was visibly broken because --compiler=any selects clang-cl on
Windows, so only the tests that explicitly ask for clang reached this code.
That containment disappears as soon as the toolchain preference changes for
any other target.
Gate on --objc-gnustep instead, which every test that wants the runtime
already passes.
Assisted-by: Claude Opus 5
---
lldb/test/Shell/helper/build.py | 12 ++++++------
1 file changed, 6 insertions(+), 6 deletions(-)
diff --git a/lldb/test/Shell/helper/build.py b/lldb/test/Shell/helper/build.py
index b21bf6e3bf45c..1303194c0e669 100755
--- a/lldb/test/Shell/helper/build.py
+++ b/lldb/test/Shell/helper/build.py
@@ -323,15 +323,15 @@ def __init__(self, toolchain_type, args, obj_ext):
assert (
not args.objc_gnustep or args.objc_gnustep_dir
), "--objc-gnustep specified without path to libobjc2"
+ # lit passes --objc-gnustep-dir to every %build, so gate on
+ # --objc-gnustep: the path alone would put ObjC flags (and shadow
+ # --sysroot) on every test.
+ use_gnustep = args.objc_gnustep and args.objc_gnustep_dir
self.objc_gnustep_inc = (
- os.path.join(args.objc_gnustep_dir, "include")
- if args.objc_gnustep_dir
- else None
+ os.path.join(args.objc_gnustep_dir, "include") if use_gnustep else None
)
self.objc_gnustep_lib = (
- os.path.join(args.objc_gnustep_dir, "lib")
- if args.objc_gnustep_dir
- else None
+ os.path.join(args.objc_gnustep_dir, "lib") if use_gnustep else None
)
self.sysroot = args.sysroot
>From a7fdc21fedaf1cd2f8ef4dcd1d45e556ea31e722 Mon Sep 17 00:00:00 2001
From: Rob <robk at robk.dev>
Date: Tue, 18 Aug 2026 22:50:49 +0100
Subject: [PATCH 39/49] [lldb][cmake] Accept a MinGW layout when looking for
libobjc2
The Windows branch looked only for lib/objc.dll, which is what an MSVC build
installs. A MinGW build follows the GNU naming convention and produces
libobjc.dll, placed wherever CMAKE_INSTALL_BINDIR points. Neither name was
found, so configuring against a MinGW libobjc2 failed outright rather than
falling back to anything.
Try the names and directories both builds use, one pattern at a time -
file(GLOB) sorts the union of its patterns, so passing them together would
not preserve the intended preference - and report the one found. The Shell
suite puts bin/ on PATH beside lib/ for the same reason.
Assisted-by: Claude Opus 5
---
lldb/cmake/modules/FindGNUstepObjC.cmake | 26 ++++++++++++++++++++----
lldb/test/Shell/lit.cfg.py | 4 +++-
2 files changed, 25 insertions(+), 5 deletions(-)
diff --git a/lldb/cmake/modules/FindGNUstepObjC.cmake b/lldb/cmake/modules/FindGNUstepObjC.cmake
index e53a89e50a481..cf4575a4961c9 100644
--- a/lldb/cmake/modules/FindGNUstepObjC.cmake
+++ b/lldb/cmake/modules/FindGNUstepObjC.cmake
@@ -22,16 +22,34 @@ if (UNIX)
set(GNUstepObjC_FOUND TRUE)
endif()
elseif (WIN32)
- set(gnustep_lib lib/objc.dll)
+ # MSVC libobjc2 installs lib/objc.dll; MinGW uses GNU naming (libobjc.dll,
+ # possibly versioned; MSYS2 ships libobjc-4.6.dll) and usually puts it in
+ # bin/.
set(gnustep_header include/objc/runtime.h)
if (GNUstepObjC_DIR)
set(gnustep_install_dir ${GNUstepObjC_DIR})
else()
set(gnustep_install_dir "C:/Program Files (x86)/libobjc")
endif()
- if (EXISTS "${gnustep_install_dir}/${gnustep_lib}" AND
- EXISTS "${gnustep_install_dir}/${gnustep_header}")
- set(GNUstepObjC_FOUND TRUE)
+ if (EXISTS "${gnustep_install_dir}/${gnustep_header}")
+ # Prefer lib/, where the DLL sits beside its import library. One pattern
+ # at a time: file(GLOB) sorts the union of its patterns, so passing them
+ # together would not preserve this order.
+ foreach (gnustep_lib_dir lib bin)
+ foreach (gnustep_lib_name objc.dll libobjc.dll libobjc-[0-9]*.dll)
+ file(GLOB gnustep_lib_matches
+ RELATIVE "${gnustep_install_dir}"
+ "${gnustep_install_dir}/${gnustep_lib_dir}/${gnustep_lib_name}")
+ if (gnustep_lib_matches)
+ list(GET gnustep_lib_matches 0 gnustep_lib)
+ set(GNUstepObjC_FOUND TRUE)
+ break()
+ endif()
+ endforeach()
+ if (GNUstepObjC_FOUND)
+ break()
+ endif()
+ endforeach()
endif()
endif()
diff --git a/lldb/test/Shell/lit.cfg.py b/lldb/test/Shell/lit.cfg.py
index ef18de401cc7d..0296a48da97fc 100644
--- a/lldb/test/Shell/lit.cfg.py
+++ b/lldb/test/Shell/lit.cfg.py
@@ -201,10 +201,12 @@ def tool(name):
if config.objc_gnustep_dir:
config.available_features.add("objc-gnustep")
if platform.system() == "Windows":
- # objc.dll must be in PATH since Windows has no rpath
+ # No rpath on Windows. MSVC libobjc2 installs the DLL in lib/, MinGW
+ # in bin/.
config.environment["PATH"] = os.path.pathsep.join(
(
os.path.join(config.objc_gnustep_dir, "lib"),
+ os.path.join(config.objc_gnustep_dir, "bin"),
config.environment.get("PATH", ""),
)
)
>From 441e33b2b1d62cd60c6d28740dee56aa2ac31647 Mon Sep 17 00:00:00 2001
From: Rob <robk at robk.dev>
Date: Tue, 18 Aug 2026 22:50:50 +0100
Subject: [PATCH 40/49] [lldb][test] Let a test declare the target and ABI of
its inferior
The Shell suite could only build inferiors for whatever the host compiler
defaults to. The API suite has had LLDB_TEST_TRIPLE for this all along, so
reuse it rather than invent a second knob - its description, "the target
triple used to build test inferiors", already describes both suites. Move
its definition up out of the API subdirectory, since a cache variable set
there only becomes visible to the Shell suite by ordering.
build.py grows --target, passed when compiling and when linking: the target
decides which linker the driver invokes and which runtime libraries it links
against, so passing it to only one of the two produces an object for one
environment and an image for another. It reaching MsvcBuilder is an error
rather than silently ignored, since that builder has no use for it.
The decisions that were being made from sys.platform are really decisions
about the target, and MinGW is where the two can disagree. Force DWARF and
link the MSVC debug CRT only for an MSVC target; MinGW emits DWARF anyway
and links a different CRT. Ask for lld when cross-targeting MinGW, because
the driver otherwise looks for its linker in the target sysroot, which need
not hold anything the host can execute. clang-cl is no longer a candidate
for --compiler=any when the target is not MSVC, since it cannot target one.
--sysroot is no longer exclusive with the Objective-C flags: it was
unreachable whenever a libobjc2 was configured, and cross-compiling needs
the target's headers as well as the runtime's.
Two things are asked for by name rather than claimed of the whole suite,
because they are true of eight tests and not of the ~1400 others: the target
to build an inferior for, and the ABI to read it with. Nothing in a PE
records which Windows ABI it was built for, so LLDB assumes the one it was
built for itself, giving a MinGW inferior Microsoft C++ record layout and a
64-bit long double. Claiming either suite-wide is not merely untidy - it
re-targets tests that never asked, and declares every PE in every lldb
process MinGW.
%inferior_abi expands to -O rather than -o: the setting only takes effect
before a target is created, and is accepted silently afterwards. Underscores
in the substitution names are deliberate too - ToolSubst wraps a key in a
word boundary, so a hyphenated name would be swallowed by a shorter one.
Finally, put the sysroot's bin on PATH for a cross-built MinGW inferior. It
needs that toolchain's libstdc++ and libgcc, and Windows has no rpath to
record where they live, so the loader would otherwise pick up whichever
MinGW distribution happens to be on PATH - typically one built against a
different C runtime.
Assisted-by: Claude Opus 5
---
lldb/test/API/CMakeLists.txt | 11 ---
lldb/test/CMakeLists.txt | 14 +++
.../Shell/Expr/objc-gnustep-class-objects.m | 4 +-
.../Shell/Expr/objc-gnustep-decl-vendor.m | 10 +--
.../Shell/Expr/objc-gnustep-dynamic-types.m | 4 +-
.../test/Shell/Expr/objc-gnustep-exceptions.m | 8 +-
lldb/test/Shell/Expr/objc-gnustep-expr.m | 4 +-
lldb/test/Shell/Expr/objc-gnustep-print.m | 10 +--
lldb/test/Shell/Expr/objc-gnustep-stepping.m | 6 +-
.../Shell/Expr/objc-gnustep-tagged-pointers.m | 4 +-
lldb/test/Shell/helper/build.py | 88 ++++++++++++++-----
lldb/test/Shell/lit.cfg.py | 46 ++++++++++
lldb/test/Shell/lit.site.cfg.py.in | 1 +
13 files changed, 154 insertions(+), 56 deletions(-)
diff --git a/lldb/test/API/CMakeLists.txt b/lldb/test/API/CMakeLists.txt
index f00e332497f95..8518a3b0cf4e7 100644
--- a/lldb/test/API/CMakeLists.txt
+++ b/lldb/test/API/CMakeLists.txt
@@ -21,16 +21,6 @@ function(add_python_test_target name test_script args comment)
add_dependencies(${name} lldb-test-depends)
endfunction()
-if (DEFINED LLVM_TARGET_TRIPLE)
- set(default_lldb_test_triple ${LLVM_TARGET_TRIPLE})
-else()
- set(default_lldb_test_triple ${LLVM_HOST_TRIPLE})
-endif()
-
-set(LLDB_TEST_TRIPLE
- ${default_lldb_test_triple}
- CACHE STRING "The target triple used to build test inferiors.")
-
# Users can override LLDB_TEST_USER_ARGS to specify arbitrary arguments to pass to the script
set(LLDB_TEST_USER_ARGS
""
@@ -93,7 +83,6 @@ set(LLDB_TEST_COMPILER "${LLDB_DEFAULT_TEST_COMPILER}" CACHE PATH "C Compiler to
set(LLDB_TEST_DSYMUTIL "${LLDB_DEFAULT_TEST_DSYMUTIL}" CACHE PATH "dsymutil used for generating dSYM bundles")
set(LLDB_TEST_MAKE "${LLDB_DEFAULT_TEST_MAKE}" CACHE PATH "make tool used for building test executables")
set(LLDB_TEST_RESOURCE_DIR "" CACHE PATH "Clang resource directory for cross-compiling test inferiors")
-set(LLDB_TEST_SYSROOT "" CACHE PATH "Sysroot for cross-compiling test inferiors")
if ("${LLDB_TEST_COMPILER}" STREQUAL "")
message(FATAL_ERROR "LLDB test compiler not specified. Tests will not run.")
diff --git a/lldb/test/CMakeLists.txt b/lldb/test/CMakeLists.txt
index b2ed4d5b9c36a..f40ec6a9302a0 100644
--- a/lldb/test/CMakeLists.txt
+++ b/lldb/test/CMakeLists.txt
@@ -59,6 +59,20 @@ set(LLDB_TEST_MODULE_CACHE_CLANG "${LLDB_TEST_BUILD_DIRECTORY}/module-cache-clan
file(MAKE_DIRECTORY ${LLDB_TEST_MODULE_CACHE_LLDB})
file(MAKE_DIRECTORY ${LLDB_TEST_MODULE_CACHE_CLANG})
+# Set before either add_subdirectory(): both suites build inferiors, and a
+# cache variable set in one only leaks out after that subdirectory has run.
+if (DEFINED LLVM_TARGET_TRIPLE)
+ set(default_lldb_test_triple ${LLVM_TARGET_TRIPLE})
+else()
+ set(default_lldb_test_triple ${LLVM_HOST_TRIPLE})
+endif()
+
+set(LLDB_TEST_TRIPLE
+ ${default_lldb_test_triple}
+ CACHE STRING "The target triple used to build test inferiors.")
+
+set(LLDB_TEST_SYSROOT "" CACHE PATH "Sysroot for cross-compiling test inferiors")
+
# Windows and Linux have no built-in ObjC runtime. Turn this on in order to run tests with GNUstep.
option(LLDB_TEST_OBJC_GNUSTEP "Enable ObjC tests with GNUstep libobjc2 on non-Apple platforms" Off)
set(LLDB_TEST_OBJC_GNUSTEP_DIR "" CACHE PATH "Custom path to the GNUstep shared library")
diff --git a/lldb/test/Shell/Expr/objc-gnustep-class-objects.m b/lldb/test/Shell/Expr/objc-gnustep-class-objects.m
index 32d6152b3b04b..469d37fd7d4b6 100644
--- a/lldb/test/Shell/Expr/objc-gnustep-class-objects.m
+++ b/lldb/test/Shell/Expr/objc-gnustep-class-objects.m
@@ -1,6 +1,6 @@
// REQUIRES: objc-gnustep
//
-// RUN: %build %s --compiler=clang --objc-gnustep --output=%t
+// RUN: %build %inferior_target %s --compiler=clang --objc-gnustep --output=%t
#import "objc/runtime.h"
@@ -42,7 +42,7 @@ @interface Derived : Base
@implementation Derived
@end
-// RUN: %lldb -b -o "b objc-gnustep-class-objects.m:53" -o "run" \
+// RUN: %lldb %inferior_abi -b -o "b objc-gnustep-class-objects.m:53" -o "run" \
// RUN: -o "frame variable -d run-target -T object" \
// RUN: -o "frame variable -d run-target -T *object" \
// RUN: -o "frame variable -d run-target -T object->isa" \
diff --git a/lldb/test/Shell/Expr/objc-gnustep-decl-vendor.m b/lldb/test/Shell/Expr/objc-gnustep-decl-vendor.m
index 993156e747da0..0a46c67fe73cb 100644
--- a/lldb/test/Shell/Expr/objc-gnustep-decl-vendor.m
+++ b/lldb/test/Shell/Expr/objc-gnustep-decl-vendor.m
@@ -4,10 +4,10 @@
// half of this program is compiled with -g0, and nothing here declares
// anything about Hidden beyond its name.
//
-// RUN: %build %S/Inputs/objc-gnustep-hidden-class.m --compiler=clang --objc-gnustep \
+// RUN: %build %inferior_target %S/Inputs/objc-gnustep-hidden-class.m --compiler=clang --objc-gnustep \
// RUN: --no-debug-info --mode=compile --output=%t-hidden.o
-// RUN: %build %s --compiler=clang --objc-gnustep --mode=compile --output=%t-main.o
-// RUN: %build %t-hidden.o %t-main.o --compiler=clang --objc-gnustep --mode=link --output=%t
+// RUN: %build %inferior_target %s --compiler=clang --objc-gnustep --mode=compile --output=%t-main.o
+// RUN: %build %inferior_target %t-hidden.o %t-main.o --compiler=clang --objc-gnustep --mode=link --output=%t
@class Hidden;
id MakeHidden(void);
@@ -21,7 +21,7 @@ int main() {
// `image lookup -t` finds nothing and exits non-zero. If this ever starts
// matching, the rest of the test proves nothing.
//
-// RUN: not %lldb -b -o "image lookup -t Hidden" -- %t \
+// RUN: not %lldb %inferior_abi -b -o "image lookup -t Hidden" -- %t \
// RUN: | FileCheck %s --check-prefix=NODWARF
//
// NODWARF-NOT: name = "Hidden"
@@ -29,7 +29,7 @@ int main() {
// The interface is synthesized from the runtime instead, so the type can be
// named and its ivars read.
//
-// RUN: %lldb -b -o "b objc-gnustep-decl-vendor.m:17" -o "run" \
+// RUN: %lldb %inferior_abi -b -o "b objc-gnustep-decl-vendor.m:17" -o "run" \
// RUN: -o "type lookup Hidden" \
// RUN: -o "expr -- ((Hidden *)hidden)->_int" \
// RUN: -o "expr -- ((Hidden *)hidden)->_float" \
diff --git a/lldb/test/Shell/Expr/objc-gnustep-dynamic-types.m b/lldb/test/Shell/Expr/objc-gnustep-dynamic-types.m
index 3da7aeb01ad06..6359271ed9f0a 100644
--- a/lldb/test/Shell/Expr/objc-gnustep-dynamic-types.m
+++ b/lldb/test/Shell/Expr/objc-gnustep-dynamic-types.m
@@ -1,6 +1,6 @@
// REQUIRES: objc-gnustep
//
-// RUN: %build %s --compiler=clang --objc-gnustep --output=%t
+// RUN: %build %inferior_target %s --compiler=clang --objc-gnustep --output=%t
#import "objc/runtime.h"
@@ -37,7 +37,7 @@ @implementation Derived
// GNUstep runtime resolves the dynamic type by reading the class structure
// from the inferior's memory and attaching the matching type from debug info.
//
-// RUN: %lldb -b -o "b objc-gnustep-dynamic-types.m:46" -o "run" \
+// RUN: %lldb %inferior_abi -b -o "b objc-gnustep-dynamic-types.m:46" -o "run" \
// RUN: -o "frame variable -d run-target object" \
// RUN: -o "frame variable -d no-dynamic-values object" -- %t | FileCheck %s
//
diff --git a/lldb/test/Shell/Expr/objc-gnustep-exceptions.m b/lldb/test/Shell/Expr/objc-gnustep-exceptions.m
index f9966f61b7b96..f5010a4955e47 100644
--- a/lldb/test/Shell/Expr/objc-gnustep-exceptions.m
+++ b/lldb/test/Shell/Expr/objc-gnustep-exceptions.m
@@ -1,6 +1,6 @@
// REQUIRES: objc-gnustep
//
-// RUN: %build %s --compiler=clang --objc-gnustep --output=%t
+// RUN: %build %inferior_target %s --compiler=clang --objc-gnustep --output=%t
#import "objc/runtime.h"
@@ -68,7 +68,7 @@ int main() {
// An Objective-C exception breakpoint stops where the exception is raised,
// and the frame recognizer presents the thrown object as `exception`.
//
-// RUN: %lldb -b -o "breakpoint set -E objc" -o "run" -o "frame variable" \
+// RUN: %lldb %inferior_abi -b -o "breakpoint set -E objc" -o "run" -o "frame variable" \
// RUN: -o "thread exception" \
// RUN: -- %t | FileCheck %s --check-prefix=THROW
//
@@ -90,7 +90,7 @@ int main() {
// The object is reachable as a real local inside the handler, where `po`
// works on it normally.
//
-// RUN: %lldb -b -o "b objc-gnustep-exceptions.m:63" -o "run" -o "po caught" \
+// RUN: %lldb %inferior_abi -b -o "b objc-gnustep-exceptions.m:63" -o "run" -o "po caught" \
// RUN: -- %t | FileCheck %s --check-prefix=CAUGHT
//
// CAUGHT: (lldb) po caught
@@ -101,7 +101,7 @@ int main() {
// so catch breakpoints are real, but only where exceptions unwind through
// the Itanium ABI, which its MSVC build does not.
//
-// RUN: %if objc-gnustep-catch %{ %lldb -b -o "breakpoint set -E objc --on-catch true --on-throw false" -o "run" -o "bt" -- %t | FileCheck %s --check-prefix=CATCH %}
+// RUN: %if objc-gnustep-catch %{ %lldb %inferior_abi -b -o "breakpoint set -E objc --on-catch true --on-throw false" -o "run" -o "bt" -- %t | FileCheck %s --check-prefix=CATCH %}
//
// CATCH: stop reason = breakpoint
// objc_begin_catch, or __cxa_begin_catch where clang routes @catch through
diff --git a/lldb/test/Shell/Expr/objc-gnustep-expr.m b/lldb/test/Shell/Expr/objc-gnustep-expr.m
index 3605bebc9cbe2..bf774fe51b525 100644
--- a/lldb/test/Shell/Expr/objc-gnustep-expr.m
+++ b/lldb/test/Shell/Expr/objc-gnustep-expr.m
@@ -1,6 +1,6 @@
// REQUIRES: objc-gnustep
//
-// RUN: %build %s --compiler=clang --objc-gnustep --output=%t
+// RUN: %build %inferior_target %s --compiler=clang --objc-gnustep --output=%t
#import "objc/runtime.h"
@@ -36,7 +36,7 @@ - (int)addFortyTwoTo:(int)value {
// registered with the runtime (the GNUstep plugin's IR pass does this);
// without it the dispatch reaches the runtime with an unregistered selector.
//
-// RUN: %lldb -b -o "b objc-gnustep-expr.m:46" -o "run" \
+// RUN: %lldb %inferior_abi -b -o "b objc-gnustep-expr.m:46" -o "run" \
// RUN: -o "expr [c addFortyTwoTo:100]" \
// RUN: -o "expr (int)[[Calc new] addFortyTwoTo:1]" -- %t | FileCheck %s
//
diff --git a/lldb/test/Shell/Expr/objc-gnustep-print.m b/lldb/test/Shell/Expr/objc-gnustep-print.m
index 04b9f693f8d0e..7000147d42539 100644
--- a/lldb/test/Shell/Expr/objc-gnustep-print.m
+++ b/lldb/test/Shell/Expr/objc-gnustep-print.m
@@ -1,6 +1,6 @@
// REQUIRES: objc-gnustep
//
-// RUN: %build %s --compiler=clang --objc-gnustep --output=%t
+// RUN: %build %inferior_target %s --compiler=clang --objc-gnustep --output=%t
#import "objc/runtime.h"
@@ -51,7 +51,7 @@ - (void)set_ivars {
}
@end
-// RUN: %lldb -b -o "b objc-gnustep-print.m:42" -o "run" -o "p self" -o "p *self" -- %t | FileCheck %s --check-prefix=SELF
+// RUN: %lldb %inferior_abi -b -o "b objc-gnustep-print.m:42" -o "run" -o "p self" -o "p *self" -- %t | FileCheck %s --check-prefix=SELF
//
// SELF: (lldb) b objc-gnustep-print.m:42
// SELF: Breakpoint {{.*}} at objc-gnustep-print.m
@@ -77,7 +77,7 @@ - (void)set_ivars {
// SELF: _id_objc = nil
// SELF: }
-// RUN: %lldb -b -o "b objc-gnustep-print.m:105" -o "run" -o "p t->_int" -o "p t->_float" -o "p t->_char" \
+// RUN: %lldb %inferior_abi -b -o "b objc-gnustep-print.m:105" -o "run" -o "p t->_int" -o "p t->_float" -o "p t->_char" \
// RUN: -o "p t->_ptr_void" -o "p t->_ptr_nsobject" -o "p t->_id_objc" -- %t | FileCheck %s --check-prefix=IVARS_SET
//
// IVARS_SET: (lldb) p t->_int
@@ -136,7 +136,7 @@ - (id)description {
}
@end
-// RUN: %lldb -b -o "b objc-gnustep-print.m:105" -o "run" -o "po t" \
+// RUN: %lldb %inferior_abi -b -o "b objc-gnustep-print.m:105" -o "run" -o "po t" \
// RUN: -- %t | FileCheck %s --check-prefix=PO
//
// PO: (lldb) po t
@@ -146,7 +146,7 @@ - (id)description {
// Stepping at a message send goes through the objc_msgSend trampoline into
// the method implementation.
//
-// RUN: %lldb -b -o "b objc-gnustep-print.m:103" -o "run" -o "step" \
+// RUN: %lldb %inferior_abi -b -o "b objc-gnustep-print.m:103" -o "run" -o "step" \
// RUN: -- %t | FileCheck %s --check-prefix=STEP
//
// STEP: (lldb) step
diff --git a/lldb/test/Shell/Expr/objc-gnustep-stepping.m b/lldb/test/Shell/Expr/objc-gnustep-stepping.m
index 17cfde7b05bb3..a44698eb2de25 100644
--- a/lldb/test/Shell/Expr/objc-gnustep-stepping.m
+++ b/lldb/test/Shell/Expr/objc-gnustep-stepping.m
@@ -1,6 +1,6 @@
// REQUIRES: objc-gnustep
//
-// RUN: %build %s --compiler=clang --objc-gnustep --output=%t
+// RUN: %build %inferior_target %s --compiler=clang --objc-gnustep --output=%t
#import "objc/runtime.h"
@@ -35,7 +35,7 @@ - (int)twice:(int)value {
// Stepping at a message send has to run through the runtime's dispatch
// function and land in the method implementation.
//
-// RUN: %lldb -b -o "b objc-gnustep-stepping.m:53" -o "run" -o "step" \
+// RUN: %lldb %inferior_abi -b -o "b objc-gnustep-stepping.m:53" -o "run" -o "step" \
// RUN: -- %t | FileCheck %s --check-prefix=STEP_IN
//
// A message to nil dispatches nowhere, so the step must not try to run to an
@@ -45,7 +45,7 @@ - (int)twice:(int)value {
// objc_msgSend.S; asserting on the frame's source location is what catches
// that, since the function name alone looks plausible either way.
//
-// RUN: %lldb -b -o "b objc-gnustep-stepping.m:55" -o "run" -o "step" \
+// RUN: %lldb %inferior_abi -b -o "b objc-gnustep-stepping.m:55" -o "run" -o "step" \
// RUN: -o "frame info" -- %t | FileCheck %s --check-prefix=STEP_OVER_NIL
//
int main() {
diff --git a/lldb/test/Shell/Expr/objc-gnustep-tagged-pointers.m b/lldb/test/Shell/Expr/objc-gnustep-tagged-pointers.m
index 55744570a2dcd..ce099dc37c875 100644
--- a/lldb/test/Shell/Expr/objc-gnustep-tagged-pointers.m
+++ b/lldb/test/Shell/Expr/objc-gnustep-tagged-pointers.m
@@ -1,6 +1,6 @@
// REQUIRES: objc-gnustep
//
-// RUN: %build %s --compiler=clang --objc-gnustep --output=%t
+// RUN: %build %inferior_target %s --compiler=clang --objc-gnustep --output=%t
#import "objc/runtime.h"
@@ -36,7 +36,7 @@ @interface Ordinary : NSObject
@implementation Ordinary
@end
-// RUN: %lldb -b -o "b objc-gnustep-tagged-pointers.m:49" -o "run" \
+// RUN: %lldb %inferior_abi -b -o "b objc-gnustep-tagged-pointers.m:49" -o "run" \
// RUN: -o "frame variable -d run-target tagged" \
// RUN: -o "frame variable -d run-target ordinary" -- %t | FileCheck %s
//
diff --git a/lldb/test/Shell/helper/build.py b/lldb/test/Shell/helper/build.py
index 1303194c0e669..0028c3ab6f6b3 100755
--- a/lldb/test/Shell/helper/build.py
+++ b/lldb/test/Shell/helper/build.py
@@ -177,6 +177,14 @@
help="Specify the C/C++ standard.",
)
+parser.add_argument(
+ "--target",
+ metavar="target",
+ dest="target",
+ required=False,
+ help="Specify the target triple to build for. Defaults to the host.",
+)
+
args = parser.parse_args(args=sys.argv[1:])
@@ -250,15 +258,36 @@ def find_executable(binary_name, search_paths):
return binary_path
-def find_toolchain(compiler, tools_dir):
+def targets_windows(target):
+ """Whether target (None means the host) runs on Windows."""
+ if target is None:
+ return sys.platform == "win32"
+ return any(name in target for name in ("windows", "mingw", "cygwin"))
+
+
+def targets_msvc(target):
+ """Whether target uses the MSVC environment: link.exe syntax, PDBs."""
+ if not targets_windows(target):
+ return False
+ if target is None:
+ return True
+ return not any(env in target for env in ("gnu", "mingw", "cygnus", "cygwin"))
+
+
+def find_toolchain(compiler, tools_dir, target=None):
if compiler == "any":
priorities = []
- if sys.platform == "win32":
+ if targets_msvc(target):
priorities = ["clang-cl", "msvc", "clang", "gcc"]
+ elif target:
+ # clang-cl cannot target a non-MSVC environment.
+ priorities = ["clang", "gcc"]
else:
priorities = ["clang", "gcc", "clang-cl"]
for toolchain in priorities:
- (type, c_compiler, cxx_compiler) = find_toolchain(toolchain, tools_dir)
+ (type, c_compiler, cxx_compiler) = find_toolchain(
+ toolchain, tools_dir, target
+ )
if type and c_compiler and cxx_compiler:
return (type, c_compiler, cxx_compiler)
# Could not find a toolchain.
@@ -334,6 +363,7 @@ def __init__(self, toolchain_type, args, obj_ext):
os.path.join(args.objc_gnustep_dir, "lib") if use_gnustep else None
)
self.sysroot = args.sysroot
+ self.target = args.target
def _exe_file_name(self):
assert self.mode != "compile"
@@ -383,6 +413,8 @@ def compiler_for_file(self, filename):
class MsvcBuilder(Builder):
def __init__(self, toolchain_type, args):
Builder.__init__(self, toolchain_type, args, ".obj")
+ if self.target:
+ raise ValueError("--target is not supported with " + toolchain_type)
if platform.uname().machine.lower() == "arm64":
self.msvc_arch_str = "arm" if self.arch == "32" else "arm64"
@@ -786,20 +818,26 @@ def _get_compilation_command(self, source, obj):
args.append("-static")
args.append("-c")
+ if self.target:
+ args.append("--target=" + self.target)
+
if sys.platform == "darwin":
args.extend(["-isysroot", self.apple_sdk])
- elif self.objc_gnustep_inc:
- if source.endswith(".m") or source.endswith(".mm"):
+ else:
+ if self.objc_gnustep_inc and (
+ source.endswith(".m") or source.endswith(".mm")
+ ):
args.extend(["-fobjc-runtime=gnustep-2.0", "-I", self.objc_gnustep_inc])
- if sys.platform == "win32":
+ if targets_msvc(self.target):
# CodeView cannot represent Objective-C types, so force
# DWARF even though the target is MSVC. The debugger needs
# it to recognize classes and resolve dynamic types.
if not self.no_debug_info:
args.append("-gdwarf")
args.extend(["-Xclang", "--dependent-lib=msvcrtd"])
- elif self.sysroot:
- args.extend(["--sysroot", self.sysroot])
+ if self.sysroot:
+ # Not exclusive with the ObjC flags: a cross build needs both.
+ args.extend(["--sysroot", self.sysroot])
if self.std:
args.append("-std={0}".format(self.std))
@@ -813,6 +851,14 @@ def _get_link_command(self):
args = [self.linker]
args = self._add_m_option_if_needed(args)
+ if self.target:
+ # The target also picks the linker and the runtime libraries.
+ args.append("--target=" + self.target)
+ if targets_windows(self.target) and not targets_msvc(self.target):
+ # The MinGW driver looks for its linker in the target sysroot,
+ # which need hold nothing the host can execute.
+ args.append("-fuse-ld=lld")
+
if self.nodefaultlib:
args.append("-nostdlib")
args.append("-static")
@@ -840,17 +886,19 @@ def _get_link_command(self):
args.extend(["-isysroot", self.apple_sdk])
args.extend(["-Wl,-lto_library", "-Wl," + system_liblto])
- elif self.objc_gnustep_lib:
- args.extend(["-L", self.objc_gnustep_lib, "-lobjc"])
- if sys.platform == "linux":
- args.extend(["-Wl,-rpath," + self.objc_gnustep_lib])
- elif sys.platform == "win32":
- # /debug:dwarf keeps the DWARF sections in the image and, unlike
- # the PDB route, writes a COFF symbol table, which the debugger
- # needs to find the runtime metadata symbols ($_OBJC_CLASS_...).
- args.extend(["-fuse-ld=lld-link", "-Wl,/debug:dwarf"])
- elif self.sysroot:
- args.extend(["--sysroot", self.sysroot])
+ else:
+ if self.objc_gnustep_lib:
+ args.extend(["-L", self.objc_gnustep_lib, "-lobjc"])
+ if targets_msvc(self.target):
+ # /debug:dwarf keeps the DWARF sections in the image and,
+ # unlike the PDB route, writes a COFF symbol table, which
+ # the debugger needs to find the runtime metadata symbols
+ # ($_OBJC_CLASS_...).
+ args.extend(["-fuse-ld=lld-link", "-Wl,/debug:dwarf"])
+ elif not targets_windows(self.target):
+ args.extend(["-Wl,-rpath," + self.objc_gnustep_lib])
+ if self.sysroot:
+ args.extend(["--sysroot", self.sysroot])
return ("linking", self._obj_file_names(), self._exe_file_name(), None, args)
@@ -959,7 +1007,7 @@ def fix_arguments(args):
fix_arguments(args)
(toolchain_type, c_compiler, cxx_compiler) = find_toolchain(
- args.compiler, args.tools_dir
+ args.compiler, args.tools_dir, args.target
)
if not toolchain_type:
print(f"Unable to find toolchain {args.compiler}")
diff --git a/lldb/test/Shell/lit.cfg.py b/lldb/test/Shell/lit.cfg.py
index 0296a48da97fc..6e6ef1891b0df 100644
--- a/lldb/test/Shell/lit.cfg.py
+++ b/lldb/test/Shell/lit.cfg.py
@@ -157,6 +157,12 @@ def calculate_arch_features(arch_string):
if config.have_lldb_server:
config.available_features.add("lldb-server")
+
+# Same spelling as lldb/test/Shell/CMakeLists.txt and lldb/test/API/lit.cfg.py.
+def targets_mingw(triple):
+ return re.search(r"windows-gnu|mingw", triple or "") is not None
+
+
def runtime_exports(directory, symbol):
"""Whether a libobjc2 under `directory` exports `symbol`."""
@@ -198,6 +204,46 @@ def tool(name):
return False
+# A test whose inferior is built for another target says so itself, rather
+# than the suite claiming it for every invocation: most of the suite debugs
+# MSVC binaries, which such a claim would describe wrongly. Both expand to
+# nothing when inferiors are built for the host, so nothing else moves.
+#
+# Underscores, not hyphens: ToolSubst wraps a key in , so %inferior-abi
+# would be eaten by an existing %inferior substitution if one were ever added.
+_test_triple = getattr(config, "test_triple", None)
+config.substitutions.append(
+ (
+ "%inferior_abi",
+ # -O, not -o: the setting only takes effect before a target exists.
+ '-O "settings set plugin.object-file.pe-coff.abi gnu"'
+ if targets_mingw(_test_triple)
+ else "",
+ )
+)
+config.substitutions.append(
+ (
+ "%inferior_target",
+ "--target=" + _test_triple
+ if _test_triple and _test_triple != config.target_triple
+ else "",
+ )
+)
+
+# Windows has no rpath, so a MinGW-built inferior needs its own toolchain's
+# DLLs (libstdc++, libgcc) ahead of any other distribution's on PATH.
+if (
+ platform.system() == "Windows"
+ and config.cmake_sysroot
+ and targets_mingw(getattr(config, "test_triple", None))
+):
+ config.environment["PATH"] = os.path.pathsep.join(
+ (
+ os.path.join(config.cmake_sysroot, "bin"),
+ config.environment.get("PATH", ""),
+ )
+ )
+
if config.objc_gnustep_dir:
config.available_features.add("objc-gnustep")
if platform.system() == "Windows":
diff --git a/lldb/test/Shell/lit.site.cfg.py.in b/lldb/test/Shell/lit.site.cfg.py.in
index 68c94cd4ee3b2..035303937a821 100644
--- a/lldb/test/Shell/lit.site.cfg.py.in
+++ b/lldb/test/Shell/lit.site.cfg.py.in
@@ -20,6 +20,7 @@ config.has_libcxx = @LLDB_HAS_LIBCXX@
config.enable_remote = not @LLDB_TEST_SHELL_DISABLE_REMOTE@
config.libcxx_libs_dir = "@LIBCXX_LIBRARY_DIR@"
config.target_triple = "@LLVM_TARGET_TRIPLE@"
+config.test_triple = "@LLDB_TEST_TRIPLE@"
config.python_executable = "@Python3_EXECUTABLE@"
config.python_root_dir = "@Python3_ROOT_DIR@"
config.have_zlib = @LLVM_ENABLE_ZLIB@
>From ac21addec09b86fc50a2f6812c43ec140b16f3fd Mon Sep 17 00:00:00 2001
From: Rob <robk at robk.dev>
Date: Tue, 18 Aug 2026 22:50:50 +0100
Subject: [PATCH 41/49] [lldb][test] Build the API suite's Objective-C
inferiors for MinGW
OS reports Windows_NT for both Windows ABIs, so everything keyed on it was
really keyed on MSVC. Ask the triple instead, and give MinGW what it needs:
lld, because ld.bfd cannot resolve libobjc2's .objc_selector_* comdats.
@catch reaches the C++ ABI's __cxa_* entry points there, but the C++ driver
already links a standard library for them, and naming one here would pick
the wrong one under a distribution shipping libc++. /debug:dwarf stays
MSVC-only - MinGW emits DWARF and a COFF symbol table without being asked.
The rule that copies the runtime next to the test binary assumed the name
objc.dll. Copy whatever the runtime is actually called, since a MinGW build
uses the GNU convention and a distribution may version it further.
gnustep-base's DLLs are not copied for MinGW. There they come from a package
whose bin/ holds the whole toolchain rather than just that library, so the
directory is named through --inferior-env instead. That is the only way it
can reach the inferior: its PATH is deliberately emptied first, to stop a
sanitized LLDB's own library path leaking into it, and only --inferior-env
is applied afterwards.
A MinGW PE also needs its ABI declared, which dotest applies through
--setting in setUp, before a test creates its target.
Assisted-by: Claude Opus 5
---
.../Python/lldbsuite/test/make/Makefile.rules | 38 +++++++++++++++++--
lldb/test/API/lit.cfg.py | 19 ++++++++++
2 files changed, 53 insertions(+), 4 deletions(-)
diff --git a/lldb/packages/Python/lldbsuite/test/make/Makefile.rules b/lldb/packages/Python/lldbsuite/test/make/Makefile.rules
index 700291012273d..c0dde5b3596d3 100644
--- a/lldb/packages/Python/lldbsuite/test/make/Makefile.rules
+++ b/lldb/packages/Python/lldbsuite/test/make/Makefile.rules
@@ -146,6 +146,16 @@ endif
# Use -target to pass the triple to the compiler.
ARCH_CFLAGS := -target $(TRIPLE)
+# $(OS) is Windows_NT for both Windows ABIs, so anything that differs between
+# them has to ask the triple.
+ifeq "$(OS)" "Windows_NT"
+ ifeq "$(findstring -gnu,$(TRIPLE))$(findstring mingw,$(TRIPLE))" ""
+ WINDOWS_ABI := msvc
+ else
+ WINDOWS_ABI := gnu
+ endif
+endif
+
#----------------------------------------------------------------------
# CC defaults to clang.
#
@@ -559,12 +569,21 @@ ifneq "$(strip $(OBJC_GNUSTEP_DIR))" ""
ifeq "$(OS)" "Linux"
LDFLAGS +=-Wl,-rpath,$(OBJC_GNUSTEP_DIR)/lib
endif
- ifeq "$(OS)" "Windows_NT"
+ ifeq "$(WINDOWS_ABI)" "msvc"
# Keep the DWARF sections and emit a COFF symbol table; the
# default CodeView route cannot represent Objective-C types.
LDFLAGS +=-Wl,/debug:dwarf
GNUSTEP_NEEDS_DLL_COPY := 1
endif
+ ifeq "$(WINDOWS_ABI)" "gnu"
+ # MinGW emits DWARF and a COFF symbol table without being asked,
+ # but ld.bfd cannot resolve libobjc2's .objc_selector_* comdats.
+ # @catch reaches the C++ ABI's __cxa_* entry points, which the
+ # C++ driver links for us - naming the library here would pick
+ # the wrong one under a libc++ distribution such as CLANG64.
+ LDFLAGS +=-fuse-ld=lld
+ GNUSTEP_NEEDS_DLL_COPY := 1
+ endif
# gnustep-base (Foundation) on top of the runtime, for tests of the
# Foundation data formatters. Its headers need the gnustep-2.x string
# ABI (a newer -fobjc-runtime than the bare-runtime tests use), blocks
@@ -584,7 +603,7 @@ ifneq "$(strip $(OBJC_GNUSTEP_DIR))" ""
ifeq "$(OS)" "Linux"
LDFLAGS +=-Wl,-rpath,$(OBJC_GNUSTEP_BASE_DIR)/lib
endif
- ifeq "$(OS)" "Windows_NT"
+ ifeq "$(WINDOWS_ABI)" "msvc"
GNUSTEP_NEEDS_BASE_DLL_COPY := 1
endif
endif
@@ -780,16 +799,27 @@ print-%:
# would otherwise fold the tab-indented lines of the next conditional block
# into this rule's recipe.
ifeq "$(GNUSTEP_NEEDS_DLL_COPY)" "1"
-all: objc.dll
+GNUSTEP_RUNTIME_DLL := $(firstword $(wildcard \
+ $(OBJC_GNUSTEP_DIR)/lib/objc.dll \
+ $(OBJC_GNUSTEP_DIR)/lib/libobjc*.dll \
+ $(OBJC_GNUSTEP_DIR)/bin/objc.dll \
+ $(OBJC_GNUSTEP_DIR)/bin/libobjc*.dll))
+
+ifneq "$(strip $(GNUSTEP_RUNTIME_DLL))" ""
+all: $(notdir $(GNUSTEP_RUNTIME_DLL))
-objc.dll: $(OBJC_GNUSTEP_DIR)/lib/objc.dll
+$(notdir $(GNUSTEP_RUNTIME_DLL)): $(GNUSTEP_RUNTIME_DLL)
cp $< $@
endif
+endif
# Likewise for gnustep-base and the runtime DLLs it depends on, which the
# tools-windows-msvc layout keeps under bin/. Their PDBs come along: the
# runtime's small-object class table is a hidden symbol that LLDB can only
# see through the PDB, and tagged pointers cannot be typed without it.
+#
+# MSVC only: a MinGW gnustep-base has no PDBs (the cp would fail), and its
+# bin/ holds a whole toolchain, so that directory goes on PATH instead.
ifeq "$(GNUSTEP_NEEDS_BASE_DLL_COPY)" "1"
all: gnustep-base-dlls
diff --git a/lldb/test/API/lit.cfg.py b/lldb/test/API/lit.cfg.py
index b7f873b3142fd..7af3704cb1b37 100644
--- a/lldb/test/API/lit.cfg.py
+++ b/lldb/test/API/lit.cfg.py
@@ -4,6 +4,7 @@
import os
import platform
+import re
import shlex
import shutil
import subprocess
@@ -253,6 +254,24 @@ def delete_module_cache(path):
if is_configured("test_triple"):
dotest_cmd += ["--triple", config.test_triple]
+# Same spelling as lldb/test/Shell/lit.cfg.py and lldb/test/Shell/CMakeLists.txt.
+is_mingw_target = platform.system() == "Windows" and re.search(
+ r"windows-gnu|mingw", is_configured("test_triple") or ""
+)
+
+# A MinGW inferior needs its toolchain's runtime DLLs, and lldbtest clears the
+# inferior's PATH, so hand back the sysroot's bin/.
+if is_mingw_target and is_configured("cmake_sysroot"):
+ dotest_cmd += [
+ "--inferior-env",
+ "PATH=" + os.path.join(config.cmake_sysroot, "bin"),
+ ]
+
+# A MinGW PE carries no ABI marker; LLDB assumes the one it was built for.
+# --setting lands in setUp, before the test creates its target.
+if is_mingw_target:
+ dotest_cmd += ["--setting", "plugin.object-file.pe-coff.abi=gnu"]
+
if is_configured("lldb_build_directory"):
dotest_cmd += ["--build-dir", config.lldb_build_directory]
>From e6aea1f6457bdc8d7cd47b6ba006113b87640122 Mon Sep 17 00:00:00 2001
From: Rob <robk at robk.dev>
Date: Tue, 18 Aug 2026 22:50:51 +0100
Subject: [PATCH 42/49] [lldb][docs] Document testing Objective-C against a
MinGW runtime
Which ABI a Windows libobjc2 was built for decides how test inferiors have to
be built and whether catch breakpoints are available at all, so say so, and
say that the suite works it out from the runtime rather than from a triple.
Record the three things that are not discoverable from the outside: that
LLDB_TEST_TRIPLE now reaches the Shell suite as well, where a test asks for
it by name; that a MinGW PE has to be told which ABI it uses, before the
target is created, because LLDB assumes the one it was built for and accepts
the setting silently afterwards; and which MinGW sysroots can be used at all,
since Objective-C inferiors are linked with the C++ driver and for a
windows-gnu target that means libstdc++ and libgcc.
Assisted-by: Claude Opus 5
---
lldb/docs/resources/build.md | 42 ++++++++++++++++++++++++++++++++++++
1 file changed, 42 insertions(+)
diff --git a/lldb/docs/resources/build.md b/lldb/docs/resources/build.md
index d01586a0f18d7..2548c2ae413dd 100644
--- a/lldb/docs/resources/build.md
+++ b/lldb/docs/resources/build.md
@@ -304,6 +304,48 @@ feature and the API tests in the `objc-gnustep` category are enabled; without
them those tests are skipped. Foundation is not required: the tests run
against libobjc2 alone.
+On Windows the runtime can be built for either ABI, and which one it is
+decides how the tests have to be built and what the debugger can do. An MSVC
+libobjc2 installs `lib/objc.dll` and raises a native SEH exception, so there
+is no symbol at which to stop when a handler is entered and catch breakpoints
+are unavailable. A MinGW one installs `libobjc.dll`, possibly versioned, and
+unwinds through the Itanium ABI, so catch breakpoints work. Neither is
+selected by hand: the test suite reads the runtime's export table and enables
+the `objc-gnustep-catch` feature only when the entry point is really there.
+
+To test against a MinGW runtime, name the triple and the sysroot it belongs
+to. LLDB itself does not need rebuilding for it:
+
+```
+-DLLDB_TEST_TRIPLE=x86_64-w64-windows-gnu
+-DLLDB_TEST_SYSROOT=C:/msys64/ucrt64
+```
+
+The sysroot has to be one whose C++ runtime the compiler expects, since
+Objective-C inferiors are linked with the C++ driver: for `windows-gnu` that
+is libstdc++ and libgcc, so MSYS2's UCRT64 and MINGW64 work while its CLANG64,
+which ships libc++ and libunwind under the same triple, does not.
+
+`LLDB_TEST_TRIPLE` is what test inferiors are built for. The API suite applies
+it to every inferior; the Shell suite applies it only where a test asks, with
+the `%inferior_target` substitution, because most of that suite debugs
+binaries that only make sense in the host environment. A Shell test that
+debugs a Windows inferior also states its ABI with `%inferior_abi`, since a PE
+records nothing that says which one it was built for.
+
+When debugging a MinGW binary by hand, tell LLDB which ABI the PE uses. It
+cannot tell from the file, and assumes the one LLDB itself was built for -
+which for an MSVC-built LLDB is the wrong answer for a MinGW program, giving
+it Microsoft C++ record layout and a 64-bit `long double`:
+
+```
+settings set plugin.object-file.pe-coff.abi gnu
+```
+
+This has to be set **before** the target is created, so pass it with `-O`
+rather than `-o`, or put it in `.lldbinit`. Set afterwards it is accepted and
+silently has no effect.
+
#### Windows
On Windows the LLDB test suite requires lld. Either add `lld` to
>From 07f96789cb848671fb54daed296a42787a156309 Mon Sep 17 00:00:00 2001
From: Rob <robk at robk.dev>
Date: Tue, 18 Aug 2026 22:50:52 +0100
Subject: [PATCH 43/49] [lldb][GNUstep] Resolve exception breakpoints against
earlier modules
An exception breakpoint's filter asks the process for the language runtime
and rejects the module outright when there is not one yet
(ExceptionSearchFilter::ModulePasses). Nothing revisits those modules, and
Target::ModulesDidLoad resolves breakpoints before it notifies language
runtimes, so a breakpoint set before running silently misses everything
loaded up to the point this runtime came into existence.
That is invisible while the entry point lives in the runtime's own module,
which is the case on ELF and the reason it has gone unnoticed. On MinGW
clang routes @catch through libstdc++'s __cxa_begin_catch, so whichever of
libobjc and libstdc++ the loader happened to map first decided whether the
catch breakpoint ever resolved - it fired about half the time.
Re-run the resolvers once, at the first module load after this runtime
exists, skipping disabled breakpoints rather than installing traps for them
and sending the event a client listening for locations-added expects.
The FIXME says where this belongs: fixing it in the shared filter means
re-resolving from Target once a filter's runtime appears, which would newly
fire for every language, so it wants its own patch.
Assisted-by: Claude Opus 5
---
.../GNUstepObjCRuntime/GNUstepObjCRuntime.cpp | 29 +++++++++++++++++++
.../GNUstepObjCRuntime/GNUstepObjCRuntime.h | 6 ++++
2 files changed, 35 insertions(+)
diff --git a/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCRuntime.cpp b/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCRuntime.cpp
index 831ba70209cec..562377170cb0e 100644
--- a/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCRuntime.cpp
+++ b/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCRuntime.cpp
@@ -14,6 +14,7 @@
#include "Plugins/TypeSystem/Clang/TypeSystemClang.h"
+#include "lldb/Breakpoint/BreakpointList.h"
#include "lldb/Core/Address.h"
#include "lldb/Core/Module.h"
#include "lldb/Core/ModuleList.h"
@@ -1382,8 +1383,36 @@ GNUstepObjCRuntime::GetLanguageSpecificData(SymbolContext sc) {
return dict_up;
}
+// FIXME: This belongs in ExceptionSearchFilter::ModulePasses, which is where
+// the defect is: it asks the process for the language runtime and rejects the
+// module outright when there is not one yet, and nothing revisits those
+// modules. Target::ModulesDidLoad resolves breakpoints before it notifies
+// language runtimes, so a breakpoint set before running misses every module
+// up to the point this runtime came into existence. Fixing it there means
+// re-resolving from Target once a filter's runtime appears, which would newly
+// fire for every language - worth doing, but not from here.
+//
+// It only bites when the entry point is outside the runtime's own module,
+// which on MinGW it is: libstdc++'s __cxa_begin_catch.
+void GNUstepObjCRuntime::ResolveExceptionBreakpoints() {
+ Target &target = GetTargetRef();
+ ModuleList &modules = target.GetImages();
+ for (bool internal : {false, true}) {
+ BreakpointList &breakpoints = target.GetBreakpointList(internal);
+ for (const BreakpointSP &bp_sp : breakpoints.Breakpoints()) {
+ if (bp_sp && bp_sp->IsEnabled() && bp_sp->GetSearchFilter() &&
+ bp_sp->GetSearchFilter()->GetFilterTy() == SearchFilter::Exception)
+ bp_sp->ResolveBreakpointInModules(modules, /*send_event=*/true);
+ }
+ }
+}
+
void GNUstepObjCRuntime::ModulesDidLoad(const ModuleList &module_list) {
ReadObjCLibraryIfNeeded(module_list);
+ if (!m_swept_exception_breakpoints) {
+ m_swept_exception_breakpoints = true;
+ ResolveExceptionBreakpoints();
+ }
// Everything cached from a symbol lookup can be invalidated by new modules:
// classes to add to the map, dispatch entry points that may only now exist,
diff --git a/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCRuntime.h b/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCRuntime.h
index d44559bb01ab3..310b0f4863a5f 100644
--- a/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCRuntime.h
+++ b/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCRuntime.h
@@ -241,6 +241,12 @@ class GNUstepObjCRuntime : public lldb_private::ObjCLanguageRuntime {
/// runtime's life.
FunctionCaller *GetObjectDescriptionCaller(ExecutionContext &exe_ctx);
+ /// Re-runs every exception breakpoint's resolver, so modules loaded before
+ /// this runtime existed are considered.
+ void ResolveExceptionBreakpoints();
+
+ bool m_swept_exception_breakpoints = false;
+
lldb::ModuleSP m_objc_module_sp;
/// Utility function wrapping the -description/-UTF8String pair; owns
>From 0d17bef10ad0a4c98f22e7489c7c051f1e4ba1d4 Mon Sep 17 00:00:00 2001
From: Rob <robk at robk.dev>
Date: Tue, 18 Aug 2026 22:50:53 +0100
Subject: [PATCH 44/49] [lldb][GNUstep] Take ivar offsets from the runtime, not
from a struct layout
An over-aligned ivar printed as garbage in an aggregate - `frame variable *w`
- while reading correctly on its own, and every ivar after it was wrong too.
libobjc2 allocates an instance behind a hidden reference-count word and
aligns each ivar with that word included (objc_compute_ivar_offsets, ivar.c),
so a long double lands at object-relative offset 8, correctly 16-byte aligned
in the allocation. Laying the class out as a plain struct puts it at 16
instead. Clang emits no offset for an Objective-C ivar - the runtime owns it
- so TypeSystemClang fell back to that layout and read the wrong bytes.
It asks the runtime first, which is how the Apple runtime supplies the same
information; this runtime just never answered. Direct access already worked
because it resolves the ivar offset symbol instead.
Assisted-by: Claude Opus 5
---
.../GNUstepObjCRuntime/GNUstepObjCRuntime.cpp | 23 +++++++++++++++++++
.../GNUstepObjCRuntime/GNUstepObjCRuntime.h | 6 +++++
2 files changed, 29 insertions(+)
diff --git a/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCRuntime.cpp b/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCRuntime.cpp
index 562377170cb0e..da928c88e28e4 100644
--- a/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCRuntime.cpp
+++ b/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCRuntime.cpp
@@ -1280,6 +1280,29 @@ GNUstepObjCRuntime::GetClassDescriptor(ValueObject &in_value) {
return ObjCLanguageRuntime::GetClassDescriptor(in_value);
}
+size_t GNUstepObjCRuntime::GetByteOffsetForIvar(CompilerType &parent_qual_type,
+ const char *ivar_name) {
+ // An instance is allocated behind a reference-count word, and libobjc2
+ // aligns each ivar with that word included (objc_compute_ivar_offsets,
+ // ivar.c). An ivar needing more than pointer alignment therefore sits at a
+ // different offset than laying the class out as a plain struct would give,
+ // and only the runtime's own metadata knows which. Offsets are absolute, so
+ // a superclass's ivar needs no adjustment.
+ ClassDescriptorSP descriptor_sp =
+ GetClassDescriptorFromClassName(parent_qual_type.GetTypeName());
+ const ConstString name(ivar_name);
+ for (uint32_t depth = 0; descriptor_sp && depth < g_max_superclass_depth;
+ ++depth, descriptor_sp = descriptor_sp->GetSuperclass()) {
+ const size_t num_ivars = descriptor_sp->GetNumIVars();
+ for (size_t i = 0; i < num_ivars; ++i) {
+ const auto &ivar = descriptor_sp->GetIVarAtIndex(i);
+ if (ivar.m_name == name)
+ return ivar.m_offset;
+ }
+ }
+ return LLDB_INVALID_IVAR_OFFSET;
+}
+
std::optional<uint64_t>
GNUstepObjCRuntime::GetTypeBitSize(const CompilerType &compiler_type) {
ClassDescriptorSP descriptor_sp =
diff --git a/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCRuntime.h b/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCRuntime.h
index 310b0f4863a5f..1d29a17bb222e 100644
--- a/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCRuntime.h
+++ b/lldb/source/Plugins/LanguageRuntime/ObjC/GNUstepObjCRuntime/GNUstepObjCRuntime.h
@@ -166,6 +166,12 @@ class GNUstepObjCRuntime : public lldb_private::ObjCLanguageRuntime {
/// explicitly, and fall back to the inherited behaviour otherwise.
std::optional<CompilerType> GetRuntimeType(CompilerType base_type) override;
+ /// Byte offset of ivar \p ivar_name within \p parent_qual_type, or
+ /// LLDB_INVALID_IVAR_OFFSET. Without this the offset comes from laying the
+ /// class out as a plain struct, which libobjc2's does not match.
+ size_t GetByteOffsetForIvar(CompilerType &parent_qual_type,
+ const char *ivar_name) override;
+
/// Size of an Objective-C class, in bits.
///
/// The inherited implementation derives this from the ivar list, as the end
>From fdca9543a6f50849aee0cf39b72b8122f83eb206 Mon Sep 17 00:00:00 2001
From: Rob <robk at robk.dev>
Date: Tue, 18 Aug 2026 22:50:54 +0100
Subject: [PATCH 45/49] [lldb][test] Require an MSVC toolchain where a test
links with one
TestIRMemoryMapWindows.test is the only test using %msvc_link, and asked only
for target-windows. That substitution exists only when lit itself runs on
Windows with cl on PATH (helper/toolchain.py), so a build configured under
vcvars but tested from a plain shell runs the literal string and the shell
returns 127. Its siblings in SymbolFile/PDB already ask for msvc.
Assisted-by: Claude Opus 5
---
lldb/test/Shell/Expr/TestIRMemoryMapWindows.test | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/lldb/test/Shell/Expr/TestIRMemoryMapWindows.test b/lldb/test/Shell/Expr/TestIRMemoryMapWindows.test
index 66f0bce304730..a3a0694a7c19e 100644
--- a/lldb/test/Shell/Expr/TestIRMemoryMapWindows.test
+++ b/lldb/test/Shell/Expr/TestIRMemoryMapWindows.test
@@ -1,4 +1,4 @@
-# REQUIRES: target-windows
+# REQUIRES: target-windows, msvc
# RUN: %clang_cl_host /Zi /GS- %p/Inputs/call-function.cpp /c /o %t.obj
# RUN: %msvc_link /debug:full %t.obj /out:%t
>From 84468ea669e7ea68a42a57a13c2a504fdb214ebe Mon Sep 17 00:00:00 2001
From: Rob <robk at robk.dev>
Date: Tue, 18 Aug 2026 22:50:55 +0100
Subject: [PATCH 46/49] [lldb][test] Derive the Windows Shell features from the
inferior's triple
target-windows, windows-msvc and windows-gnu came from the triple LLDB itself
was built for, which is backwards: a test gating on them is asking what its
own inferior is. Everywhere except a cross-configured build the two triples
are the same, so this reads as it always did; only a build testing MinGW
inferiors from an MSVC-built LLDB can tell the difference, and there the old
answer was wrong.
Three tests wanted one of the other two triples and now say so. The
ObjectFile/PECOFF default-triple pair exists to pin what LLDB assumes for a
PE that declares no ABI, which is a property of how LLDB was built.
Target/dependent-modules-nodupe-windows chooses linker syntax for a binary it
builds with %clang_host, so it wants the host's.
Assisted-by: Claude Opus 5
---
.../PECOFF/default-triple-windows-gnu.yaml | 2 +-
.../PECOFF/default-triple-windows-msvc.yaml | 2 +-
.../dependent-modules-nodupe-windows.test | 4 ++--
lldb/test/Shell/lit.cfg.py | 23 ++++++++++++++++---
4 files changed, 24 insertions(+), 7 deletions(-)
diff --git a/lldb/test/Shell/ObjectFile/PECOFF/default-triple-windows-gnu.yaml b/lldb/test/Shell/ObjectFile/PECOFF/default-triple-windows-gnu.yaml
index 7d21dd4e14a00..ff0d8da456ebd 100644
--- a/lldb/test/Shell/ObjectFile/PECOFF/default-triple-windows-gnu.yaml
+++ b/lldb/test/Shell/ObjectFile/PECOFF/default-triple-windows-gnu.yaml
@@ -1,4 +1,4 @@
-# XFAIL: !windows-gnu
+# XFAIL: !lldb-default-triple-windows-gnu
# RUN: yaml2obj %s -o %t
# RUN: lldb-test object-file %t | FileCheck %s
diff --git a/lldb/test/Shell/ObjectFile/PECOFF/default-triple-windows-msvc.yaml b/lldb/test/Shell/ObjectFile/PECOFF/default-triple-windows-msvc.yaml
index 1f0380e1f856d..19cd3a996d26f 100644
--- a/lldb/test/Shell/ObjectFile/PECOFF/default-triple-windows-msvc.yaml
+++ b/lldb/test/Shell/ObjectFile/PECOFF/default-triple-windows-msvc.yaml
@@ -1,4 +1,4 @@
-# XFAIL: windows-gnu
+# XFAIL: lldb-default-triple-windows-gnu
# RUN: yaml2obj %s -o %t
# RUN: lldb-test object-file %t | FileCheck %s
diff --git a/lldb/test/Shell/Target/dependent-modules-nodupe-windows.test b/lldb/test/Shell/Target/dependent-modules-nodupe-windows.test
index f84714869dd48..c7e1acbc95762 100644
--- a/lldb/test/Shell/Target/dependent-modules-nodupe-windows.test
+++ b/lldb/test/Shell/Target/dependent-modules-nodupe-windows.test
@@ -4,10 +4,10 @@
# process actually loads the DLL.
# RUN: %clang_host -g0 -O0 -shared %S/Inputs/shlib.c -o %t.shlib.dll \
-# RUN: %if windows-msvc %{-Wl,-implib:%t.shlib.lib -Wl,-debug:none%} \
+# RUN: %if host-windows-msvc %{-Wl,-implib:%t.shlib.lib -Wl,-debug:none%} \
# RUN: %else %{-Wl,--out-implib=%t.shlib.lib%}
# RUN: %clang_host -g0 -O0 %S/Inputs/main.c %t.shlib.lib -o %t.main.exe \
-# RUN: %if windows-msvc %{-Wl,-debug:none%}
+# RUN: %if host-windows-msvc %{-Wl,-debug:none%}
# RUN: %lldb -b -o "#before" -o "target modules list" -o "b main" -o run \
# RUN: -o "#after" -o "target modules list" %t.main.exe | FileCheck --ignore-case %s
diff --git a/lldb/test/Shell/lit.cfg.py b/lldb/test/Shell/lit.cfg.py
index 6e6ef1891b0df..6ba28a0d94e58 100644
--- a/lldb/test/Shell/lit.cfg.py
+++ b/lldb/test/Shell/lit.cfg.py
@@ -68,15 +68,32 @@
if re.match(r"^arm(hf.*-linux)|(.*-linux-gnuabihf)", config.target_triple):
config.available_features.add("armhf-linux")
-if re.match(r".*-(windows|mingw32)", config.target_triple):
+# These describe what test inferiors are built as, which is what a test
+# gating on them cares about. Everywhere except a cross-configured build
+# LLDB_TEST_TRIPLE is the build triple, so this reads the same as it always
+# did. Two other triples matter to a handful of tests and are kept distinct:
+# the one LLDB itself was built for, and the host's.
+inferior_triple = getattr(config, "test_triple", None) or config.target_triple
+
+if re.match(r".*-(windows|mingw32)", inferior_triple):
config.available_features.add("target-windows")
-if re.match(r".*-(windows-msvc)$", config.target_triple):
+if re.match(r".*-(windows-msvc)$", inferior_triple):
config.available_features.add("windows-msvc")
-if re.match(r".*-(windows-gnu|mingw32)$", config.target_triple):
+if re.match(r".*-(windows-gnu|mingw32)$", inferior_triple):
config.available_features.add("windows-gnu")
+# What LLDB assumes for a PE that says nothing about its own ABI, which is
+# the thing ObjectFile/PECOFF's default-triple tests exist to pin.
+if re.match(r".*-(windows-gnu|mingw32)$", config.target_triple):
+ config.available_features.add("lldb-default-triple-windows-gnu")
+
+# %clang_host builds for the host, so a test choosing linker syntax for one
+# of its own binaries needs this rather than either of the above.
+if re.match(r".*-(windows-msvc)$", config.host_triple):
+ config.available_features.add("host-windows-msvc")
+
if config.targets_to_build:
for arch in config.targets_to_build.split(";"):
if arch:
>From 3547eb9c5731bc409b699cfe1ae6b4cd1004735d Mon Sep 17 00:00:00 2001
From: Rob <robk at robk.dev>
Date: Wed, 19 Aug 2026 19:30:17 +0100
Subject: [PATCH 47/49] [lldb][GNUstep] Add a test for ivar offsets from the
runtime
The fix that takes ivar offsets from libobjc2 rather than from a
reconstructed struct layout shipped without one. Cover it: an ivar
needing more than pointer alignment, read both through the aggregate
and directly, plus the ivar after it.
Without the override the aggregate reports 0 for the over-aligned ivar
and garbage for the one following it, while direct access is correct -
which is what makes this worth asserting both ways.
Assisted-by: Claude Opus 5
---
.../Shell/Expr/objc-gnustep-ivar-offsets.m | 63 +++++++++++++++++++
1 file changed, 63 insertions(+)
create mode 100644 lldb/test/Shell/Expr/objc-gnustep-ivar-offsets.m
diff --git a/lldb/test/Shell/Expr/objc-gnustep-ivar-offsets.m b/lldb/test/Shell/Expr/objc-gnustep-ivar-offsets.m
new file mode 100644
index 0000000000000..c6e95ac983bdb
--- /dev/null
+++ b/lldb/test/Shell/Expr/objc-gnustep-ivar-offsets.m
@@ -0,0 +1,63 @@
+// REQUIRES: objc-gnustep
+//
+// RUN: %build %inferior_target %s --compiler=clang --objc-gnustep --output=%t
+
+#import "objc/runtime.h"
+
+ at protocol NSCoding
+ at end
+
+#ifdef __has_attribute
+#if __has_attribute(objc_root_class)
+__attribute__((objc_root_class))
+#endif
+#endif
+ at interface NSObject <NSCoding> {
+ id isa;
+ int refcount;
+}
++ (id)new;
+ at end
+ at implementation NSObject
++ (id)new {
+ return class_createInstance(self, 0);
+}
+ at end
+
+// libobjc2 aligns each ivar with the hidden reference-count word included, so
+// an ivar needing more than pointer alignment does not land where laying the
+// class out as a plain struct would put it. __int128 rather than long double
+// because it is over-aligned on every target this runs on: long double is
+// 8-byte aligned for MSVC, which would make this vacuous there.
+ at interface Wide : NSObject {
+ at public
+ int before;
+ __int128 aligned;
+ int after;
+}
+ at end
+ at implementation Wide
+ at end
+
+// RUN: %lldb %inferior_abi -b -o "breakpoint set -p \"break [h]ere\" -X main" -o "run" \
+// RUN: -o "frame variable *wide" -o "p wide->aligned" -- %t \
+// RUN: | FileCheck %s
+//
+int main() {
+ Wide *wide = (Wide *)[Wide new];
+ wide->before = 1;
+ wide->aligned = 42;
+ wide->after = 3;
+ return 0; // break here
+}
+//
+// The aggregate must agree with direct access, for the over-aligned ivar and
+// for every ivar after it.
+//
+// CHECK: (lldb) frame variable *wide
+// CHECK: before = 1
+// CHECK-NEXT: aligned = 42
+// CHECK-NEXT: after = 3
+//
+// CHECK: (lldb) p wide->aligned
+// CHECK: (__int128) 42
>From 6ea6b1d2b315e921b8eaf0e60fd7848a2a8e1fb4 Mon Sep 17 00:00:00 2001
From: Rob <robk at robk.dev>
Date: Wed, 19 Aug 2026 20:08:25 +0100
Subject: [PATCH 48/49] [lldb][GNUstep] Trim the Objective-C test comments to
what a reader needs
Drop the narration that restates what the RUN and CHECK lines already say,
and the note claiming a class here cannot declare ivars because clang
asserts on them - that was fixed upstream in #215753, and it pointed at an
explanation in objc-gnustep-print.m that no longer exists.
What is kept is what makes a test valid rather than what it does: why the
decl-vendor test must first prove `image lookup -t` finds nothing, why the
nil-receiver step asserts a source location instead of a function name, and
the runtime facts a reader cannot infer from the source.
The stepping test's breakpoints named their own line numbers in a comment as
well as in the RUN line, which is two places to go stale; the comment now
just marks the line.
Assisted-by: Claude Opus 5
---
.../Shell/Expr/objc-gnustep-class-objects.m | 20 +++++++------------
.../Shell/Expr/objc-gnustep-decl-vendor.m | 8 +-------
.../test/Shell/Expr/objc-gnustep-exceptions.m | 10 +++-------
lldb/test/Shell/Expr/objc-gnustep-print.m | 10 +++-------
lldb/test/Shell/Expr/objc-gnustep-stepping.m | 20 ++++++++-----------
.../Shell/Expr/objc-gnustep-tagged-pointers.m | 2 --
6 files changed, 22 insertions(+), 48 deletions(-)
diff --git a/lldb/test/Shell/Expr/objc-gnustep-class-objects.m b/lldb/test/Shell/Expr/objc-gnustep-class-objects.m
index 469d37fd7d4b6..a2dc3671555c4 100644
--- a/lldb/test/Shell/Expr/objc-gnustep-class-objects.m
+++ b/lldb/test/Shell/Expr/objc-gnustep-class-objects.m
@@ -7,12 +7,10 @@
@protocol NSCoding
@end
-// A root class may declare its `isa` as `id` rather than `Class` (the GNUstep
-// tests and many libobjc2 programs do). Because `id` can carry a dynamic
-// type, the value of such a field - a class object - is offered to the
-// runtime for dynamic typing. libobjc2 names a metaclass after its class, so
-// a naive runtime would then report the class object as an instance of the
-// class, and expanding it would recurse forever through the same `isa`.
+// A root class may declare its `isa` as `id` rather than `Class`, and `id`
+// carries a dynamic type - so a class object gets offered to the runtime for
+// typing. libobjc2 names a metaclass after its class, which is what makes
+// reporting a class object as an instance of that class possible.
#ifdef __has_attribute
#if __has_attribute(objc_root_class)
__attribute__((objc_root_class))
@@ -29,9 +27,6 @@ + (id)new {
}
@end
-// (No ivars beyond the root class's: clang trips an assertion compiling
-// some GNUstep classes with ivars in +assertions builds, see
-// objc-gnustep-print.m.)
@interface Base : NSObject
@end
@implementation Base
@@ -42,7 +37,7 @@ @interface Derived : Base
@implementation Derived
@end
-// RUN: %lldb %inferior_abi -b -o "b objc-gnustep-class-objects.m:53" -o "run" \
+// RUN: %lldb %inferior_abi -b -o "b objc-gnustep-class-objects.m:49" -o "run" \
// RUN: -o "frame variable -d run-target -T object" \
// RUN: -o "frame variable -d run-target -T *object" \
// RUN: -o "frame variable -d run-target -T object->isa" \
@@ -54,12 +49,11 @@ int main() {
return object == 0;
}
//
-// The object itself gets its dynamic type...
// CHECK: (lldb) frame variable -d run-target -T object
// CHECK: (Derived *) object = 0x
//
-// ...and its `isa` stays a plain `id`: it points at the class object, which
-// must not be presented as an instance.
+// The `isa` must stay a plain `id`: it points at the class object, which is
+// not an instance of the class.
// CHECK: (lldb) frame variable -d run-target -T *object
// CHECK: (Derived) *object = {
// CHECK: (id) isa = 0x
diff --git a/lldb/test/Shell/Expr/objc-gnustep-decl-vendor.m b/lldb/test/Shell/Expr/objc-gnustep-decl-vendor.m
index 0a46c67fe73cb..5ea1d7571c011 100644
--- a/lldb/test/Shell/Expr/objc-gnustep-decl-vendor.m
+++ b/lldb/test/Shell/Expr/objc-gnustep-decl-vendor.m
@@ -26,8 +26,6 @@ int main() {
//
// NODWARF-NOT: name = "Hidden"
-// The interface is synthesized from the runtime instead, so the type can be
-// named and its ivars read.
//
// RUN: %lldb %inferior_abi -b -o "b objc-gnustep-decl-vendor.m:17" -o "run" \
// RUN: -o "type lookup Hidden" \
@@ -51,7 +49,6 @@ int main() {
// VENDOR: (lldb) expr -- ((Hidden *)hidden)->_float
// VENDOR: (float) $1 = 2
//
-// And the object expands in full, which is what a debugger UI shows.
//
// VENDOR: (lldb) frame variable -d run-target *hidden
// VENDOR-DAG: _int = 1
@@ -59,8 +56,7 @@ int main() {
// VENDOR-DAG: _char = '{{.*}}3'
// VENDOR-DAG: _ptr = 0x{{0*}}4
//
-// Methods are synthesized too, so a message send type-checks and runs. The
-// selector's name comes from the symbol clang emits for it: after
+// A selector's name comes from the symbol clang emits for it: after
// __objc_load the name field in memory holds a dispatch index instead.
//
// VENDOR: (lldb) expr -- [(Hidden *)hidden plainInt]
@@ -69,8 +65,6 @@ int main() {
// VENDOR: (lldb) expr -- [(Hidden *)hidden doubled:21]
// VENDOR: (int) $3 = 42
//
-// Class methods come from the metaclass, where libobjc2 keeps them as its
-// instance methods.
//
// VENDOR: (lldb) expr -- [Hidden classAnswer]
// VENDOR: (int) $4 = 7
diff --git a/lldb/test/Shell/Expr/objc-gnustep-exceptions.m b/lldb/test/Shell/Expr/objc-gnustep-exceptions.m
index f5010a4955e47..35f729b94d381 100644
--- a/lldb/test/Shell/Expr/objc-gnustep-exceptions.m
+++ b/lldb/test/Shell/Expr/objc-gnustep-exceptions.m
@@ -65,8 +65,6 @@ int main() {
return g_caught_name == 0;
}
-// An Objective-C exception breakpoint stops where the exception is raised,
-// and the frame recognizer presents the thrown object as `exception`.
//
// RUN: %lldb %inferior_abi -b -o "breakpoint set -E objc" -o "run" -o "frame variable" \
// RUN: -o "thread exception" \
@@ -78,17 +76,15 @@ int main() {
// THROW: (lldb) run
// THROW: stop reason = hit Objective-C exception
//
-// The recognizer synthesizes the argument, so the thrown object shows up in
-// `frame variable` even though objc_exception_throw has no debug info - and
-// it carries the dynamic type, not the `id` the runtime declares.
+// The recognizer synthesizes the argument, so this works even though
+// objc_exception_throw has no debug info, and it carries the dynamic type
+// rather than the `id` the runtime declares.
// THROW: (lldb) frame variable
// THROW: (Boom *) exception = 0x
//
// THROW: (lldb) thread exception
// THROW: (Boom *) exception = 0x
-// The object is reachable as a real local inside the handler, where `po`
-// works on it normally.
//
// RUN: %lldb %inferior_abi -b -o "b objc-gnustep-exceptions.m:63" -o "run" -o "po caught" \
// RUN: -- %t | FileCheck %s --check-prefix=CAUGHT
diff --git a/lldb/test/Shell/Expr/objc-gnustep-print.m b/lldb/test/Shell/Expr/objc-gnustep-print.m
index 7000147d42539..2c5b39850ed20 100644
--- a/lldb/test/Shell/Expr/objc-gnustep-print.m
+++ b/lldb/test/Shell/Expr/objc-gnustep-print.m
@@ -105,11 +105,9 @@ int main() {
return 0;
}
-// `po` sends -description and then -UTF8String to the result, which is what
-// gnustep-base's _NSPrintForDebugger does (Source/NSDebug.m), reached
-// through libobjc2's exported runtime API rather than through that symbol.
-// Nothing below needs Foundation, so this also covers `po` against a bare
-// runtime - a configuration where gnustep-base's hook does not exist at all.
+// `po` sends -description then -UTF8String, built from libobjc2's exported
+// API rather than resolving gnustep-base's _NSPrintForDebugger. Nothing here
+// needs Foundation, so this covers `po` against a bare runtime too.
@interface Str : NSObject {
const char *_bytes;
}
@@ -143,8 +141,6 @@ - (id)description {
// PO: <TestObj: described>
// PO-NOT: warning: `po` was unsuccessful
-// Stepping at a message send goes through the objc_msgSend trampoline into
-// the method implementation.
//
// RUN: %lldb %inferior_abi -b -o "b objc-gnustep-print.m:103" -o "run" -o "step" \
// RUN: -- %t | FileCheck %s --check-prefix=STEP
diff --git a/lldb/test/Shell/Expr/objc-gnustep-stepping.m b/lldb/test/Shell/Expr/objc-gnustep-stepping.m
index a44698eb2de25..9a99171ec6bb9 100644
--- a/lldb/test/Shell/Expr/objc-gnustep-stepping.m
+++ b/lldb/test/Shell/Expr/objc-gnustep-stepping.m
@@ -32,27 +32,23 @@ - (int)twice:(int)value {
}
@end
-// Stepping at a message send has to run through the runtime's dispatch
-// function and land in the method implementation.
//
-// RUN: %lldb %inferior_abi -b -o "b objc-gnustep-stepping.m:53" -o "run" -o "step" \
+// RUN: %lldb %inferior_abi -b -o "b objc-gnustep-stepping.m:49" -o "run" -o "step" \
// RUN: -- %t | FileCheck %s --check-prefix=STEP_IN
//
-// A message to nil dispatches nowhere, so the step must not try to run to an
-// implementation - and must not strand the user in the runtime's dispatch
-// assembly either. A runtime built from source has line information for that
-// assembly, so without stepping back out the debugger would stop in
-// objc_msgSend.S; asserting on the frame's source location is what catches
-// that, since the function name alone looks plausible either way.
+// A message to nil dispatches nowhere, so the step must land back in the
+// caller rather than in the runtime's dispatch assembly. The assertion is on
+// the frame's source location because the function name alone looks
+// plausible either way.
//
-// RUN: %lldb %inferior_abi -b -o "b objc-gnustep-stepping.m:55" -o "run" -o "step" \
+// RUN: %lldb %inferior_abi -b -o "b objc-gnustep-stepping.m:51" -o "run" -o "step" \
// RUN: -o "frame info" -- %t | FileCheck %s --check-prefix=STEP_OVER_NIL
//
int main() {
Doubler *doubler = [Doubler new];
- int value = [doubler twice:21]; // line 53: STEP_IN breaks here
+ int value = [doubler twice:21]; // STEP_IN breaks here
Doubler *nothing = (Doubler *)0;
- int none = [nothing twice:1]; // line 55: STEP_OVER_NIL breaks here
+ int none = [nothing twice:1]; // STEP_OVER_NIL breaks here
return value + none;
}
//
diff --git a/lldb/test/Shell/Expr/objc-gnustep-tagged-pointers.m b/lldb/test/Shell/Expr/objc-gnustep-tagged-pointers.m
index ce099dc37c875..a76c0c5d8c99e 100644
--- a/lldb/test/Shell/Expr/objc-gnustep-tagged-pointers.m
+++ b/lldb/test/Shell/Expr/objc-gnustep-tagged-pointers.m
@@ -49,8 +49,6 @@ int main() {
return tagged == ordinary;
}
//
-// The tagged value's class comes from the runtime's small object table, while
-// an ordinary pointer is still resolved by reading its class structure.
//
// CHECK: (lldb) frame variable -d run-target tagged
// CHECK: (TinyNumber *) tagged = 0x{{0*}}19
>From 40fdb99b7263cb5633752330f68b3dd54f44dc57 Mon Sep 17 00:00:00 2001
From: Rob <robk at robk.dev>
Date: Thu, 20 Aug 2026 09:37:38 +0100
Subject: [PATCH 49/49] [lldb] Look up a language runtime's IR passes by
primary language
An expression in an Objective-C++ frame reports its language as
eLanguageTypeObjC_plus_plus, and Process::GetLanguageRuntime keys its
cache on exactly that, so it asks the plugins to create a runtime for
ObjC++ and gets none - every ObjC runtime's CreateInstance accepts only
eLanguageTypeObjC. The expression then runs with no IR passes.
Ask for the primary language instead, which is what the comment in
GetLanguageRuntime already says a caller should do, since a runtime can
serve several LanguageTypes. Accepting ObjC++ in CreateInstance would not
work: the cache is keyed by the requested language, so it would build a
second, independent runtime.
Nothing else in tree implements GetIRPasses, so this changes behaviour
only for the runtime that does. For the GNUstep runtime it is the
difference between a message send in an Objective-C++ frame returning the
right value and returning zero: the pass is what interns the expression's
selectors with libobjc2, and an unregistered selector dispatches nowhere.
Assisted-by: Claude Opus 5
---
.../Clang/ClangExpressionParser.cpp | 3 +-
lldb/test/Shell/Expr/lit.local.cfg | 1 +
lldb/test/Shell/Expr/objc-gnustep-objcxx.mm | 64 +++++++++++++++++++
3 files changed, 67 insertions(+), 1 deletion(-)
create mode 100644 lldb/test/Shell/Expr/lit.local.cfg
create mode 100644 lldb/test/Shell/Expr/objc-gnustep-objcxx.mm
diff --git a/lldb/source/Plugins/ExpressionParser/Clang/ClangExpressionParser.cpp b/lldb/source/Plugins/ExpressionParser/Clang/ClangExpressionParser.cpp
index deab9a2a572ef..2c58d8355b652 100644
--- a/lldb/source/Plugins/ExpressionParser/Clang/ClangExpressionParser.cpp
+++ b/lldb/source/Plugins/ExpressionParser/Clang/ClangExpressionParser.cpp
@@ -1547,7 +1547,8 @@ lldb_private::Status ClangExpressionParser::DoPrepareForExecution(
lang.GetDescription().data());
lldb::ProcessSP process_sp = exe_ctx.GetProcessSP();
if (process_sp && lang) {
- auto runtime = process_sp->GetLanguageRuntime(lang.AsLanguageType());
+ auto runtime = process_sp->GetLanguageRuntime(
+ Language::GetPrimaryLanguage(lang.AsLanguageType()));
if (runtime)
runtime->GetIRPasses(custom_passes);
}
diff --git a/lldb/test/Shell/Expr/lit.local.cfg b/lldb/test/Shell/Expr/lit.local.cfg
new file mode 100644
index 0000000000000..8cf804fe127ad
--- /dev/null
+++ b/lldb/test/Shell/Expr/lit.local.cfg
@@ -0,0 +1 @@
+config.suffixes = [".test", ".cpp", ".s", ".m", ".mm", ".ll", ".c"]
diff --git a/lldb/test/Shell/Expr/objc-gnustep-objcxx.mm b/lldb/test/Shell/Expr/objc-gnustep-objcxx.mm
new file mode 100644
index 0000000000000..9bc79a64d9a01
--- /dev/null
+++ b/lldb/test/Shell/Expr/objc-gnustep-objcxx.mm
@@ -0,0 +1,64 @@
+// REQUIRES: objc-gnustep
+//
+// RUN: %build %inferior_target %s --compiler=clang --objc-gnustep --output=%t
+
+#import "objc/runtime.h"
+
+ at protocol NSCoding
+ at end
+
+#ifdef __has_attribute
+#if __has_attribute(objc_root_class)
+__attribute__((objc_root_class))
+#endif
+#endif
+ at interface NSObject <NSCoding> {
+ id isa;
+ int refcount;
+}
++ (id)new;
+ at end
+ at implementation NSObject
++ (id)new {
+ return class_createInstance(self, 0);
+}
+ at end
+
+ at interface Widget : NSObject {
+ at public
+ int count;
+}
+- (int)twice:(int)n;
+ at end
+ at implementation Widget
+- (int)twice:(int)n { return n * 2; }
+ at end
+
+struct CppThing {
+ int a;
+};
+
+// A message send in an Objective-C++ frame needs the same selector
+// registration as one in Objective-C: the expression's selectors are only
+// interned by the runtime if the IR pass runs, and the runtime is looked up
+// by the expression's language.
+//
+// RUN: %lldb %inferior_abi -b -o "breakpoint set -p \"break [h]ere\" -X main" \
+// RUN: -o "run" -o "expr (int)[w twice:21]" \
+// RUN: -o "frame variable *w" -o "frame variable t" -- %t | FileCheck %s
+//
+int main() {
+ Widget *w = (Widget *)[Widget new];
+ w->count = 7;
+ struct CppThing t = {3};
+ return t.a; // break here
+}
+//
+// CHECK: (lldb) expr (int)[w twice:21]
+// CHECK: (int) $0 = 42
+//
+// CHECK: (lldb) frame variable *w
+// CHECK: count = 7
+//
+// CHECK: (lldb) frame variable t
+// CHECK: (CppThing) t = (a = 3)
More information about the llvm-commits
mailing list