From 1931c927db6d732bc1459bf5941e6da29c090688 Mon Sep 17 00:00:00 2001 From: h2zero Date: Sun, 1 Nov 2026 16:13:41 -0600 Subject: [PATCH] Add NimBLEUtils::runOnHostTask for safely running callables on the NimBLE host task Summary * Adds a public utility, bool NimBLEUtils::runOnHostTask(std::function func). It lets library users run code on the NimBLE host task. Callers capture whatever context they need in the callable instead of passing a void* plus a lifetime contract. Behavior * Already on the host task: func runs immediately and the call returns true. * Any other task: func is moved into a heap-owned context and queued on the default NimBLE event queue. The host task runs it asynchronously, then frees the context. * Host task not running (before NimBLEDevice::init() or after deinit): the call returns false and the callable is dropped. * Empty std::function: returns false. Design notes * Ownership: the callable and its ble_npl_event live in one heap-allocated HostTaskContext. This replaces a stack-local event and a pointer to the caller's std::function, which were both dangling hazards. * Captured state: values captured by the callable are copied or moved into the context. Anything captured by reference or raw pointer must stay valid until the callable runs. Use values or a shared_ptr when lifetime is uncertain. * Pending-task tracking: queued contexts are kept in a spinlock-protected intrusive list and unlinked in executeHostTask. * Shutdown: NimBLEUtils::deinit() calls the new drainHostTasks(). It clears the host-task handle and removes queued events with ble_npl_eventq_remove. It then frees the contexts without running them. Queueing and the handle check share one lock, so a call racing with deinit is either rejected or drained, never leaked. API contract * The callable runs asynchronously unless the caller is already on the host task. This is not a completion or wait mechanism. * The callable must not throw. It runs inside NimBLE's C event loop. * Work queued but not yet run when the stack is deinitialized is discarded. * A false return means the work was not run or queued. --- src/NimBLEDevice.cpp | 1 + src/NimBLEUtils.cpp | 88 ++++++++++++++++++++++++++++++++++++++++++++ src/NimBLEUtils.h | 10 +++++ 3 files changed, 99 insertions(+) diff --git a/src/NimBLEDevice.cpp b/src/NimBLEDevice.cpp index 4d1e40c3..09df941a 100644 --- a/src/NimBLEDevice.cpp +++ b/src/NimBLEDevice.cpp @@ -885,6 +885,7 @@ void NimBLEDevice::onSync(void) { */ void NimBLEDevice::host_task(void* param) { NIMBLE_LOGI(LOG_TAG, "NimBLE Started!"); + NimBLEUtils::m_hostTaskHandle = ble_npl_get_current_task_id(); nimble_port_run(); // This function will return only when nimble_port_stop() is executed nimble_port_freertos_deinit(); } // host_task diff --git a/src/NimBLEUtils.cpp b/src/NimBLEUtils.cpp index 7b440125..9ae6e18d 100644 --- a/src/NimBLEUtils.cpp +++ b/src/NimBLEUtils.cpp @@ -23,8 +23,10 @@ # ifdef USING_NIMBLE_ARDUINO_HEADERS # include "nimble/nimble/host/include/host/ble_hs.h" +# include "nimble/porting/nimble/include/nimble/nimble_port.h" # else # include "host/ble_hs.h" +# include "nimble/nimble_port.h" # endif /**** FIX COMPILATION ****/ @@ -34,6 +36,8 @@ # include # include +# include +# include # ifndef MYNEWT_VAL_NIMBLE_CPP_ENABLE_RETURN_CODE_TEXT # ifdef CONFIG_NIMBLE_CPP_ENABLE_RETURN_CODE_TEXT @@ -64,6 +68,9 @@ static const char* LOG_TAG = "NimBLEUtils"; std::vector NimBLEUtils::m_taskSemEntries{}; std::atomic NimBLEUtils::m_semUserCount{0}; std::atomic_flag NimBLEUtils::m_taskSemLock = ATOMIC_FLAG_INIT; +std::atomic NimBLEUtils::m_hostTaskHandle{nullptr}; +std::atomic_flag NimBLEUtils::m_hostTaskLock = ATOMIC_FLAG_INIT; +NimBLEUtils::HostTaskContext* NimBLEUtils::m_pendingHostTasks{nullptr}; /** * @brief Construct a NimBLEUtils::TaskData instance. @@ -165,6 +172,8 @@ void NimBLEUtils::taskRelease(const TaskData& taskData, int flags) { * @details This should be called when the NimBLE stack is deinitialized to clean up any resources used by waiting tasks. */ void NimBLEUtils::deinit() { + drainHostTasks(); + // wait for any active users of the semaphore vector to finish while (m_semUserCount.load() > 0) { ble_npl_time_delay(1); @@ -186,6 +195,85 @@ void NimBLEUtils::deinit() { m_taskSemEntries.clear(); } +struct NimBLEUtils::HostTaskContext { + ble_npl_event ev; + std::function func; + HostTaskContext* next{nullptr}; +}; + +/** + * @brief Run a callable on the NimBLE host task. + * @param [in] func The callable to run; capture any needed context by value (or shared_ptr). + * @return true if func was run or queued, false if it was dropped because the host task is not running. + * @details If called from the host task, func runs immediately. Otherwise it is moved into + * an owned context and queued, so it runs asynchronously later. Anything func borrows by + * reference or raw pointer must remain valid until it runs. func must not throw. + * Tasks still queued when the stack is deinitialized are discarded without running. + */ +bool NimBLEUtils::runOnHostTask(std::function func) { + if (!func) { + return false; + } + + if (m_hostTaskHandle.load() == ble_npl_get_current_task_id()) { + func(); + return true; + } + + auto* ctx = new HostTaskContext{{}, std::move(func)}; + ble_npl_event_init(&ctx->ev, executeHostTask, ctx); + + // The handle check and queueing are under the lock so deinit() cannot drain between them. + SpinGuard guard(m_hostTaskLock); + if (m_hostTaskHandle.load() == nullptr) { + delete ctx; + return false; + } + + ctx->next = m_pendingHostTasks; + m_pendingHostTasks = ctx; + ble_npl_eventq_put(nimble_port_get_dflt_eventq(), &ctx->ev); + return true; +} + +void NimBLEUtils::executeHostTask(ble_npl_event* ev) { + auto* ctx = static_cast(ev->arg); + { + SpinGuard guard(m_hostTaskLock); + for (HostTaskContext** p = &m_pendingHostTasks; *p != nullptr; p = &(*p)->next) { + if (*p == ctx) { + *p = ctx->next; + break; + } + } + } + + std::unique_ptr owner{ctx}; + ctx->func(); +} + +/** + * @brief Stops accepting host task work and discards any tasks that were queued but never ran. + */ +void NimBLEUtils::drainHostTasks() { + HostTaskContext* list = nullptr; + { + SpinGuard guard(m_hostTaskLock); + m_hostTaskHandle.store(nullptr); + list = m_pendingHostTasks; + m_pendingHostTasks = nullptr; + for (auto* c = list; c != nullptr; c = c->next) { + ble_npl_eventq_remove(nimble_port_get_dflt_eventq(), &c->ev); + } + } + + while (list != nullptr) { + auto* next = list->next; + delete list; + list = next; + } +} + /** * @brief Converts a return code from the NimBLE stack to a text string. * @param [in] rc The return code to convert. diff --git a/src/NimBLEUtils.h b/src/NimBLEUtils.h index 0bf780c2..8a78d597 100644 --- a/src/NimBLEUtils.h +++ b/src/NimBLEUtils.h @@ -43,9 +43,11 @@ void nimble_cpp_assert(const char* file, unsigned line) __attribute((weak, noret # include # include # include +# include class NimBLEAddress; struct ble_npl_sem; +struct ble_npl_event; /** * @brief A BLE Utility class with methods for debugging and general purpose use. @@ -94,6 +96,7 @@ class NimBLEUtils { static NimBLEAddress generateAddr(bool nrpa); static bool taskWait(const TaskData& taskData, uint32_t timeout); static void taskRelease(const TaskData& taskData, int rc = 0); + static bool runOnHostTask(std::function func); private: friend class NimBLEDevice; @@ -101,6 +104,13 @@ class NimBLEUtils { static std::vector m_taskSemEntries; static std::atomic m_semUserCount; static std::atomic_flag m_taskSemLock; + static std::atomic m_hostTaskHandle; + + struct HostTaskContext; + static std::atomic_flag m_hostTaskLock; + static HostTaskContext* m_pendingHostTasks; + static void executeHostTask(ble_npl_event* ev); + static void drainHostTasks(); }; #endif // CONFIG_BT_NIMBLE_ENABLED