pw_rpc: Add function for yielding the RPC lock in C++

Upcoming CLs will require threads to yield the RPC lock while waiting
for a different thread to finish executing an RPC callback.

- Add a config option for selecting how to yield (busy loop,
  pw::this_thread::sleep_for(), pw::this_thread::yield()).
- Add dependencies on pw_thread if backends are defined.
- Have the pw_rpc:disable_global_mutex config select the busy loop yield
  approach, since platforms with the mutex disabled either do not
  support threading or exclusively use RPC from one thread.
- Disable the RPC lock on a few platforms with no RTOS.

Change-Id: I94517d66e04b51b95a911adac560426ee3683388
Reviewed-on: https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/126751
Commit-Queue: Auto-Submit <auto-submit@pigweed.google.com.iam.gserviceaccount.com>
Reviewed-by: Alexei Frolov <frolv@google.com>
Pigweed-Auto-Submit: Wyatt Hepler <hepler@google.com>
diff --git a/pw_rpc/BUILD.bazel b/pw_rpc/BUILD.bazel
index f669214..004c60d 100644
--- a/pw_rpc/BUILD.bazel
+++ b/pw_rpc/BUILD.bazel
@@ -122,6 +122,7 @@
         "//pw_status",
         "//pw_sync:lock_annotations",
         "//pw_sync:mutex",
+        "//pw_thread:sleep",
         "//pw_toolchain:no_destructor",
     ],
 )
diff --git a/pw_rpc/BUILD.gn b/pw_rpc/BUILD.gn
index c0af29f..1c7bbdb 100644
--- a/pw_rpc/BUILD.gn
+++ b/pw_rpc/BUILD.gn
@@ -35,7 +35,10 @@
 }
 
 config("disable_global_mutex_config") {
-  defines = [ "PW_RPC_USE_GLOBAL_MUTEX=0" ]
+  defines = [
+    "PW_RPC_USE_GLOBAL_MUTEX=0",
+    "PW_RPC_YIELD_MODE=PW_RPC_YIELD_MODE_BUSY_LOOP",
+  ]
   visibility = [ ":*" ]
 }
 
@@ -168,6 +171,16 @@
     ":log_config",
     dir_pw_log,
   ]
+
+  # pw_rpc needs a way to yield the current thread. Depending on its
+  # configuration, it may need either pw_thread:sleep or pw_thread:yield.
+  if (pw_thread_SLEEP_BACKEND != "") {
+    deps += [ "$dir_pw_thread:sleep" ]
+  }
+  if (pw_thread_YIELD_BACKEND != "") {
+    deps += [ "$dir_pw_thread:yield" ]
+  }
+
   public = [
     "public/pw_rpc/channel.h",
     "public/pw_rpc/method_id.h",
diff --git a/pw_rpc/CMakeLists.txt b/pw_rpc/CMakeLists.txt
index ba4c97c..2d55a68 100644
--- a/pw_rpc/CMakeLists.txt
+++ b/pw_rpc/CMakeLists.txt
@@ -156,9 +156,18 @@
     pw_log
     pw_rpc.log_config
 )
-if (NOT "${pw_sync.mutex_BACKEND}" STREQUAL "")
+if(NOT "${pw_sync.mutex_BACKEND}" STREQUAL "")
   pw_target_link_targets(pw_rpc.common PUBLIC pw_sync.mutex)
 endif()
+
+if(NOT "${pw_thread.sleep_BACKEND}" STREQUAL "")
+  pw_target_link_targets(pw_rpc.common PUBLIC pw_thread.sleep)
+endif()
+
+if(NOT "${pw_thread.yield_BACKEND}" STREQUAL "")
+  pw_target_link_targets(pw_rpc.common PUBLIC pw_thread.yield)
+endif()
+
 if(Zephyr_FOUND AND CONFIG_PIGWEED_RPC_COMMON)
   zephyr_link_libraries(pw_rpc.common)
 endif()
diff --git a/pw_rpc/docs.rst b/pw_rpc/docs.rst
index f8f4598..636ad95 100644
--- a/pw_rpc/docs.rst
+++ b/pw_rpc/docs.rst
@@ -1464,6 +1464,22 @@
 
   This is enabled by default.
 
+.. c:macro:: PW_RPC_YIELD_MODE
+
+  pw_rpc must yield the current thread when waiting for a callback to complete
+  in a different thread. ``PW_RPC_YIELD_MODE`` determines how to yield. There
+  are three supported settings:
+
+  - ``PW_RPC_YIELD_MODE_BUSY_LOOP``. Do nothing. Release and reacquire the RPC
+    lock in a busy loop. :c:macro:`PW_RPC_USE_GLOBAL_MUTEX` must be 0 as well.
+  - ``PW_RPC_YIELD_MODE_SLEEP``. Yield with 1-tick calls to
+    :cpp:func:`pw::this_thread::sleep_for`. A backend must be configured for
+    pw_thread:sleep.
+  - ``PW_RPC_YIELD_MODE_YIELD``. Yield with :cpp:func:`pw::this_thread::yield`.
+    A backend must be configured for pw_thread:yield. IMPORTANT: On some
+    platforms, :cpp:func:`pw::this_thread::yield` does not yield to lower
+    priority tasks and should not be used here.
+
 .. c:macro:: PW_RPC_DYNAMIC_ALLOCATION
 
   Whether pw_rpc should use dynamic memory allocation internally. If enabled,
