pw_sync: Add GenericBasicLockable

Lock types that derive from pw::sync::VirtualBasicLockable need to
implement DoLockOperation() for the values of Operation::kLock and
Operation::kUnlock. For most lock types, this translates to simply
calling lock() or unlock() on the underlying lock instance,
respectively. This CL deduplicates code from Mutex, TimedMutex, and
InterruptSpinLock by providing a common implementation templated on the
lock type.

Change-Id: I962e329b47aeb50918ed5667520d0d3417fdb8ce
Reviewed-on: https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/165930
Commit-Queue: Aaron Green <aarongreen@google.com>
Reviewed-by: Wyatt Hepler <hepler@google.com>
diff --git a/pw_sync/docs.rst b/pw_sync/docs.rst
index 9e0df4c..5cc7996 100644
--- a/pw_sync/docs.rst
+++ b/pw_sync/docs.rst
@@ -731,9 +731,9 @@
 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
+VirtualBasicLockable
+--------------------
+The ``VirtualBasicLockable`` 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:
@@ -742,9 +742,22 @@
 * :cpp:func:`pw::sync::VirtualTimedMutex`
 * :cpp:func:`pw::sync::VirtualInterruptSpinLock`
 
+GenericBasicLockable
+--------------------
+``GenericBasicLockable`` is a helper construct that can be used to declare
+virtual versions of a critical section lock primitive that meets the
+`BasicLockable <https://en.cppreference.com/w/cpp/named_req/BasicLockable>`_
+C++ named requirement. For example, given a ``Mutex`` type with ``lock()`` and
+``unlock()`` methods, a ``VirtualMutex`` type that derives from
+``VirtualBasicLockable`` can be declared as follows:
+
+.. code-block:: cpp
+
+   class VirtualMutex : public GenericBasicLockable<Mutex> {};
+
 Borrowable
 ==========
-The Borrowable is a helper construct that enables callers to borrow an object
+``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
diff --git a/pw_sync/mutex_facade_test.cc b/pw_sync/mutex_facade_test.cc
index 2b7926e..bf4e61d 100644
--- a/pw_sync/mutex_facade_test.cc
+++ b/pw_sync/mutex_facade_test.cc
@@ -33,7 +33,7 @@
 // TODO(b/235284163): Add real concurrency tests once we have pw::thread.
 
 TEST(Mutex, LockUnlock) {
-  pw::sync::Mutex mutex;
+  Mutex mutex;
   mutex.lock();
   // TODO(b/235284163): Ensure it fails to lock when already held.
   // EXPECT_FALSE(mutex.try_lock());
@@ -49,7 +49,7 @@
 }
 
 TEST(Mutex, TryLockUnlock) {
-  pw::sync::Mutex mutex;
+  Mutex mutex;
   const bool locked = mutex.try_lock();
   EXPECT_TRUE(locked);
   if (locked) {
@@ -62,7 +62,7 @@
 PW_SYNC_ADD_BORROWABLE_LOCK_NAMED_TESTS(BorrowableMutex, Mutex);
 
 TEST(VirtualMutex, LockUnlock) {
-  pw::sync::VirtualMutex mutex;
+  VirtualMutex mutex;
   mutex.lock();
   // TODO(b/235284163): Ensure it fails to lock when already held.
   // EXPECT_FALSE(mutex.try_lock());
@@ -77,16 +77,25 @@
   static_virtual_mutex.unlock();
 }
 
+TEST(VirtualMutex, LockUnlockExternal) {
+  VirtualMutex virtual_mutex;
+  auto& mutex = virtual_mutex.mutex();
+  mutex.lock();
+  // TODO(b/235284163): Ensure it fails to lock when already held.
+  // EXPECT_FALSE(mutex.try_lock());
+  mutex.unlock();
+}
+
 PW_SYNC_ADD_BORROWABLE_LOCK_NAMED_TESTS(BorrowableVirtualMutex, VirtualMutex);
 
 TEST(Mutex, LockUnlockInC) {
-  pw::sync::Mutex mutex;
+  Mutex mutex;
   pw_sync_Mutex_CallLock(&mutex);
   pw_sync_Mutex_CallUnlock(&mutex);
 }
 
 TEST(Mutex, TryLockUnlockInC) {
-  pw::sync::Mutex mutex;
+  Mutex mutex;
   ASSERT_TRUE(pw_sync_Mutex_CallTryLock(&mutex));
   // TODO(b/235284163): Ensure it fails to lock when already held.
   // EXPECT_FALSE(pw_sync_Mutex_CallTryLock(&mutex));
diff --git a/pw_sync/public/pw_sync/interrupt_spin_lock.h b/pw_sync/public/pw_sync/interrupt_spin_lock.h
index 7726c92..996ad6d 100644
--- a/pw_sync/public/pw_sync/interrupt_spin_lock.h
+++ b/pw_sync/public/pw_sync/interrupt_spin_lock.h
@@ -81,32 +81,8 @@
 };
 
 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_;
-};
+    VirtualInterruptSpinLock final
+    : public GenericBasicLockable<InterruptSpinLock> {};
 
 }  // namespace pw::sync
 
