pw_sync: add Borrowable helper for transactional guarding
Adds a templated pw::sync::Borrowable wrapper which permits
objects or pointers/references to them to be wrapped with a lock
to enable threadsafety wrapping for data and/or objects with
transactional APIs where an internal lock may not make sense.
Change-Id: Ic59df609e933a571fd755b943f8ffcaccc4a5201
Reviewed-on: https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/59120
Reviewed-by: Wyatt Hepler <hepler@google.com>
Reviewed-by: Keir Mierle <keir@google.com>
Pigweed-Auto-Submit: Ewout van Bekkum <ewout@google.com>
Commit-Queue: Auto-Submit <auto-submit@pigweed.google.com.iam.gserviceaccount.com>
diff --git a/pw_sync/BUILD.bazel b/pw_sync/BUILD.bazel
index 5687165..c8cbe1c 100644
--- a/pw_sync/BUILD.bazel
+++ b/pw_sync/BUILD.bazel
@@ -108,6 +108,17 @@
],
)
+pw_cc_library(
+ name = "borrow",
+ hdrs = [
+ "public/pw_sync/borrow.h",
+ ],
+ includes = ["public"],
+ deps = [
+ "//pw_assert",
+ ],
+)
+
pw_cc_facade(
name = "mutex_facade",
hdrs = [
@@ -333,6 +344,18 @@
)
pw_cc_test(
+ name = "borrow_test",
+ srcs = [
+ "borrow_test.cc",
+ ],
+ deps = [
+ ":borrow",
+ "//pw_assert",
+ "//pw_unit_test",
+ ],
+)
+
+pw_cc_test(
name = "binary_semaphore_facade_test",
srcs = [
"binary_semaphore_facade_test.cc",
diff --git a/pw_sync/BUILD.gn b/pw_sync/BUILD.gn
index 5b6fecf..67d0448 100644
--- a/pw_sync/BUILD.gn
+++ b/pw_sync/BUILD.gn
@@ -16,6 +16,7 @@
import("$dir_pw_build/facade.gni")
import("$dir_pw_build/target_types.gni")
+import("$dir_pw_chrono/backend.gni")
import("$dir_pw_docgen/docs.gni")
import("$dir_pw_unit_test/test.gni")
import("backend.gni")
@@ -58,6 +59,12 @@
public_deps = [ "$dir_pw_preprocessor" ]
}
+pw_source_set("borrow") {
+ public_configs = [ ":public_include_path" ]
+ public = [ "public/pw_sync/borrow.h" ]
+ public_deps = [ dir_pw_assert ]
+}
+
pw_facade("mutex") {
backend = pw_sync_MUTEX_BACKEND
public_configs = [ ":public_include_path" ]
@@ -152,6 +159,7 @@
pw_test_group("tests") {
tests = [
+ ":borrow_test",
":binary_semaphore_facade_test",
":counting_semaphore_facade_test",
":mutex_facade_test",
@@ -162,6 +170,14 @@
]
}
+pw_test("borrow_test") {
+ sources = [ "borrow_test.cc" ]
+ deps = [
+ ":borrow",
+ dir_pw_assert,
+ ]
+}
+
pw_test("binary_semaphore_facade_test") {
enable_if = pw_sync_BINARY_SEMAPHORE_BACKEND != ""
sources = [
diff --git a/pw_sync/borrow_test.cc b/pw_sync/borrow_test.cc
new file mode 100644
index 0000000..4782d78
--- /dev/null
+++ b/pw_sync/borrow_test.cc
@@ -0,0 +1,307 @@
+// Copyright 2021 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#include "pw_sync/borrow.h"
+
+#include <chrono>
+#include <ratio>
+
+#include "gtest/gtest.h"
+#include "pw_assert/check.h"
+
+namespace pw::sync {
+namespace {
+
+template <typename Lock>
+class BorrowableTest : public ::testing::Test {
+ protected:
+ static constexpr int kInitialValue = 42;
+
+ BorrowableTest()
+ : foo_{.value = kInitialValue}, borrowable_foo_(lock_, foo_) {}
+
+ void SetUp() override {
+ EXPECT_FALSE(lock_.locked()); // Ensure it's not locked on construction.
+ }
+
+ struct Foo {
+ int value;
+ };
+ Lock lock_;
+ Foo foo_;
+ Borrowable<Foo&, Lock> borrowable_foo_;
+};
+
+class BasicLockable {
+ public:
+ void lock() {
+ PW_CHECK(!locked_, "Recursive lock detected");
+ locked_ = true;
+ }
+
+ void unlock() {
+ PW_CHECK(locked_, "Unlock while unlocked detected");
+ locked_ = false;
+ }
+
+ bool locked() const { return locked_; }
+
+ protected:
+ bool locked_ = false;
+};
+
+using BorrowableBasicLockableTest = BorrowableTest<BasicLockable>;
+
+TEST_F(BorrowableBasicLockableTest, Acquire) {
+ {
+ BorrowedPointer<Foo, BasicLockable> borrowed_foo =
+ borrowable_foo_.acquire();
+ EXPECT_TRUE(lock_.locked()); // Ensure the lock is held.
+ EXPECT_EQ(borrowed_foo->value, kInitialValue);
+ borrowed_foo->value = 13;
+ }
+ EXPECT_FALSE(lock_.locked()); // Ensure the lock is released.
+ EXPECT_EQ(foo_.value, 13);
+}
+
+TEST_F(BorrowableBasicLockableTest, RepeatedAcquire) {
+ {
+ BorrowedPointer<Foo, BasicLockable> borrowed_foo =
+ borrowable_foo_.acquire();
+ EXPECT_TRUE(lock_.locked()); // Ensure the lock is held.
+ EXPECT_EQ(borrowed_foo->value, kInitialValue);
+ borrowed_foo->value = 13;
+ }
+ EXPECT_FALSE(lock_.locked()); // Ensure the lock is released.
+ {
+ BorrowedPointer<Foo, BasicLockable> borrowed_foo =
+ borrowable_foo_.acquire();
+ EXPECT_TRUE(lock_.locked()); // Ensure the lock is held.
+ EXPECT_EQ(borrowed_foo->value, 13);
+ }
+ EXPECT_FALSE(lock_.locked()); // Ensure the lock is released.
+}
+
+TEST_F(BorrowableBasicLockableTest, Moveable) {
+ Borrowable<Foo&, BasicLockable> borrowable_foo = std::move(borrowable_foo_);
+ {
+ BorrowedPointer<Foo, BasicLockable> borrowed_foo = borrowable_foo.acquire();
+ EXPECT_TRUE(lock_.locked()); // Ensure the lock is held.
+ EXPECT_EQ(borrowed_foo->value, kInitialValue);
+ borrowed_foo->value = 13;
+ }
+ EXPECT_FALSE(lock_.locked()); // Ensure the lock is released.
+}
+
+TEST_F(BorrowableBasicLockableTest, Copyable) {
+ const Borrowable<Foo&, BasicLockable>& other = borrowable_foo_;
+ Borrowable<Foo&, BasicLockable> borrowable_foo(other);
+ {
+ BorrowedPointer<Foo, BasicLockable> borrowed_foo = borrowable_foo.acquire();
+ EXPECT_TRUE(lock_.locked()); // Ensure the lock is held.
+ EXPECT_EQ(borrowed_foo->value, kInitialValue);
+ borrowed_foo->value = 13;
+ }
+ EXPECT_FALSE(lock_.locked()); // Ensure the lock is released.
+}
+
+class Lockable : public BasicLockable {
+ public:
+ bool try_lock() {
+ if (locked()) {
+ return false;
+ }
+ locked_ = true;
+ return true;
+ }
+};
+
+using BorrowableLockableTest = BorrowableTest<Lockable>;
+
+TEST_F(BorrowableLockableTest, Acquire) {
+ {
+ BorrowedPointer<Foo, Lockable> borrowed_foo = borrowable_foo_.acquire();
+ EXPECT_TRUE(lock_.locked()); // Ensure the lock is held.
+ EXPECT_EQ(borrowed_foo->value, kInitialValue);
+ borrowed_foo->value = 13;
+ }
+ EXPECT_FALSE(lock_.locked()); // Ensure the lock is released.
+ EXPECT_EQ(foo_.value, 13);
+}
+
+TEST_F(BorrowableLockableTest, RepeatedAcquire) {
+ {
+ BorrowedPointer<Foo, Lockable> borrowed_foo = borrowable_foo_.acquire();
+ EXPECT_TRUE(lock_.locked()); // Ensure the lock is held.
+ EXPECT_EQ(borrowed_foo->value, kInitialValue);
+ borrowed_foo->value = 13;
+ }
+ EXPECT_FALSE(lock_.locked()); // Ensure the lock is released.
+ {
+ BorrowedPointer<Foo, Lockable> borrowed_foo = borrowable_foo_.acquire();
+ EXPECT_TRUE(lock_.locked()); // Ensure the lock is held.
+ EXPECT_EQ(borrowed_foo->value, 13);
+ }
+ EXPECT_FALSE(lock_.locked()); // Ensure the lock is released.
+}
+
+TEST_F(BorrowableLockableTest, TryAcquireSuccess) {
+ {
+ std::optional<BorrowedPointer<Foo, Lockable>> maybe_borrowed_foo =
+ borrowable_foo_.try_acquire();
+ ASSERT_TRUE(maybe_borrowed_foo.has_value());
+ EXPECT_TRUE(lock_.locked()); // Ensure the lock is held.
+ EXPECT_EQ(maybe_borrowed_foo.value()->value, kInitialValue);
+ }
+ EXPECT_FALSE(lock_.locked()); // Ensure the lock is released.
+}
+
+TEST_F(BorrowableLockableTest, TryAcquireFailure) {
+ lock_.lock();
+ EXPECT_TRUE(lock_.locked());
+ {
+ std::optional<BorrowedPointer<Foo, Lockable>> maybe_borrowed_foo =
+ borrowable_foo_.try_acquire();
+ EXPECT_FALSE(maybe_borrowed_foo.has_value());
+ }
+ EXPECT_TRUE(lock_.locked());
+ lock_.unlock();
+}
+
+struct Clock {
+ using rep = int64_t;
+ using period = std::micro;
+ using duration = std::chrono::duration<rep, period>;
+ using time_point = std::chrono::time_point<Clock>;
+};
+
+class TimedLockable : public Lockable {
+ public:
+ bool try_lock() {
+ if (locked()) {
+ return false;
+ }
+ locked_ = true;
+ return true;
+ }
+
+ bool try_lock_for(const Clock::duration&) { return try_lock(); }
+ bool try_lock_until(const Clock::time_point&) { return try_lock(); }
+};
+
+using BorrowableTimedLockableTest = BorrowableTest<TimedLockable>;
+
+TEST_F(BorrowableTimedLockableTest, Acquire) {
+ {
+ BorrowedPointer<Foo, TimedLockable> borrowed_foo =
+ borrowable_foo_.acquire();
+ EXPECT_TRUE(lock_.locked()); // Ensure the lock is held.
+ EXPECT_EQ(borrowed_foo->value, kInitialValue);
+ borrowed_foo->value = 13;
+ }
+ EXPECT_FALSE(lock_.locked()); // Ensure the lock is released.
+ EXPECT_EQ(foo_.value, 13);
+}
+
+TEST_F(BorrowableTimedLockableTest, RepeatedAcquire) {
+ {
+ BorrowedPointer<Foo, TimedLockable> borrowed_foo =
+ borrowable_foo_.acquire();
+ EXPECT_TRUE(lock_.locked()); // Ensure the lock is held.
+ EXPECT_EQ(borrowed_foo->value, kInitialValue);
+ borrowed_foo->value = 13;
+ }
+ EXPECT_FALSE(lock_.locked()); // Ensure the lock is released.
+ {
+ BorrowedPointer<Foo, TimedLockable> borrowed_foo =
+ borrowable_foo_.acquire();
+ EXPECT_TRUE(lock_.locked()); // Ensure the lock is held.
+ EXPECT_EQ(borrowed_foo->value, 13);
+ }
+ EXPECT_FALSE(lock_.locked()); // Ensure the lock is released.
+}
+
+TEST_F(BorrowableTimedLockableTest, TryAcquireSuccess) {
+ {
+ std::optional<BorrowedPointer<Foo, TimedLockable>> maybe_borrowed_foo =
+ borrowable_foo_.try_acquire();
+ ASSERT_TRUE(maybe_borrowed_foo.has_value());
+ EXPECT_TRUE(lock_.locked()); // Ensure the lock is held.
+ EXPECT_EQ(maybe_borrowed_foo.value()->value, kInitialValue);
+ }
+ EXPECT_FALSE(lock_.locked()); // Ensure the lock is released.
+}
+
+TEST_F(BorrowableTimedLockableTest, TryAcquireFailure) {
+ lock_.lock();
+ EXPECT_TRUE(lock_.locked());
+ {
+ std::optional<BorrowedPointer<Foo, TimedLockable>> maybe_borrowed_foo =
+ borrowable_foo_.try_acquire();
+ EXPECT_FALSE(maybe_borrowed_foo.has_value());
+ }
+ EXPECT_TRUE(lock_.locked());
+ lock_.unlock();
+}
+
+TEST_F(BorrowableTimedLockableTest, TryAcquireForSuccess) {
+ {
+ std::optional<BorrowedPointer<Foo, TimedLockable>> maybe_borrowed_foo =
+ borrowable_foo_.try_acquire_for(std::chrono::seconds(0));
+ ASSERT_TRUE(maybe_borrowed_foo.has_value());
+ EXPECT_TRUE(lock_.locked()); // Ensure the lock is held.
+ EXPECT_EQ(maybe_borrowed_foo.value()->value, kInitialValue);
+ }
+ EXPECT_FALSE(lock_.locked()); // Ensure the lock is released.
+}
+
+TEST_F(BorrowableTimedLockableTest, TryAcquireForFailure) {
+ lock_.lock();
+ EXPECT_TRUE(lock_.locked());
+ {
+ std::optional<BorrowedPointer<Foo, TimedLockable>> maybe_borrowed_foo =
+ borrowable_foo_.try_acquire_for(std::chrono::seconds(0));
+ EXPECT_FALSE(maybe_borrowed_foo.has_value());
+ }
+ EXPECT_TRUE(lock_.locked());
+ lock_.unlock();
+}
+
+TEST_F(BorrowableTimedLockableTest, TryAcquireUntilSuccess) {
+ {
+ std::optional<BorrowedPointer<Foo, TimedLockable>> maybe_borrowed_foo =
+ borrowable_foo_.try_acquire_until(
+ Clock::time_point(std::chrono::seconds(0)));
+ ASSERT_TRUE(maybe_borrowed_foo.has_value());
+ EXPECT_TRUE(lock_.locked()); // Ensure the lock is held.
+ EXPECT_EQ(maybe_borrowed_foo.value()->value, kInitialValue);
+ }
+ EXPECT_FALSE(lock_.locked()); // Ensure the lock is released.
+}
+
+TEST_F(BorrowableTimedLockableTest, TryAcquireUntilFailure) {
+ lock_.lock();
+ EXPECT_TRUE(lock_.locked());
+ {
+ std::optional<BorrowedPointer<Foo, TimedLockable>> maybe_borrowed_foo =
+ borrowable_foo_.try_acquire_until(
+ Clock::time_point(std::chrono::seconds(0)));
+ EXPECT_FALSE(maybe_borrowed_foo.has_value());
+ }
+ EXPECT_TRUE(lock_.locked());
+ lock_.unlock();
+}
+
+} // namespace
+} // namespace pw::sync
diff --git a/pw_sync/docs.rst b/pw_sync/docs.rst
index 1308f00..7dcb97b 100644
--- a/pw_sync/docs.rst
+++ b/pw_sync/docs.rst
@@ -948,6 +948,218 @@
Documents functions that dynamically check to see if a lock is held, and fail
if it is not held.
+-----------------------------
+Critical Section Lock Helpers
+-----------------------------
+
+Borrowable
+==========
+The Borrowable is a helper construct that enables callers to borrow an object
+which is guarded by a lock, enabling a containerized style of external locking.
+
+Users who need access to the guarded object can ask to acquire a
+``BorrowedPointer`` which permits access while the lock is held.
+
+This class is compatible with locks which comply with
+`BasicLockable <https://en.cppreference.com/w/cpp/named_req/BasicLockable>`_,
+`Lockable <https://en.cppreference.com/w/cpp/named_req/Lockable>`_, and
+`TimedLockable <https://en.cppreference.com/w/cpp/named_req/TimedLockable>`_
+C++ named requirements.
+
+External vs Internal locking
+----------------------------
+Before we explain why Borrowable is useful, it's important to understand the
+trade-offs when deciding on using internal and/or external locking.
+
+Internal locking is when the lock is hidden from the caller entirely and is used
+internally to the API. For example:
+
+.. code-block:: cpp
+
+ class BankAccount {
+ public:
+ void Deposit(int amount) {
+ std::lock_guard lock(mutex_);
+ balance_ += amount;
+ }
+
+ void Withdraw(int amount) {
+ std::lock_guard lock(mutex_);
+ balance_ -= amount;
+ }
+
+ void Balance() const {
+ std::lock_guard lock(mutex_);
+ return balance_;
+ }
+
+ private:
+ int balance_ PW_GUARDED_BY(mutex_);
+ pw::sync::Mutex mutex_;
+ };
+
+Internal locking guarantees that any concurrent calls to its public member
+functions don't corrupt an instance of that class. This is typically ensured by
+having each member function acquire a lock on the object upon entry. This way,
+for any instance, there can only be one member function call active at any
+moment, serializing the operations.
+
+One common issue that pops up is that member functions may have to call other
+member functions which also require locks. This typically results in a
+duplication of the public API into an internal mirror where the lock is already
+held. This along with having to modify every thread-safe public member function
+may results in an increased code size.
+
+However, with the per-method locking approach, it is not possible to perform a
+multi-method thread-safe transaction. For example, what if we only wanted to
+withdraw money if the balance was high enough? With the current API there would
+be a risk that money is withdrawn after we've checked the balance.
+
+This is usually why external locking is used. This is when the lock is exposed
+to the caller and may be used externally to the public API. External locking
+can take may forms which may even include mixing internal and external locking.
+In its most simplistic form it is an external lock used along side each
+instance, e.g.:
+
+.. code-block:: cpp
+
+ class BankAccount {
+ public:
+ void Deposit(int amount) {
+ balance_ += amount;
+ }
+
+ void Withdraw(int amount) {
+ balance_ -= amount;
+ }
+
+ void Balance() const {
+ return balance_;
+ }
+
+ private:
+ int balance_;
+ };
+
+ pw::sync::Mutex bobs_account_mutex;
+ BankAccount bobs_account PW_GUARDED_BY(bobs_account_mutex);
+
+The lock is acquired before the bank account is used for a transaction. In
+addition, we do not have to modify every public function and its trivial to
+call other public member functions from a public member function. However, as
+you can imagine instantiating and passing around the instances and their locks
+can become error prone.
+
+This is why ``Borrowable`` exists.
+
+Why use Borrowable?
+-------------------
+``Borrowable`` offers code-size efficient way to enable external locking that is
+easy and safe to use. It is effectively a container which holds references to a
+protected instance and its lock which provides RAII-style access.
+
+.. code-block:: cpp
+
+ pw::sync::Mutex bobs_account_mutex;
+ BankAccount bobs_account PW_GUARDED_BY(bobs_account_mutex);
+ pw::sync::Borrowable<BankAccount&, pw::sync::Mutex> bobs_acount(
+ bobs_account_mutex, bobs_account);
+
+This construct is useful when sharing objects or data which are transactional in
+nature where making individual operations threadsafe is insufficient. See the
+section on internal vs external locking tradeoffs above.
+
+It can also offer a code-size and stack-usage efficient way to separate timeout
+constraints between the acquiring of the shared object and timeouts used for the
+shared object's API. For example, imagine you have an I2c bus which is used by
+several threads and you'd like to specify an ACK timeout of 50ms. It'd be ideal
+if the duration it takes to gain exclusive access to the I2c bus does not eat
+into the ACK timeout you'd like to use for the transaction. Borrowable can help
+you do exactly this if you provide access to the I2c bus through a
+``Borrowable``.
+
+C++
+---
+.. cpp:class:: template <typename GuardedType, typename Lock> pw::sync::BorrowedPointer
+
+ The BorrowedPointer is an RAII handle which wraps a pointer to a borrowed
+ object along with a held lock which is guarding the object. When destroyed,
+ the lock is released.
+
+ This object is moveable, but not copyable.
+
+ .. cpp:function:: GuardedType* operator->()
+
+ Provides access to the borrowed object's members.
+
+ .. cpp:function:: GuardedType& operator*()
+
+ Provides access to the borrowed object directly.
+
+ **Warning:** The member of pointer member access operator, operator->(), is
+ recommended over this API as this is prone to leaking references. However,
+ this is sometimes necessary.
+
+ **Warning:** Be careful not to leak references to the borrowed object.
+
+.. cpp:class:: template <typename GuardedReference, typename Lock> pw::sync::Borrowable
+
+ .. cpp:function:: BorrowedPointer<GuardedType, Lock> acquire()
+
+ Blocks indefinitely until the object can be borrowed. Failures are fatal.
+
+ .. cpp:function:: std::optional<BorrowedPointer<GuardedType, Lock>> try_acquire()
+
+ Tries to borrow the object in a non-blocking manner. Returns a
+ BorrowedPointer on success, otherwise std::nullopt (nothing).
+
+ .. cpp:function:: template <class Rep, class Period> std::optional<BorrowedPointer<GuardedType, Lock>> try_acquire_for(std::chrono::duration<Rep, Period> timeout)
+
+ Tries to borrow the object. Blocks until the specified timeout has elapsed
+ or the object has been borrowed, whichever comes first. Returns a
+ BorrowedPointer on success, otherwise std::nullopt (nothing).
+
+ .. cpp:function:: template <class Rep, class Period> std::optional<BorrowedPointer<GuardedType, Lock>> try_acquire_until(std::chrono::duration<Rep, Period> deadline)
+
+ Tries to borrow the object. Blocks until the specified deadline has been
+ reached or the object has been borrowed, whichever comes first. Returns a
+ BorrowedPointer on success, otherwise std::nullopt (nothing).
+
+Example in C++
+^^^^^^^^^^^^^^
+
+.. code-block:: cpp
+
+ #include <chrono>
+
+ #include "pw_bytes/span.h"
+ #include "pw_i2c/initiator.h"
+ #include "pw_status/try.h"
+ #include "pw_status/result.h"
+ #include "pw_sync/borrow.h"
+ #include "pw_sync/mutex.h"
+
+ class ExampleI2c : public pw::i2c::Initiator;
+
+ pw::sync::Mutex i2c_mutex;
+ ExampleI2c i2c;
+ pw::sync::Borrowable<ExampleI2c&, pw::sync::Mutex> borrowable_i2c(
+ i2c_mutex, i2c);
+
+ pw::Result<ConstByteSpan> ReadI2cData(ByteSpan buffer) {
+ // Block indefinitely waiting to borrow the i2c bus.
+ pw::sync::BorrowedPointer<ExampleI2c, pw::sync::Mutex> borrowed_i2c =
+ borrowable_i2c.acquire();
+
+ // Execute a sequence of transactions to get the needed data.
+ PW_TRY(borrowed_i2c->WriteFor(kFirstWrite, std::chrono::milliseconds(50)));
+ PW_TRY(borrowed_i2c->WriteReadFor(kSecondWrite, buffer,
+ std::chrono::milliseconds(10)));
+
+ // Borrowed i2c pointer is returned when the scope exits.
+ return buffer;
+ }
+
--------------------
Signaling Primitives
--------------------
diff --git a/pw_sync/public/pw_sync/borrow.h b/pw_sync/public/pw_sync/borrow.h
new file mode 100644
index 0000000..4ae02ed
--- /dev/null
+++ b/pw_sync/public/pw_sync/borrow.h
@@ -0,0 +1,152 @@
+// Copyright 2021 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+#pragma once
+
+#include <chrono>
+#include <mutex>
+#include <optional>
+#include <type_traits>
+
+#include "pw_assert/assert.h"
+
+namespace pw::sync {
+
+// The BorrowedPointer is an RAII handle which wraps a pointer to a borrowed
+// object along with a held lock which is guarding the object. When destroyed,
+// the lock is released.
+template <typename GuardedType, typename Lock>
+class BorrowedPointer {
+ public:
+ // Release the lock on destruction.
+ ~BorrowedPointer() = default;
+
+ // This object is moveable, but not copyable.
+ //
+ // Postcondition: The other BorrowedPointer is no longer valid and will assert
+ // if the GuardedType is accessed.
+ BorrowedPointer(BorrowedPointer&& other)
+ : unique_lock_(std::move(other.unique_lock_)), object_(other.object_) {
+ other.object_ = nullptr;
+ }
+ BorrowedPointer& operator=(BorrowedPointer&& other) {
+ unique_lock_ = std::move(other.unique_lock_);
+ object_ = other.object_;
+ other.object_ = nullptr;
+ return *this;
+ }
+ BorrowedPointer(const BorrowedPointer&) = delete;
+ BorrowedPointer& operator=(const BorrowedPointer&) = delete;
+
+ // Provides access to the borrowed object's members.
+ GuardedType* operator->() {
+ PW_ASSERT(object_ != nullptr); // Ensure this isn't a stale moved instance.
+ return object_;
+ }
+
+ // Provides access to the borrowed object directly.
+ //
+ // NOTE: The member of pointer member access operator, operator->(), is
+ // recommended over this API as this is prone to leaking references. However,
+ // this is sometimes necessary.
+ //
+ // WARNING: Be careful not to leak references to the borrowed object!
+ GuardedType& operator*() {
+ PW_ASSERT(object_ != nullptr); // Ensure this isn't a stale moved instance.
+ return *object_;
+ }
+
+ private:
+ // Allow BorrowedPointer creation inside of Borrowable's acquire methods.
+ template <typename G, typename L>
+ friend class Borrowable;
+
+ BorrowedPointer(std::unique_lock<Lock> unique_lock, GuardedType* object)
+ : unique_lock_(std::move(unique_lock)), object_(object) {}
+
+ std::unique_lock<Lock> unique_lock_;
+ GuardedType* object_;
+};
+
+// The Borrowable is a helper construct that enables callers to borrow an object
+// which is guarded by a lock.
+//
+// Users who need access to the guarded object can ask to acquire a
+// BorrowedPointer which permits access while the lock is held.
+//
+// This class is compatible with locks which comply with BasicLockable,
+// Lockable, and TimedLockable C++ named requirements.
+template <typename GuardedReference, typename Lock>
+class Borrowable {
+ public:
+ static_assert(std::is_reference<GuardedReference>::value,
+ "GuardedReference must be a reference type");
+
+ using guarded_type = typename std::remove_reference<GuardedReference>::type;
+
+ constexpr Borrowable(Lock& lock, GuardedReference object)
+ : lock_(&lock), object_(&object) {}
+
+ Borrowable(const Borrowable&) = default;
+ Borrowable& operator=(const Borrowable&) = default;
+ Borrowable(Borrowable&& other) = default;
+ Borrowable& operator=(Borrowable&& other) = default;
+
+ // Blocks indefinitely until the object can be borrowed. Failures are fatal.
+ BorrowedPointer<guarded_type, Lock> acquire() {
+ std::unique_lock unique_lock(*lock_);
+ return BorrowedPointer<guarded_type, Lock>(std::move(unique_lock), object_);
+ }
+
+ // Tries to borrow the object in a non-blocking manner. Returns a
+ // BorrowedPointer on success, otherwise std::nullopt (nothing).
+ std::optional<BorrowedPointer<guarded_type, Lock>> try_acquire() {
+ std::unique_lock unique_lock(*lock_, std::defer_lock);
+ if (!unique_lock.try_lock()) {
+ return std::nullopt;
+ }
+ return BorrowedPointer<guarded_type, Lock>(std::move(unique_lock), object_);
+ }
+
+ // Tries to borrow the object. Blocks until the specified timeout has elapsed
+ // or the object has been borrowed, whichever comes first. Returns a
+ // BorrowedPointer on success, otherwise std::nullopt (nothing).
+ template <class Rep, class Period>
+ std::optional<BorrowedPointer<guarded_type, Lock>> try_acquire_for(
+ std::chrono::duration<Rep, Period> timeout) {
+ std::unique_lock unique_lock(*lock_, std::defer_lock);
+ if (!unique_lock.try_lock_for(timeout)) {
+ return std::nullopt;
+ }
+ return BorrowedPointer<guarded_type, Lock>(std::move(unique_lock), object_);
+ }
+
+ // Tries to borrow the object. Blocks until the specified deadline has passed
+ // or the object has been borrowed, whichever comes first. Returns a
+ // BorrowedPointer on success, otherwise std::nullopt (nothing).
+ template <class Clock, class Duration>
+ std::optional<BorrowedPointer<guarded_type, Lock>> try_acquire_until(
+ std::chrono::time_point<Clock, Duration> deadline) {
+ std::unique_lock unique_lock(*lock_, std::defer_lock);
+ if (!unique_lock.try_lock_until(deadline)) {
+ return std::nullopt;
+ }
+ return BorrowedPointer<guarded_type, Lock>(std::move(unique_lock), object_);
+ }
+
+ private:
+ Lock* lock_;
+ guarded_type* object_;
+};
+
+} // namespace pw::sync