[Lldb-commits] [lldb] [lldb] Add Locked<T> handle and locking accessors on Module (PR #198941)
Jonas Devlieghere via lldb-commits
lldb-commits at lists.llvm.org
Thu May 21 09:18:29 PDT 2026
https://github.com/JDevlieghere updated https://github.com/llvm/llvm-project/pull/198941
>From 2e723106069b563bb2e769161352bf0bdb6ce227 Mon Sep 17 00:00:00 2001
From: Jonas Devlieghere <jonas at devlieghere.com>
Date: Thu, 21 May 2026 09:16:54 -0700
Subject: [PATCH] [lldb] Add Locked<T> and SharedLocked<T> RAII handles in
Utility
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Introduce two RAII handles that pair a pointer-like value with a lock on
a caller-supplied mutex.
- Locked<PtrT, Mutex>: holds std::unique_lock<Mutex>. Move-only;
enforces exclusive access. Defaults to std::recursive_mutex to match
LLDB's existing synchronization style.
- SharedLocked<PtrT, Mutex>: holds a reference-counted
std::shared_lock<Mutex>. Copyable — copies share the same reader
lock and the lock releases when the last copy goes away. Defaults
to llvm::sys::RWMutex, the LLDB convention.
PtrT may be a raw pointer, std::shared_ptr, or std::unique_ptr;
convenience aliases LockedPtr/LockedSP/LockedUP and
SharedLockedPtr/SharedLockedSP/SharedLockedUP cover the common
combinations. SharedLocked's borrowed pointer is const-qualified so
readers can't mutate the pointee.
---
lldb/include/lldb/Utility/Locked.h | 142 ++++++++++++++++++
lldb/unittests/Utility/CMakeLists.txt | 1 +
lldb/unittests/Utility/LockedTest.cpp | 208 ++++++++++++++++++++++++++
3 files changed, 351 insertions(+)
create mode 100644 lldb/include/lldb/Utility/Locked.h
create mode 100644 lldb/unittests/Utility/LockedTest.cpp
diff --git a/lldb/include/lldb/Utility/Locked.h b/lldb/include/lldb/Utility/Locked.h
new file mode 100644
index 0000000000000..2e8488df3b0e7
--- /dev/null
+++ b/lldb/include/lldb/Utility/Locked.h
@@ -0,0 +1,142 @@
+//===-- Locked.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_UTILITY_LOCKED_H
+#define LLDB_UTILITY_LOCKED_H
+
+#include "llvm/Support/RWMutex.h"
+
+#include <memory>
+#include <mutex>
+#include <shared_mutex>
+#include <type_traits>
+#include <utility>
+
+namespace lldb_private {
+
+namespace detail {
+/// Common pointer-like accessors shared by `Locked` and `SharedLocked`.
+template <typename Derived, typename PtrT> class LockedAccessors {
+public:
+ auto operator->() const { return Raw(); }
+ decltype(auto) operator*() const { return *Self()->m_ptr; }
+ auto get() const { return Raw(); }
+ explicit operator bool() const { return Raw() != nullptr; }
+
+private:
+ const Derived *Self() const { return static_cast<const Derived *>(this); }
+
+ auto Raw() const {
+ if constexpr (std::is_pointer_v<PtrT>)
+ return Self()->m_ptr;
+ else
+ return Self()->m_ptr.get();
+ }
+};
+} // namespace detail
+
+/// A move-only RAII handle that pairs a pointer-like value with an exclusive
+/// lock on a caller-supplied mutex. While the handle is alive the borrowed
+/// pointer is serialized against other threads that go through the same
+/// mutex.
+///
+/// `PtrT` is the pointer-like value: a raw pointer (`T*`),
+/// `std::shared_ptr<T>`, or `std::unique_ptr<T>`. `Mutex` may be any type
+/// that satisfies `Lockable` — `std::mutex`, `std::recursive_mutex`,
+/// `std::shared_mutex`, or `llvm::sys::RWMutex` all work. Use the
+/// `LockedPtr`, `LockedSP`, `LockedUP` aliases for the common combinations.
+template <typename PtrT, typename Mutex = std::recursive_mutex>
+class Locked : public detail::LockedAccessors<Locked<PtrT, Mutex>, PtrT> {
+ friend class detail::LockedAccessors<Locked<PtrT, Mutex>, PtrT>;
+
+public:
+ using mutex_type = Mutex;
+ using lock_type = std::unique_lock<Mutex>;
+
+ Locked() = default;
+ Locked(mutex_type &m, PtrT p) : m_lock(m), m_ptr(std::move(p)) {}
+ Locked(lock_type lock, PtrT p)
+ : m_lock(std::move(lock)), m_ptr(std::move(p)) {}
+
+ Locked(Locked &&) noexcept = default;
+ Locked &operator=(Locked &&) noexcept = default;
+ Locked(const Locked &) = delete;
+ Locked &operator=(const Locked &) = delete;
+
+private:
+ lock_type m_lock;
+ PtrT m_ptr{};
+};
+
+/// A copyable RAII handle that pairs a pointer-like value with a shared
+/// (reader) lock on a caller-supplied mutex. Copies share the same
+/// underlying reader lock through reference counting; the lock is released
+/// when the last copy is destroyed. This makes a `SharedLocked` cheap to
+/// pass through code paths that branch or fan out without each leaf having
+/// to re-acquire.
+///
+/// The borrowed pointer is `const`-qualified so callers cannot mutate the
+/// pointee while holding only a reader's lock. `Mutex` must satisfy
+/// `SharedLockable` — `llvm::sys::RWMutex` (the LLDB convention) or
+/// `std::shared_mutex`. Use the `SharedLockedPtr`, `SharedLockedSP`,
+/// `SharedLockedUP` aliases for the common combinations.
+template <typename PtrT, typename Mutex = llvm::sys::RWMutex>
+class SharedLocked
+ : public detail::LockedAccessors<SharedLocked<PtrT, Mutex>, PtrT> {
+ friend class detail::LockedAccessors<SharedLocked<PtrT, Mutex>, PtrT>;
+
+public:
+ using mutex_type = Mutex;
+ using lock_type = std::shared_lock<Mutex>;
+
+ SharedLocked() = default;
+ SharedLocked(mutex_type &m, PtrT p)
+ : m_lock(std::make_shared<lock_type>(m)), m_ptr(std::move(p)) {}
+ SharedLocked(lock_type lock, PtrT p)
+ : m_lock(std::make_shared<lock_type>(std::move(lock))),
+ m_ptr(std::move(p)) {}
+
+ SharedLocked(const SharedLocked &) = default;
+ SharedLocked &operator=(const SharedLocked &) = default;
+ SharedLocked(SharedLocked &&) noexcept = default;
+ SharedLocked &operator=(SharedLocked &&) noexcept = default;
+
+private:
+ std::shared_ptr<lock_type> m_lock;
+ PtrT m_ptr{};
+};
+
+/// Exclusive (write) access aliases. The default mutex is
+/// `std::recursive_mutex` to match the existing LLDB synchronization style.
+/// @{
+template <typename T, typename Mutex = std::recursive_mutex>
+using LockedPtr = Locked<T *, Mutex>;
+
+template <typename T, typename Mutex = std::recursive_mutex>
+using LockedSP = Locked<std::shared_ptr<T>, Mutex>;
+
+template <typename T, typename Mutex = std::recursive_mutex>
+using LockedUP = Locked<std::unique_ptr<T>, Mutex>;
+/// @}
+
+/// Shared (read) access aliases. The default mutex is `llvm::sys::RWMutex`,
+/// the LLDB convention for read/write locks.
+/// @{
+template <typename T, typename Mutex = llvm::sys::RWMutex>
+using SharedLockedPtr = SharedLocked<const T *, Mutex>;
+
+template <typename T, typename Mutex = llvm::sys::RWMutex>
+using SharedLockedSP = SharedLocked<std::shared_ptr<const T>, Mutex>;
+
+template <typename T, typename Mutex = llvm::sys::RWMutex>
+using SharedLockedUP = SharedLocked<std::unique_ptr<const T>, Mutex>;
+/// @}
+
+} // namespace lldb_private
+
+#endif // LLDB_UTILITY_LOCKED_H
diff --git a/lldb/unittests/Utility/CMakeLists.txt b/lldb/unittests/Utility/CMakeLists.txt
index 9970c2f36e440..56b3c86fa7060 100644
--- a/lldb/unittests/Utility/CMakeLists.txt
+++ b/lldb/unittests/Utility/CMakeLists.txt
@@ -16,6 +16,7 @@ add_lldb_unittest(UtilityTests
FileSpecTest.cpp
FlagsTest.cpp
ListenerTest.cpp
+ LockedTest.cpp
LogTest.cpp
NameMatchesTest.cpp
NonNullSharedPtrTest.cpp
diff --git a/lldb/unittests/Utility/LockedTest.cpp b/lldb/unittests/Utility/LockedTest.cpp
new file mode 100644
index 0000000000000..ebbbf2de3d2ea
--- /dev/null
+++ b/lldb/unittests/Utility/LockedTest.cpp
@@ -0,0 +1,208 @@
+//===-- LockedTest.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 "lldb/Utility/Locked.h"
+
+#include "gtest/gtest.h"
+
+#include <memory>
+#include <mutex>
+#include <shared_mutex>
+#include <type_traits>
+
+using namespace lldb_private;
+
+namespace {
+struct Widget {
+ int value = 0;
+};
+} // namespace
+
+// Default mutex types match the LLDB conventions: recursive_mutex for write
+// access, llvm::sys::RWMutex for read access.
+static_assert(std::is_same_v<LockedPtr<Widget>::mutex_type,
+ std::recursive_mutex>);
+static_assert(std::is_same_v<LockedSP<Widget>::mutex_type,
+ std::recursive_mutex>);
+static_assert(std::is_same_v<LockedUP<Widget>::mutex_type,
+ std::recursive_mutex>);
+static_assert(std::is_same_v<SharedLockedPtr<Widget>::mutex_type,
+ llvm::sys::RWMutex>);
+static_assert(std::is_same_v<SharedLockedSP<Widget>::mutex_type,
+ llvm::sys::RWMutex>);
+static_assert(std::is_same_v<SharedLockedUP<Widget>::mutex_type,
+ llvm::sys::RWMutex>);
+
+// Force compile-time validation of every (pointer-flavor x mutex) combination
+// the templates are intended to support.
+template class lldb_private::Locked<Widget *, std::mutex>;
+template class lldb_private::Locked<Widget *, std::recursive_mutex>;
+template class lldb_private::Locked<Widget *, llvm::sys::RWMutex>;
+template class lldb_private::Locked<std::shared_ptr<Widget>,
+ std::recursive_mutex>;
+template class lldb_private::Locked<std::unique_ptr<Widget>,
+ std::recursive_mutex>;
+template class lldb_private::SharedLocked<const Widget *, std::shared_mutex>;
+template class lldb_private::SharedLocked<const Widget *, llvm::sys::RWMutex>;
+template class lldb_private::SharedLocked<std::shared_ptr<const Widget>,
+ llvm::sys::RWMutex>;
+template class lldb_private::SharedLocked<std::unique_ptr<const Widget>,
+ llvm::sys::RWMutex>;
+
+// Locked is move-only; SharedLocked is copyable.
+static_assert(!std::is_copy_constructible_v<LockedPtr<Widget>>);
+static_assert(!std::is_copy_assignable_v<LockedPtr<Widget>>);
+static_assert(std::is_move_constructible_v<LockedPtr<Widget>>);
+static_assert(std::is_move_assignable_v<LockedPtr<Widget>>);
+
+static_assert(std::is_copy_constructible_v<SharedLockedPtr<Widget>>);
+static_assert(std::is_copy_assignable_v<SharedLockedPtr<Widget>>);
+static_assert(std::is_move_constructible_v<SharedLockedPtr<Widget>>);
+static_assert(std::is_move_assignable_v<SharedLockedPtr<Widget>>);
+
+TEST(LockedTest, DefaultConstructed) {
+ LockedPtr<Widget> handle;
+ EXPECT_FALSE(handle);
+ EXPECT_EQ(handle.get(), nullptr);
+
+ SharedLockedPtr<Widget> shared;
+ EXPECT_FALSE(shared);
+ EXPECT_EQ(shared.get(), nullptr);
+}
+
+TEST(LockedTest, ExclusivePtrAccess) {
+ std::recursive_mutex mutex;
+ Widget widget;
+
+ LockedPtr<Widget> handle(mutex, &widget);
+ ASSERT_TRUE(handle);
+ EXPECT_EQ(handle.get(), &widget);
+
+ // Recursive mutex lets us reacquire on the same thread, proving the lock
+ // really is held.
+ EXPECT_TRUE(mutex.try_lock());
+ mutex.unlock();
+
+ handle->value = 7;
+ EXPECT_EQ((*handle).value, 7);
+}
+
+TEST(LockedTest, ExclusiveReleasesOnDestruction) {
+ std::mutex mutex;
+ Widget widget;
+ {
+ LockedPtr<Widget, std::mutex> handle(mutex, &widget);
+ EXPECT_FALSE(mutex.try_lock());
+ }
+ EXPECT_TRUE(mutex.try_lock());
+ mutex.unlock();
+}
+
+TEST(LockedTest, MoveTransfersLock) {
+ std::mutex mutex;
+ Widget widget;
+
+ LockedPtr<Widget, std::mutex> first(mutex, &widget);
+ EXPECT_FALSE(mutex.try_lock());
+
+ LockedPtr<Widget, std::mutex> second = std::move(first);
+ EXPECT_FALSE(mutex.try_lock());
+ EXPECT_TRUE(second);
+ EXPECT_EQ(second.get(), &widget);
+}
+
+TEST(LockedTest, AcceptsExternallyAcquiredLock) {
+ std::mutex mutex;
+ Widget widget;
+
+ std::unique_lock<std::mutex> lock(mutex);
+ LockedPtr<Widget, std::mutex> handle(std::move(lock), &widget);
+ ASSERT_TRUE(handle);
+ EXPECT_FALSE(mutex.try_lock());
+}
+
+TEST(LockedTest, SharedPtrFlavor) {
+ std::recursive_mutex mutex;
+ auto widget_sp = std::make_shared<Widget>();
+ widget_sp->value = 42;
+
+ LockedSP<Widget> handle(mutex, widget_sp);
+ ASSERT_TRUE(handle);
+ EXPECT_EQ(handle.get(), widget_sp.get());
+ EXPECT_EQ(handle->value, 42);
+}
+
+TEST(LockedTest, UniquePtrFlavor) {
+ std::recursive_mutex mutex;
+ auto widget_up = std::make_unique<Widget>();
+ widget_up->value = 99;
+ Widget *raw = widget_up.get();
+
+ LockedUP<Widget> handle(mutex, std::move(widget_up));
+ ASSERT_TRUE(handle);
+ EXPECT_EQ(handle.get(), raw);
+ EXPECT_EQ(handle->value, 99);
+}
+
+TEST(LockedTest, SharedAccessOnRWMutex) {
+ llvm::sys::RWMutex mutex;
+ Widget widget;
+ widget.value = 5;
+
+ SharedLockedPtr<Widget> reader(mutex, &widget);
+ ASSERT_TRUE(reader);
+ EXPECT_EQ(reader->value, 5);
+
+ static_assert(
+ std::is_same_v<decltype(reader.get()), const Widget *>,
+ "shared access borrows a const-qualified pointer");
+}
+
+// Copies of a SharedLocked share the same reader lock; the lock is released
+// only when the last copy goes away. Verified by polling try_lock() — it
+// must keep failing while any copy is alive.
+TEST(LockedTest, SharedAccessIsRefCounted) {
+ llvm::sys::RWMutex mutex;
+ Widget widget;
+
+ std::optional<SharedLockedPtr<Widget>> first;
+ first.emplace(mutex, &widget);
+ EXPECT_FALSE(mutex.try_lock());
+
+ std::optional<SharedLockedPtr<Widget>> second = first;
+ ASSERT_TRUE(second);
+ EXPECT_FALSE(mutex.try_lock());
+
+ first.reset();
+ // Second copy still holds the reader lock.
+ EXPECT_FALSE(mutex.try_lock());
+
+ second.reset();
+ // All copies gone — the writer lock is now obtainable.
+ EXPECT_TRUE(mutex.try_lock());
+ mutex.unlock();
+}
+
+TEST(LockedTest, SharedAcceptsExternallyAcquiredLock) {
+ llvm::sys::RWMutex mutex;
+ Widget widget;
+
+ std::shared_lock<llvm::sys::RWMutex> lock(mutex);
+ SharedLockedPtr<Widget> handle(std::move(lock), &widget);
+ ASSERT_TRUE(handle);
+ EXPECT_FALSE(mutex.try_lock());
+}
+
+TEST(LockedTest, ExclusiveAccessOnRWMutex) {
+ llvm::sys::RWMutex mutex;
+ Widget widget;
+ LockedPtr<Widget, llvm::sys::RWMutex> writer(mutex, &widget);
+ ASSERT_TRUE(writer);
+ writer->value = 11;
+ EXPECT_EQ(widget.value, 11);
+}
More information about the lldb-commits
mailing list