diff --git a/pw_sync/public/pw_sync/mutex.h b/pw_sync/public/pw_sync/mutex.h
index 5b42468..18729f5 100644
--- a/pw_sync/public/pw_sync/mutex.h
+++ b/pw_sync/public/pw_sync/mutex.h
@@ -86,31 +86,9 @@
 };
 
 class PW_LOCKABLE("pw::sync::VirtualMutex") VirtualMutex final
-    : public VirtualBasicLockable {
+    : public GenericBasicLockable<Mutex> {
  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_;
+  Mutex& mutex() { return impl(); }
 };
 
 }  // namespace pw::sync
diff --git a/pw_sync/public/pw_sync/timed_mutex.h b/pw_sync/public/pw_sync/timed_mutex.h
index abb48c1..396f9b6 100644
--- a/pw_sync/public/pw_sync/timed_mutex.h
+++ b/pw_sync/public/pw_sync/timed_mutex.h
@@ -71,31 +71,9 @@
 };
 
 class PW_LOCKABLE("pw::sync::VirtualTimedMutex") VirtualTimedMutex final
-    : public VirtualBasicLockable {
+    : public GenericBasicLockable<TimedMutex> {
  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_;
+  TimedMutex& timed_mutex() { return impl(); }
 };
 
 }  // namespace pw::sync
diff --git a/pw_sync/public/pw_sync/virtual_basic_lockable.h b/pw_sync/public/pw_sync/virtual_basic_lockable.h
index 5010365..c41f39b 100644
--- a/pw_sync/public/pw_sync/virtual_basic_lockable.h
+++ b/pw_sync/public/pw_sync/virtual_basic_lockable.h
@@ -70,4 +70,34 @@
   void DoLockOperation(Operation) override {}
 };
 
+/// Templated base class to facilitate making "Virtual{LockType}" from a
+/// "LockType" class that provides `lock()` and `unlock()` methods.
+/// The resulting classes will derive from `VirtualBasicLockable`.
+///
+/// Example:
+///   class VirtualMutex : public GenericBasicLockable<Mutex> {};
+template <typename LockType>
+class GenericBasicLockable : public VirtualBasicLockable {
+ public:
+  virtual ~GenericBasicLockable() = default;
+
+ protected:
+  LockType& impl() { return impl_; }
+
+ private:
+  void DoLockOperation(Operation operation) override
+      PW_NO_LOCK_SAFETY_ANALYSIS {
+    switch (operation) {
+      case Operation::kLock:
+        return impl_.lock();
+
+      case Operation::kUnlock:
+      default:
+        return impl_.unlock();
+    }
+  }
+
+  LockType impl_;
+};
+
 }  // namespace pw::sync
