[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.
This commit is contained in:
Lang Hames 2026-03-16 16:40:34 +11:00 committed by GitHub
parent 696e82db33
commit c6626f0b70
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 239 additions and 0 deletions

View File

@ -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

View File

@ -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 <mutex>
#include <utility>
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 <typename T, typename LockT,
typename MutexT = typename LockT::mutex_type>
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 <typename OpT>
decltype(auto)
with_ref(OpT &&Op) && noexcept(noexcept(std::forward<OpT>(Op)(R))) {
return std::forward<OpT>(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 <typename OpT>
decltype(auto) with_ref(OpT &&Op) const && noexcept(
noexcept(std::forward<OpT>(Op)(std::as_const(R)))) {
return std::forward<OpT>(Op)(std::as_const(R));
}
private:
LockT Lock;
T &R;
};
/// Deduction guide: defaults LockT to std::scoped_lock<MutexT>.
template <typename T, typename MutexT>
LockedAccess(T &, MutexT &)
-> LockedAccess<T, std::scoped_lock<MutexT>, MutexT>;
} // namespace orc_rt
#endif // ORC_RT_LOCKEDACCESS_H

View File

@ -22,6 +22,7 @@ add_orc_rt_unittest(CoreTests
ExecutorAddressTest.cpp
IntervalMapTest.cpp
IntervalSetTest.cpp
LockedAccessTest.cpp
MathTest.cpp
MemoryFlagsTest.cpp
QueueingTaskDispatcherTest.cpp

View File

@ -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 <mutex>
#include <thread>
#include <type_traits>
using namespace orc_rt;
namespace {
template <typename T> 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<std::pair<int, int>> F({42, 7});
EXPECT_EQ(F.get()->first, 42);
EXPECT_EQ(F.get()->second, 7);
}
TEST(LockedAccessTest, ArrowWrite) {
Foo<std::pair<int, int>> 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<int> F(42);
static_assert(std::is_const_v<
std::remove_pointer_t<decltype(F.getConst().operator->())>>,
"const overload should return const pointer");
}
TEST(LockedAccessTest, DerefRead) {
Foo<int> F(42);
EXPECT_EQ(*F.get(), 42);
}
TEST(LockedAccessTest, DerefWrite) {
Foo<int> F;
*F.get() = 7;
EXPECT_EQ(F.Val, 7);
}
TEST(LockedAccessTest, DerefPassToFunction) {
Foo<int> F(42);
auto timesTwo = [](int &X) { return X * 2; };
EXPECT_EQ(timesTwo(*F.get()), 84);
}
TEST(LockedAccessTest, ConstDerefReturnsConstRef) {
Foo<int> F(42);
static_assert(
std::is_const_v<std::remove_reference_t<decltype(*F.getConst())>>,
"const overload should return const reference");
}
TEST(LockedAccessTest, WithRefRead) {
Foo<int> F(42);
F.get().with_ref([](int &Y) { EXPECT_EQ(Y, 42); });
}
TEST(LockedAccessTest, WithRefWrite) {
Foo<int> F;
F.get().with_ref([](int &Y) { Y = 7; });
EXPECT_EQ(F.Val, 7);
}
TEST(LockedAccessTest, WithRefReturnValue) {
Foo<int> F(42);
int Result = F.get().with_ref([](int &Y) { return Y + 1; });
EXPECT_EQ(Result, 43);
}
TEST(LockedAccessTest, WithRefReturnReference) {
Foo<std::pair<int, int>> F({1, 2});
int &Ref = F.get().with_ref(
[](std::pair<int, int> &P) -> int & { return P.second; });
EXPECT_EQ(&Ref, &F.Val.second);
}
TEST(LockedAccessTest, ConstWithRefGetsConstReference) {
Foo<int> F(42);
F.getConst().with_ref([](const int &Y) { EXPECT_EQ(Y, 42); });
}
TEST(LockedAccessTest, WithRefMultiStatement) {
Foo<std::pair<int, int>> F;
F.get().with_ref([](std::pair<int, int> &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<Checker> *F;
bool isLocked() { return !F->M.try_lock(); }
};
Foo<Checker> F({&F});
EXPECT_TRUE(F.get()->isLocked());
}
TEST(LockedAccessTest, HoldsLockDuringWithRef) {
Foo<int> F;
EXPECT_TRUE(F.get().with_ref([&](int &) { return !F.M.try_lock(); }));
}
TEST(LockedAccessTest, ProtectsAcrossThreads) {
Foo<int> 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);
}