From c6626f0b7019ea4c8bca862fc0f9e8a775ee8cd8 Mon Sep 17 00:00:00 2001 From: Lang Hames Date: Mon, 16 Mar 2026 16:40:34 +1100 Subject: [PATCH] [orc-rt] Add LockedAccess utility. (#186737) LockedAccess provides pointer-like access to a value while holding a lock. All accessors are rvalue-ref-qualified, restricting usage to temporaries to prevent accidental lock lifetime extension. A with_ref method is provided for multi-statement critical sections. --- orc-rt/include/CMakeLists.txt | 1 + orc-rt/include/orc-rt/LockedAccess.h | 86 +++++++++++++++ orc-rt/unittests/CMakeLists.txt | 1 + orc-rt/unittests/LockedAccessTest.cpp | 151 ++++++++++++++++++++++++++ 4 files changed, 239 insertions(+) create mode 100644 orc-rt/include/orc-rt/LockedAccess.h create mode 100644 orc-rt/unittests/LockedAccessTest.cpp diff --git a/orc-rt/include/CMakeLists.txt b/orc-rt/include/CMakeLists.txt index a6272676e01a..9887eb382fe4 100644 --- a/orc-rt/include/CMakeLists.txt +++ b/orc-rt/include/CMakeLists.txt @@ -11,6 +11,7 @@ set(ORC_RT_HEADERS orc-rt/ExecutorAddress.h orc-rt/IntervalMap.h orc-rt/IntervalSet.h + orc-rt/LockedAccess.h orc-rt/Math.h orc-rt/MemoryFlags.h orc-rt/QueueingTaskDispatcher.h diff --git a/orc-rt/include/orc-rt/LockedAccess.h b/orc-rt/include/orc-rt/LockedAccess.h new file mode 100644 index 000000000000..01878b50dd3e --- /dev/null +++ b/orc-rt/include/orc-rt/LockedAccess.h @@ -0,0 +1,86 @@ +//===---------- LockedAccess.h - Locked access wrapper ----------*- 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 +// +//===----------------------------------------------------------------------===// +// +// Convenience wrapper for simple locked access to a value. +// +//===----------------------------------------------------------------------===// + +#ifndef ORC_RT_LOCKEDACCESS_H +#define ORC_RT_LOCKEDACCESS_H + +#include +#include + +namespace orc_rt { + +/// A convenience wrapper for simple locked access to a value. +/// +/// LockedAccess acquires a lock on construction and releases it on +/// destruction, providing pointer-like access to the value in between. +/// All accessors are rvalue-ref-qualified, so LockedAccess can only be used +/// as a temporary — it cannot be stored in a variable or member. +/// +/// This is intended for simple, short critical sections where a class wants +/// to return locked access to an internal value. For more complex locking +/// patterns (e.g. lock/unlock/relock, condition variables, multiple locks) +/// use std::unique_lock or std::scoped_lock directly. +template +class LockedAccess { +public: + /// Construct a LockedAccess that references \p R and locks \p M. + LockedAccess(T &R, MutexT &M) : Lock(M), R(R) {} + + // LockedAccess is not copyable or movable. + LockedAccess(const LockedAccess &) = delete; + LockedAccess &operator=(const LockedAccess &) = delete; + LockedAccess(LockedAccess &&) = delete; + LockedAccess &operator=(LockedAccess &&) = delete; + + /// Returns a reference to the locked value. The returned reference must not + /// be used after this LockedAccess temporary is destroyed, as the lock will + /// no longer be held. + T &operator*() && noexcept { return R; } + const T &operator*() const && noexcept { return R; } + + /// Returns a pointer to the locked value for member access. The pointer must + /// not be used after this LockedAccess temporary is destroyed, as the lock + /// will no longer be held. + T *operator->() && noexcept { return &R; } + const T *operator->() const && noexcept { return &R; } + + /// Calls \p Op with a mutable reference to the locked value, returning + /// whatever \p Op returns. The lock is held for the duration of the call. + /// Use this for multi-statement critical sections. + template + decltype(auto) + with_ref(OpT &&Op) && noexcept(noexcept(std::forward(Op)(R))) { + return std::forward(Op)(R); + } + + /// Calls \p Op with a const reference to the locked value, returning + /// whatever \p Op returns. The lock is held for the duration of the call. + template + decltype(auto) with_ref(OpT &&Op) const && noexcept( + noexcept(std::forward(Op)(std::as_const(R)))) { + return std::forward(Op)(std::as_const(R)); + } + +private: + LockT Lock; + T &R; +}; + +/// Deduction guide: defaults LockT to std::scoped_lock. +template +LockedAccess(T &, MutexT &) + -> LockedAccess, MutexT>; + +} // namespace orc_rt + +#endif // ORC_RT_LOCKEDACCESS_H diff --git a/orc-rt/unittests/CMakeLists.txt b/orc-rt/unittests/CMakeLists.txt index 501cd367d1ab..a6ad6e06e4a6 100644 --- a/orc-rt/unittests/CMakeLists.txt +++ b/orc-rt/unittests/CMakeLists.txt @@ -22,6 +22,7 @@ add_orc_rt_unittest(CoreTests ExecutorAddressTest.cpp IntervalMapTest.cpp IntervalSetTest.cpp + LockedAccessTest.cpp MathTest.cpp MemoryFlagsTest.cpp QueueingTaskDispatcherTest.cpp diff --git a/orc-rt/unittests/LockedAccessTest.cpp b/orc-rt/unittests/LockedAccessTest.cpp new file mode 100644 index 000000000000..da93b370da13 --- /dev/null +++ b/orc-rt/unittests/LockedAccessTest.cpp @@ -0,0 +1,151 @@ +//===- LockedAccessTest.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 +// +//===----------------------------------------------------------------------===// +// +// Tests for orc-rt's LockedAccess.h APIs. +// +//===----------------------------------------------------------------------===// + +#include "orc-rt/LockedAccess.h" +#include "gtest/gtest.h" + +#include +#include +#include + +using namespace orc_rt; + +namespace { + +template struct Foo { + std::mutex M; + T Val{}; + + Foo() = default; + Foo(T Val) : Val(std::move(Val)) {} + + auto get() { return LockedAccess(Val, M); } + + auto getConst() { return LockedAccess(std::as_const(Val), M); } +}; + +} // anonymous namespace + +TEST(LockedAccessTest, ArrowRead) { + Foo> F({42, 7}); + EXPECT_EQ(F.get()->first, 42); + EXPECT_EQ(F.get()->second, 7); +} + +TEST(LockedAccessTest, ArrowWrite) { + Foo> F; + F.get()->first = 10; + F.get()->second = 20; + EXPECT_EQ(F.Val.first, 10); + EXPECT_EQ(F.Val.second, 20); +} + +TEST(LockedAccessTest, ConstArrowReturnsConstPtr) { + Foo F(42); + static_assert(std::is_const_v< + std::remove_pointer_t())>>, + "const overload should return const pointer"); +} + +TEST(LockedAccessTest, DerefRead) { + Foo F(42); + EXPECT_EQ(*F.get(), 42); +} + +TEST(LockedAccessTest, DerefWrite) { + Foo F; + *F.get() = 7; + EXPECT_EQ(F.Val, 7); +} + +TEST(LockedAccessTest, DerefPassToFunction) { + Foo F(42); + auto timesTwo = [](int &X) { return X * 2; }; + EXPECT_EQ(timesTwo(*F.get()), 84); +} + +TEST(LockedAccessTest, ConstDerefReturnsConstRef) { + Foo F(42); + static_assert( + std::is_const_v>, + "const overload should return const reference"); +} + +TEST(LockedAccessTest, WithRefRead) { + Foo F(42); + F.get().with_ref([](int &Y) { EXPECT_EQ(Y, 42); }); +} + +TEST(LockedAccessTest, WithRefWrite) { + Foo F; + F.get().with_ref([](int &Y) { Y = 7; }); + EXPECT_EQ(F.Val, 7); +} + +TEST(LockedAccessTest, WithRefReturnValue) { + Foo F(42); + int Result = F.get().with_ref([](int &Y) { return Y + 1; }); + EXPECT_EQ(Result, 43); +} + +TEST(LockedAccessTest, WithRefReturnReference) { + Foo> F({1, 2}); + int &Ref = F.get().with_ref( + [](std::pair &P) -> int & { return P.second; }); + EXPECT_EQ(&Ref, &F.Val.second); +} + +TEST(LockedAccessTest, ConstWithRefGetsConstReference) { + Foo F(42); + F.getConst().with_ref([](const int &Y) { EXPECT_EQ(Y, 42); }); +} + +TEST(LockedAccessTest, WithRefMultiStatement) { + Foo> F; + F.get().with_ref([](std::pair &P) { + P.first = 10; + P.second = P.first + 5; + }); + EXPECT_EQ(F.Val.first, 10); + EXPECT_EQ(F.Val.second, 15); +} + +TEST(LockedAccessTest, HoldsLockDuringArrow) { + struct Checker { + Foo *F; + bool isLocked() { return !F->M.try_lock(); } + }; + Foo F({&F}); + EXPECT_TRUE(F.get()->isLocked()); +} + +TEST(LockedAccessTest, HoldsLockDuringWithRef) { + Foo F; + EXPECT_TRUE(F.get().with_ref([&](int &) { return !F.M.try_lock(); })); +} + +TEST(LockedAccessTest, ProtectsAcrossThreads) { + Foo F; + const int Iters = 10000; + + auto Increment = [&]() { + for (int I = 0; I < Iters; ++I) + F.get().with_ref([](int &C) { ++C; }); + }; + + std::thread T1(Increment); + std::thread T2(Increment); + T1.join(); + T2.join(); + + EXPECT_EQ(F.Val, 2 * Iters); +}