diff --git a/pw_sync/timed_mutex_facade_test.cc b/pw_sync/timed_mutex_facade_test.cc
index b74c569..26a3b47 100644
--- a/pw_sync/timed_mutex_facade_test.cc
+++ b/pw_sync/timed_mutex_facade_test.cc
@@ -49,7 +49,7 @@
 // TODO(b/235284163): Add real concurrency tests once we have pw::thread.
 
 TEST(TimedMutex, LockUnlock) {
-  pw::sync::TimedMutex mutex;
+  TimedMutex mutex;
   mutex.lock();
   mutex.unlock();
   // TODO(b/235284163): Ensure it fails to lock when already held by someone
@@ -67,7 +67,7 @@
 }
 
 TEST(TimedMutex, TryLockUnlock) {
-  pw::sync::TimedMutex mutex;
+  TimedMutex mutex;
   const bool locked = mutex.try_lock();
   EXPECT_TRUE(locked);
   if (locked) {
@@ -79,7 +79,7 @@
 }
 
 TEST(TimedMutex, TryLockUnlockFor) {
-  pw::sync::TimedMutex mutex;
+  TimedMutex mutex;
 
   SystemClock::time_point before = SystemClock::now();
   const bool locked = mutex.try_lock_for(kRoundedArbitraryDuration);
@@ -98,7 +98,7 @@
 }
 
 TEST(TimedMutex, TryLockUnlockUntil) {
-  pw::sync::TimedMutex mutex;
+  TimedMutex mutex;
 
   const SystemClock::time_point deadline =
       SystemClock::now() + kRoundedArbitraryDuration;
@@ -121,7 +121,7 @@
                                               chrono::SystemClock);
 
 TEST(VirtualTimedMutex, LockUnlock) {
-  pw::sync::VirtualTimedMutex mutex;
+  VirtualTimedMutex mutex;
   mutex.lock();
   // TODO(b/235284163): Ensure it fails to lock when already held by someone
   // else.
@@ -138,18 +138,27 @@
   static_virtual_mutex.unlock();
 }
 
+TEST(VirtualMutex, LockUnlockExternal) {
+  VirtualTimedMutex virtual_timed_mutex;
+  auto& mutex = virtual_timed_mutex.timed_mutex();
+  mutex.lock();
+  // TODO(b/235284163): Ensure it fails to lock when already held.
+  // EXPECT_FALSE(mutex.try_lock());
+  mutex.unlock();
+}
+
 PW_SYNC_ADD_BORROWABLE_TIMED_LOCK_NAMED_TESTS(BorrowableVirtualTimedMutex,
                                               VirtualTimedMutex,
                                               chrono::SystemClock);
 
 TEST(TimedMutex, LockUnlockInC) {
-  pw::sync::TimedMutex mutex;
+  TimedMutex mutex;
   pw_sync_TimedMutex_CallLock(&mutex);
   pw_sync_TimedMutex_CallUnlock(&mutex);
 }
 
 TEST(TimedMutex, TryLockUnlockInC) {
-  pw::sync::TimedMutex mutex;
+  TimedMutex mutex;
   ASSERT_TRUE(pw_sync_TimedMutex_CallTryLock(&mutex));
   // TODO(b/235284163): Ensure it fails to lock when already held by someone
   // else.
@@ -158,7 +167,7 @@
 }
 
 TEST(TimedMutex, TryLockUnlockForInC) {
-  pw::sync::TimedMutex mutex;
+  TimedMutex mutex;
 
   pw_chrono_SystemClock_TimePoint before = pw_chrono_SystemClock_Now();
   ASSERT_TRUE(
@@ -176,7 +185,7 @@
 }
 
 TEST(TimedMutex, TryLockUnlockUntilInC) {
-  pw::sync::TimedMutex mutex;
+  TimedMutex mutex;
   pw_chrono_SystemClock_TimePoint deadline;
   deadline.duration_since_epoch.ticks =
       pw_chrono_SystemClock_Now().duration_since_epoch.ticks +