diff --git a/pw_rpc/endpoint.cc b/pw_rpc/endpoint.cc
index 9a70f0d..a3d922c 100644
--- a/pw_rpc/endpoint.cc
+++ b/pw_rpc/endpoint.cc
@@ -22,6 +22,54 @@
 #include "pw_rpc/internal/lock.h"
 #include "pw_toolchain/no_destructor.h"
 
+#if PW_RPC_YIELD_MODE == PW_RPC_YIELD_MODE_BUSY_LOOP
+
+static_assert(
+    PW_RPC_USE_GLOBAL_MUTEX == 0,
+    "The RPC global mutex is enabled, but no pw_rpc yield mode is selected! "
+    "Because the global mutex is in use, pw_rpc may be used from multiple "
+    "threads. This could result in thread starvation. To fix this, set "
+    "PW_RPC_YIELD to PW_RPC_YIELD_MODE_SLEEP and add a dependency on "
+    "pw_thread:sleep.");
+
+#elif PW_RPC_YIELD_MODE == PW_RPC_YIELD_MODE_SLEEP
+
+#if !__has_include("pw_thread/sleep.h")
+
+static_assert(false,
+              "PW_RPC_YIELD_MODE is PW_RPC_YIELD_MODE_SLEEP "
+              "(pw::this_thread::sleep_for()), but no backend is set for "
+              "pw_thread:sleep. Set a pw_thread:sleep backend or use a "
+              "different PW_RPC_YIELD_MODE setting.");
+
+#endif  // !__has_include("pw_thread/sleep.h")
+
+#include "pw_thread/sleep.h"
+
+#elif PW_RPC_YIELD_MODE == PW_RPC_YIELD_MODE_YIELD
+
+#if !__has_include("pw_thread/yield.h")
+
+static_assert(false,
+              "PW_RPC_YIELD_MODE is PW_RPC_YIELD_MODE_YIELD "
+              "(pw::this_thread::yield()), but no backend is set for "
+              "pw_thread:yield. Set a pw_thread:yield backend or use a "
+              "different PW_RPC_YIELD_MODE setting.");
+
+#endif  // !__has_include("pw_thread/yield.h")
+
+#include "pw_thread/yield.h"
+
+#else
+
+static_assert(
+    false,
+    "PW_RPC_YIELD_MODE macro must be set to PW_RPC_YIELD_MODE_BUSY_LOOP, "
+    "PW_RPC_YIELD_MODE_SLEEP (pw::this_thread::sleep_for()), or "
+    "PW_RPC_YIELD_MODE_YIELD (pw::this_thread::yield())");
+
+#endif  // PW_RPC_YIELD_MODE
+
 namespace pw::rpc::internal {
 
 RpcLock& rpc_lock() {
@@ -29,6 +77,16 @@
   return *lock;
 }
 
+void YieldRpcLock() {
+  rpc_lock().unlock();
+#if PW_RPC_YIELD_MODE == PW_RPC_YIELD_MODE_SLEEP
+  this_thread::sleep_for(chrono::SystemClock::duration(1));
+#elif PW_RPC_YIELD_MODE == PW_RPC_YIELD_MODE_YIELD
+  this_thread::yield();
+#endif  // PW_RPC_YIELD_MODE
+  rpc_lock().lock();
+}
+
 Endpoint::~Endpoint() {
   // Since the calls remove themselves from the Endpoint in
   // CloseAndSendResponse(), close responders until no responders remain.
diff --git a/pw_rpc/public/pw_rpc/internal/config.h b/pw_rpc/public/pw_rpc/internal/config.h
index d29c24e..8b4f6ba 100644
--- a/pw_rpc/public/pw_rpc/internal/config.h
+++ b/pw_rpc/public/pw_rpc/internal/config.h
@@ -49,6 +49,28 @@
 #define PW_RPC_USE_GLOBAL_MUTEX 1
 #endif  // PW_RPC_USE_GLOBAL_MUTEX
 
+// pw_rpc must yield the current thread when waiting for a callback to complete
+// in a different thread. PW_RPC_YIELD_MODE determines how to yield. There are
+// three supported settings:
+//
+//   PW_RPC_YIELD_MODE_BUSY_LOOP - Do nothing. Release and reacquire the RPC
+//       lock in a busy loop. PW_RPC_USE_GLOBAL_MUTEX must be 0.
+//   PW_RPC_YIELD_MODE_SLEEP - Yield with 1-tick calls to
+//       pw::this_thread::sleep_for(). A backend must be configured for
+//       pw_thread:sleep.
+//   PW_RPC_YIELD_MODE_YIELD - Yield with pw::this_thread::yield(). A backend
+//       must be configured for pw_thread:yield. IMPORTANT: On some platforms,
+//       pw::this_thread::yield() does not yield to lower priority tasks and
+//       should not be used here.
+//
+#define PW_RPC_YIELD_MODE_BUSY_LOOP 100
+#define PW_RPC_YIELD_MODE_SLEEP 101
+#define PW_RPC_YIELD_MODE_YIELD 102
+
+#ifndef PW_RPC_YIELD_MODE
+#define PW_RPC_YIELD_MODE PW_RPC_YIELD_MODE_SLEEP
+#endif  // PW_RPC_YIELD_MODE
+
 // Whether pw_rpc should use dynamic memory allocation internally. If enabled,
 // pw_rpc dynamically allocates channels and its encoding buffers. RPC users may
 // use dynamic allocation independently of this option (e.g. to allocate pw_rpc
diff --git a/pw_rpc/public/pw_rpc/internal/lock.h b/pw_rpc/public/pw_rpc/internal/lock.h
index 50c53d5..e0bf67b 100644
--- a/pw_rpc/public/pw_rpc/internal/lock.h
+++ b/pw_rpc/public/pw_rpc/internal/lock.h
@@ -53,4 +53,7 @@
 
 RpcLock& rpc_lock();
 
+// Releases the RPC lock, yields, and reacquires it.
+void YieldRpcLock() PW_EXCLUSIVE_LOCKS_REQUIRED(rpc_lock());
+
 }  // namespace pw::rpc::internal
diff --git a/targets/arduino/target_toolchains.gni b/targets/arduino/target_toolchains.gni
index e5120c0..3b74b8b 100644
--- a/targets/arduino/target_toolchains.gni
+++ b/targets/arduino/target_toolchains.gni
@@ -47,6 +47,7 @@
   pw_sync_INTERRUPT_SPIN_LOCK_BACKEND =
       "$dir_pw_sync_baremetal:interrupt_spin_lock"
   pw_sync_MUTEX_BACKEND = "$dir_pw_sync_baremetal:mutex"
+  pw_rpc_CONFIG = "$dir_pw_rpc:disable_global_mutex"
   pw_sys_io_BACKEND = dir_pw_sys_io_arduino
   pw_rpc_system_server_BACKEND = "$dir_pw_hdlc:hdlc_sys_io_system_server"
   pw_arduino_build_INIT_BACKEND = "$dir_pigweed/targets/arduino:pre_init"
diff --git a/targets/lm3s6965evb_qemu/target_toolchains.gni b/targets/lm3s6965evb_qemu/target_toolchains.gni
index 334aef8..25779b8 100644
--- a/targets/lm3s6965evb_qemu/target_toolchains.gni
+++ b/targets/lm3s6965evb_qemu/target_toolchains.gni
@@ -44,6 +44,7 @@
   pw_sync_INTERRUPT_SPIN_LOCK_BACKEND =
       "$dir_pw_sync_baremetal:interrupt_spin_lock"
   pw_sync_MUTEX_BACKEND = "$dir_pw_sync_baremetal:mutex"
+  pw_rpc_CONFIG = "$dir_pw_rpc:disable_global_mutex"
 
   # pw_cpu_exception_armv7m tests do not work as expected in QEMU. It does not
   # appear the divide-by-zero traps as expected when enabled, which prevents the
diff --git a/targets/rp2040/BUILD.gn b/targets/rp2040/BUILD.gn
index 44f5350..329d95e 100644
--- a/targets/rp2040/BUILD.gn
+++ b/targets/rp2040/BUILD.gn
@@ -55,6 +55,7 @@
     pw_sync_INTERRUPT_SPIN_LOCK_BACKEND =
         "$dir_pw_sync_baremetal:interrupt_spin_lock"
     pw_sync_MUTEX_BACKEND = "$dir_pw_sync_baremetal:mutex"
+    pw_rpc_CONFIG = "$dir_pw_rpc:disable_global_mutex"
 
     # Silence GN variable overwrite warning.
     pw_build_LINK_DEPS = []
diff --git a/targets/stm32f429i_disc1/target_toolchains.gni b/targets/stm32f429i_disc1/target_toolchains.gni
index 8903ec0..d7c0fdf 100644
--- a/targets/stm32f429i_disc1/target_toolchains.gni
+++ b/targets/stm32f429i_disc1/target_toolchains.gni
@@ -60,6 +60,7 @@
   pw_sync_INTERRUPT_SPIN_LOCK_BACKEND =
       "$dir_pw_sync_baremetal:interrupt_spin_lock"
   pw_sync_MUTEX_BACKEND = "$dir_pw_sync_baremetal:mutex"
+  pw_rpc_CONFIG = "$dir_pw_rpc:disable_global_mutex"
   pw_log_BACKEND = dir_pw_log_basic
   pw_sys_io_BACKEND = dir_pw_sys_io_baremetal_stm32f429
   pw_rpc_system_server_BACKEND = "$dir_pw_hdlc:hdlc_sys_io_system_server"