pw_sync: adds VirtualBasicLockable interface
Adds a virtual lock interface in case the lock selection cannot
be templated.
Change-Id: I79f3f28c28572e970ddf4002477e0ea2c6ee02ab
Reviewed-on: https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/58202
Pigweed-Auto-Submit: Ewout van Bekkum <ewout@google.com>
Commit-Queue: Ewout van Bekkum <ewout@google.com>
Reviewed-by: Keir Mierle <keir@google.com>
diff --git a/pw_sync/BUILD.bazel b/pw_sync/BUILD.bazel
index c8cbe1c..d395863 100644
--- a/pw_sync/BUILD.bazel
+++ b/pw_sync/BUILD.bazel
@@ -115,10 +115,23 @@
],
includes = ["public"],
deps = [
+ ":virtual_basic_lockable",
"//pw_assert",
],
)
+pw_cc_library(
+ name = "virtual_basic_lockable",
+ hdrs = [
+ "public/pw_sync/virtual_basic_lockable.h",
+ ],
+ includes = ["public"],
+ deps = [
+ ":lock_annotations",
+ "//pw_polyfill",
+ ],
+)
+
pw_cc_facade(
name = "mutex_facade",
hdrs = [
@@ -127,6 +140,7 @@
includes = ["public"],
deps = [
":lock_annotations",
+ ":virtual_basic_lockable",
"//pw_preprocessor",
],
)
@@ -163,6 +177,7 @@
deps = [
":lock_annotations",
":mutex_facade",
+ ":virtual_basic_lockable",
"//pw_chrono:system_clock",
"//pw_preprocessor",
],
@@ -176,6 +191,7 @@
deps = [
":mutex",
":timed_mutex_facade",
+ ":virtual_basic_lockable",
"@pigweed_config//:pw_sync_timed_mutex_backend",
],
)
@@ -200,6 +216,7 @@
includes = ["public"],
deps = [
":lock_annotations",
+ ":virtual_basic_lockable",
"//pw_preprocessor",
],
)
@@ -350,6 +367,7 @@
],
deps = [
":borrow",
+ ":virtual_basic_lockable",
"//pw_assert",
"//pw_unit_test",
],
diff --git a/pw_sync/BUILD.gn b/pw_sync/BUILD.gn
index 67d0448..16554c1 100644
--- a/pw_sync/BUILD.gn
+++ b/pw_sync/BUILD.gn
@@ -62,7 +62,19 @@
pw_source_set("borrow") {
public_configs = [ ":public_include_path" ]
public = [ "public/pw_sync/borrow.h" ]
- public_deps = [ dir_pw_assert ]
+ public_deps = [
+ ":virtual_basic_lockable",
+ dir_pw_assert,
+ ]
+}
+
+pw_source_set("virtual_basic_lockable") {
+ public_configs = [ ":public_include_path" ]
+ public = [ "public/pw_sync/virtual_basic_lockable.h" ]
+ public_deps = [
+ ":lock_annotations",
+ dir_pw_polyfill,
+ ]
}
pw_facade("mutex") {
@@ -71,6 +83,7 @@
public = [ "public/pw_sync/mutex.h" ]
public_deps = [
":lock_annotations",
+ ":virtual_basic_lockable",
"$dir_pw_preprocessor",
]
sources = [ "mutex.cc" ]
@@ -82,6 +95,7 @@
public = [ "public/pw_sync/timed_mutex.h" ]
public_deps = [
":mutex",
+ ":virtual_basic_lockable",
"$dir_pw_chrono:system_clock",
"$dir_pw_preprocessor",
]
@@ -94,6 +108,7 @@
public = [ "public/pw_sync/interrupt_spin_lock.h" ]
public_deps = [
":lock_annotations",
+ ":virtual_basic_lockable",
"$dir_pw_preprocessor",
]
sources = [ "interrupt_spin_lock.cc" ]
@@ -174,6 +189,7 @@
sources = [ "borrow_test.cc" ]
deps = [
":borrow",
+ ":virtual_basic_lockable",
dir_pw_assert,
]
}
diff --git a/pw_sync/CMakeLists.txt b/pw_sync/CMakeLists.txt
index 69c553e..d7e3af7 100644
--- a/pw_sync/CMakeLists.txt
+++ b/pw_sync/CMakeLists.txt
@@ -19,5 +19,6 @@
mutex.cc
PUBLIC_DEPS
pw_chrono.system_clock
+ pw_polyfill
pw_preprocessor
)
diff --git a/pw_sync/borrow_test.cc b/pw_sync/borrow_test.cc
index 4782d78..adec2af 100644
--- a/pw_sync/borrow_test.cc
+++ b/pw_sync/borrow_test.cc
@@ -19,6 +19,7 @@
#include "gtest/gtest.h"
#include "pw_assert/check.h"
+#include "pw_sync/virtual_basic_lockable.h"
namespace pw::sync {
namespace {
@@ -43,22 +44,30 @@
Borrowable<Foo&, Lock> borrowable_foo_;
};
-class BasicLockable {
+class BasicLockable : public VirtualBasicLockable {
public:
- void lock() {
- PW_CHECK(!locked_, "Recursive lock detected");
- locked_ = true;
- }
-
- void unlock() {
- PW_CHECK(locked_, "Unlock while unlocked detected");
- locked_ = false;
- }
+ virtual ~BasicLockable() = default;
bool locked() const { return locked_; }
protected:
bool locked_ = false;
+
+ private:
+ void DoLockOperation(Operation operation) override {
+ switch (operation) {
+ case Operation::kLock:
+ PW_CHECK(!locked_, "Recursive lock detected");
+ locked_ = true;
+ return;
+
+ case Operation::kUnlock:
+ default:
+ PW_CHECK(locked_, "Unlock while unlocked detected");
+ locked_ = false;
+ return;
+ }
+ }
};
using BorrowableBasicLockableTest = BorrowableTest<BasicLockable>;
diff --git a/pw_sync/docs.rst b/pw_sync/docs.rst
index 7dcb97b..fd2bef8 100644
--- a/pw_sync/docs.rst
+++ b/pw_sync/docs.rst
@@ -952,6 +952,34 @@
Critical Section Lock Helpers
-----------------------------
+Virtual Lock Interfaces
+=======================
+Virtual lock interfaces can be useful when lock selection cannot be templated.
+
+Why use virtual locks?
+----------------------
+Virtual locks enable depending on locks without templating implementation code
+on the type, while retaining flexibility with respect to the concrete lock type.
+Pigweed tries to avoid pushing policy on to users, and virtual locks are one way
+to accomplish that without templating everything.
+
+A case when virtual locks are useful is when the concrete lock type changes at
+run time. For example, access to flash may be protected at run time by an
+internal mutex, however at crash time we may want to switch to a no-op lock. A
+virtual lock interface could be used here to minimize the code-size cost that
+would occur otherwise if the flash driver were templated.
+
+VirtualBasicLock
+----------------
+The ``VirtualBasicLock`` interface meets the
+`BasicLockable <https://en.cppreference.com/w/cpp/named_req/BasicLockable>`_ C++
+named requirement. Our critical section lock primitives offer optional virtual
+versions, including:
+
+* ``pw::sync::VirtualMutex``
+* ``pw::sync::VirtualTimedMutex``
+* ``pw::sync::VirtualInterruptSpinLock``
+
Borrowable
==========
The Borrowable is a helper construct that enables callers to borrow an object
@@ -966,6 +994,9 @@
`TimedLockable <https://en.cppreference.com/w/cpp/named_req/TimedLockable>`_
C++ named requirements.
+By default the selected lock type is a ``pw::sync::VirtualBasicLockable``. If
+this virtual interface is used, the templated lock parameter can be skipped.
+
External vs Internal locking
----------------------------
Before we explain why Borrowable is useful, it's important to understand the
@@ -1080,7 +1111,7 @@
C++
---
-.. cpp:class:: template <typename GuardedType, typename Lock> pw::sync::BorrowedPointer
+.. cpp:class:: template <typename GuardedType, typename Lock = pw::sync::VirtualBasicLockable> 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,
@@ -1102,7 +1133,7 @@
**Warning:** Be careful not to leak references to the borrowed object.
-.. cpp:class:: template <typename GuardedReference, typename Lock> pw::sync::Borrowable
+.. cpp:class:: template <typename GuardedReference, typename Lock = pw::sync::VirtualBasicLockable> pw::sync::Borrowable
.. cpp:function:: BorrowedPointer<GuardedType, Lock> acquire()
@@ -1141,14 +1172,13 @@
class ExampleI2c : public pw::i2c::Initiator;
- pw::sync::Mutex i2c_mutex;
+ pw::sync::VirtualMutex i2c_mutex;
ExampleI2c i2c;
- pw::sync::Borrowable<ExampleI2c&, pw::sync::Mutex> borrowable_i2c(
- i2c_mutex, i2c);
+ pw::sync::Borrowable<ExampleI2c&> 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 =
+ pw::sync::BorrowedPointer<ExampleI2c> borrowed_i2c =
borrowable_i2c.acquire();
// Execute a sequence of transactions to get the needed data.
diff --git a/pw_sync/interrupt_spin_lock_facade_test.cc b/pw_sync/interrupt_spin_lock_facade_test.cc
index 0d8a059..880ac81 100644
--- a/pw_sync/interrupt_spin_lock_facade_test.cc
+++ b/pw_sync/interrupt_spin_lock_facade_test.cc
@@ -58,6 +58,22 @@
}
}
+TEST(VirtualInterruptSpinLock, LockUnlock) {
+ pw::sync::VirtualInterruptSpinLock interrupt_spin_lock;
+ interrupt_spin_lock.lock();
+ // TODO(pwbug/291): Ensure it fails to lock when already held.
+ // EXPECT_FALSE(interrupt_spin_lock.try_lock());
+ interrupt_spin_lock.unlock();
+}
+
+VirtualInterruptSpinLock static_virtual_interrupt_spin_lock;
+TEST(VirtualInterruptSpinLock, LockUnlockStatic) {
+ static_virtual_interrupt_spin_lock.lock();
+ // TODO(pwbug/291): Ensure it fails to lock when already held.
+ // EXPECT_FALSE(static_virtual_interrupt_spin_lock.try_lock());
+ static_virtual_interrupt_spin_lock.unlock();
+}
+
TEST(InterruptSpinLock, LockUnlockInC) {
pw::sync::InterruptSpinLock interrupt_spin_lock;
pw_sync_InterruptSpinLock_CallLock(&interrupt_spin_lock);
diff --git a/pw_sync/mutex_facade_test.cc b/pw_sync/mutex_facade_test.cc
index 6788536..771b003 100644
--- a/pw_sync/mutex_facade_test.cc
+++ b/pw_sync/mutex_facade_test.cc
@@ -58,6 +58,22 @@
}
}
+TEST(VirtualMutex, LockUnlock) {
+ pw::sync::VirtualMutex mutex;
+ mutex.lock();
+ // TODO(pwbug/291): Ensure it fails to lock when already held.
+ // EXPECT_FALSE(mutex.try_lock());
+ mutex.unlock();
+}
+
+VirtualMutex static_virtual_mutex;
+TEST(VirtualMutex, LockUnlockStatic) {
+ static_virtual_mutex.lock();
+ // TODO(pwbug/291): Ensure it fails to lock when already held.
+ // EXPECT_FALSE(static_virtual_mutex.try_lock());
+ static_virtual_mutex.unlock();
+}
+
TEST(Mutex, LockUnlockInC) {
pw::sync::Mutex mutex;
pw_sync_Mutex_CallLock(&mutex);
diff --git a/pw_sync/public/pw_sync/borrow.h b/pw_sync/public/pw_sync/borrow.h
index 4ae02ed..038df5f 100644
--- a/pw_sync/public/pw_sync/borrow.h
+++ b/pw_sync/public/pw_sync/borrow.h
@@ -19,13 +19,14 @@
#include <type_traits>
#include "pw_assert/assert.h"
+#include "pw_sync/virtual_basic_lockable.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>
+template <typename GuardedType, typename Lock = pw::sync::VirtualBasicLockable>
class BorrowedPointer {
public:
// Release the lock on destruction.
@@ -86,7 +87,8 @@
//
// This class is compatible with locks which comply with BasicLockable,
// Lockable, and TimedLockable C++ named requirements.
-template <typename GuardedReference, typename Lock>
+template <typename GuardedReference,
+ typename Lock = pw::sync::VirtualBasicLockable>
class Borrowable {
public:
static_assert(std::is_reference<GuardedReference>::value,
diff --git a/pw_sync/public/pw_sync/interrupt_spin_lock.h b/pw_sync/public/pw_sync/interrupt_spin_lock.h
index a8c7674..d9b75f7 100644
--- a/pw_sync/public/pw_sync/interrupt_spin_lock.h
+++ b/pw_sync/public/pw_sync/interrupt_spin_lock.h
@@ -20,6 +20,7 @@
#ifdef __cplusplus
+#include "pw_sync/virtual_basic_lockable.h"
#include "pw_sync_backend/interrupt_spin_lock_native.h"
namespace pw::sync {
@@ -79,6 +80,34 @@
backend::NativeInterruptSpinLock native_type_;
};
+class PW_LOCKABLE("pw::sync::VirtualInterruptSpinLock")
+ VirtualInterruptSpinLock final : public VirtualBasicLockable {
+ public:
+ VirtualInterruptSpinLock() = default;
+
+ VirtualInterruptSpinLock(const VirtualInterruptSpinLock&) = delete;
+ VirtualInterruptSpinLock(VirtualInterruptSpinLock&&) = delete;
+ VirtualInterruptSpinLock& operator=(const VirtualInterruptSpinLock&) = delete;
+ VirtualInterruptSpinLock& operator=(VirtualInterruptSpinLock&&) = delete;
+
+ InterruptSpinLock& interrupt_spin_lock() { return interrupt_spin_lock_; }
+
+ private:
+ void DoLockOperation(Operation operation) override
+ PW_NO_LOCK_SAFETY_ANALYSIS {
+ switch (operation) {
+ case Operation::kLock:
+ return interrupt_spin_lock_.lock();
+
+ case Operation::kUnlock:
+ default:
+ return interrupt_spin_lock_.unlock();
+ }
+ }
+
+ InterruptSpinLock interrupt_spin_lock_;
+};
+
} // namespace pw::sync
#include "pw_sync_backend/interrupt_spin_lock_inline.h"
diff --git a/pw_sync/public/pw_sync/mutex.h b/pw_sync/public/pw_sync/mutex.h
index a2da0f7..622e40f 100644
--- a/pw_sync/public/pw_sync/mutex.h
+++ b/pw_sync/public/pw_sync/mutex.h
@@ -20,6 +20,7 @@
#ifdef __cplusplus
+#include "pw_sync/virtual_basic_lockable.h"
#include "pw_sync_backend/mutex_native.h"
namespace pw::sync {
@@ -73,6 +74,34 @@
backend::NativeMutex native_type_;
};
+class PW_LOCKABLE("pw::sync::VirtualMutex") VirtualMutex final
+ : public VirtualBasicLockable {
+ public:
+ VirtualMutex() = default;
+
+ VirtualMutex(const VirtualMutex&) = delete;
+ VirtualMutex(VirtualMutex&&) = delete;
+ VirtualMutex& operator=(const VirtualMutex&) = delete;
+ VirtualMutex& operator=(VirtualMutex&&) = delete;
+
+ Mutex& mutex() { return mutex_; }
+
+ private:
+ void DoLockOperation(Operation operation) override
+ PW_NO_LOCK_SAFETY_ANALYSIS {
+ switch (operation) {
+ case Operation::kLock:
+ return mutex_.lock();
+
+ case Operation::kUnlock:
+ default:
+ return mutex_.unlock();
+ }
+ }
+
+ Mutex mutex_;
+};
+
} // namespace pw::sync
#include "pw_sync_backend/mutex_inline.h"
diff --git a/pw_sync/public/pw_sync/timed_mutex.h b/pw_sync/public/pw_sync/timed_mutex.h
index a7fac1a..eb03284 100644
--- a/pw_sync/public/pw_sync/timed_mutex.h
+++ b/pw_sync/public/pw_sync/timed_mutex.h
@@ -22,6 +22,8 @@
#ifdef __cplusplus
+#include "pw_sync/virtual_basic_lockable.h"
+
namespace pw::sync {
// The TimedMutex is a synchronization primitive that can be used to protect
@@ -65,6 +67,34 @@
PW_EXCLUSIVE_TRYLOCK_FUNCTION(true);
};
+class PW_LOCKABLE("pw::sync::VirtualTimedMutex") VirtualTimedMutex final
+ : public VirtualBasicLockable {
+ public:
+ VirtualTimedMutex() = default;
+
+ VirtualTimedMutex(const VirtualTimedMutex&) = delete;
+ VirtualTimedMutex(VirtualTimedMutex&&) = delete;
+ VirtualTimedMutex& operator=(const VirtualTimedMutex&) = delete;
+ VirtualTimedMutex& operator=(VirtualTimedMutex&&) = delete;
+
+ TimedMutex& timed_mutex() { return timed_mutex_; }
+
+ private:
+ void DoLockOperation(Operation operation) override
+ PW_NO_LOCK_SAFETY_ANALYSIS {
+ switch (operation) {
+ case Operation::kLock:
+ return timed_mutex_.lock();
+
+ case Operation::kUnlock:
+ default:
+ return timed_mutex_.unlock();
+ }
+ }
+
+ TimedMutex timed_mutex_;
+};
+
} // namespace pw::sync
#include "pw_sync_backend/timed_mutex_inline.h"
diff --git a/pw_sync/public/pw_sync/virtual_basic_lockable.h b/pw_sync/public/pw_sync/virtual_basic_lockable.h
new file mode 100644
index 0000000..4593e07
--- /dev/null
+++ b/pw_sync/public/pw_sync/virtual_basic_lockable.h
@@ -0,0 +1,73 @@
+// 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 "pw_polyfill/language_feature_macros.h"
+#include "pw_sync/lock_annotations.h"
+
+namespace pw::sync {
+
+// The VirtualBasicLockable is a virtual lock abstraction for locks which meet
+// the C++ named BasicLockable requirements of lock() and unlock().
+//
+// This virtual indirection is useful in case you need configurable lock
+// selection in a portable module where the final type is not defined upstream
+// and ergo module configuration cannot be used or in case the lock type is not
+// fixed at compile time, for example to support run time and crash time use of
+// an object without incurring the code size hit for templating the object.
+class PW_LOCKABLE("pw::sync::VirtualBasicLockable") VirtualBasicLockable {
+ public:
+ void lock() PW_EXCLUSIVE_LOCK_FUNCTION() {
+ DoLockOperation(Operation::kLock);
+ };
+
+ void unlock() PW_UNLOCK_FUNCTION() { DoLockOperation(Operation::kUnlock); };
+
+ protected:
+ ~VirtualBasicLockable() = default;
+
+ enum class Operation {
+ kLock,
+ kUnlock,
+ };
+
+ private:
+ // Uses a single virtual method with an enum to minimize the vtable cost per
+ // implementation of VirtualBasicLockable.
+ virtual void DoLockOperation(Operation operation) = 0;
+};
+
+// The NoOpLock is a type of VirtualBasicLockable that does nothing, i.e. lock
+// operations are no-ops.
+class PW_LOCKABLE("pw::sync::NoOpLock") NoOpLock final
+ : public VirtualBasicLockable {
+ public:
+ constexpr NoOpLock() {}
+ NoOpLock(const NoOpLock&) = delete;
+ NoOpLock(NoOpLock&&) = delete;
+ NoOpLock& operator=(const NoOpLock&) = delete;
+ NoOpLock& operator=(NoOpLock&&) = delete;
+
+ // Gives access to a global NoOpLock instance. It is not necessary to have
+ // multiple NoOpLock instances since they have no state and do nothing.
+ static NoOpLock& Instance() {
+ PW_CONSTINIT static NoOpLock lock;
+ return lock;
+ }
+
+ private:
+ void DoLockOperation(Operation) override {}
+};
+
+} // namespace pw::sync
diff --git a/pw_sync/timed_mutex_facade_test.cc b/pw_sync/timed_mutex_facade_test.cc
index 22ed0d3..cdeb631 100644
--- a/pw_sync/timed_mutex_facade_test.cc
+++ b/pw_sync/timed_mutex_facade_test.cc
@@ -114,6 +114,22 @@
}
}
+TEST(VirtualTimedMutex, LockUnlock) {
+ pw::sync::VirtualTimedMutex mutex;
+ mutex.lock();
+ // TODO(pwbug/291): Ensure it fails to lock when already held.
+ // EXPECT_FALSE(mutex.try_lock());
+ mutex.unlock();
+}
+
+VirtualTimedMutex static_virtual_mutex;
+TEST(VirtualTimedMutex, LockUnlockStatic) {
+ static_virtual_mutex.lock();
+ // TODO(pwbug/291): Ensure it fails to lock when already held.
+ // EXPECT_FALSE(static_virtual_mutex.try_lock());
+ static_virtual_mutex.unlock();
+}
+
TEST(TimedMutex, LockUnlockInC) {
pw::sync::TimedMutex mutex;
pw_sync_TimedMutex_CallLock(&mutex);