| 132 | } |
| 133 | |
| 134 | class CoroutinePlatformPthread : public ICoroutinePlatform { |
| 135 | public: |
| 136 | void* createContext(void (*entry_fn)(), size_t /*stack_size*/) FL_NOEXCEPT override { |
| 137 | auto* ctx = new PthreadCoroCtx(); // ok bare allocation - platform context lifetime |
| 138 | ctx->entry_fn = entry_fn; |
| 139 | ctx->thread.reset(new fl::thread(pthread_coro_thread_main, ctx)); // ok bare allocation |
| 140 | return ctx; |
| 141 | } |
| 142 | |
| 143 | void* createRunnerContext() FL_NOEXCEPT override { |
| 144 | // Runner is the calling thread (sketch thread). No OS thread to spawn — |
| 145 | // we just need a parking spot for it. |
| 146 | auto* ctx = new PthreadCoroCtx(); // ok bare allocation |
| 147 | return ctx; |
| 148 | } |
| 149 | |
| 150 | void destroyContext(void* ctx_p) FL_NOEXCEPT override { |
| 151 | auto* ctx = static_cast<PthreadCoroCtx*>(ctx_p); |
| 152 | if (!ctx) return; |
| 153 | |
| 154 | if (ctx->thread) { |
| 155 | // Teardown protocol — guarantees the worker has fully stopped |
| 156 | // touching *ctx before we free it. |
| 157 | // |
| 158 | // 1. Set `exit_requested` and `signaled` under the lock. |
| 159 | // 2. notify_all() — wakes the worker out of either its initial |
| 160 | // cv.wait() or any contextSwitch() cv.wait(). |
| 161 | // 3. The worker rechecks `exit_requested` after every cv.wait() |
| 162 | // and either falls off pthread_coro_thread_main (initial |
| 163 | // wait) or calls pthread_exit() (contextSwitch). |
| 164 | // 4. join() blocks until that exit completes — only then is it |
| 165 | // safe to delete the thread object and *ctx. |
| 166 | // |
| 167 | // NB: under `-fno-exceptions`, `pthread_exit` skips C++ stack |
| 168 | // unwinding. Library frames release their locks before exit (see |
| 169 | // contextSwitch / pthread_coro_thread_main), but RAII objects |
| 170 | // living on the *user* coroutine's stack are abandoned without |
| 171 | // destructors. See the "Coroutine teardown contract" block at |
| 172 | // the top of this file for the caller-facing implications. |
| 173 | { |
| 174 | fl::lock_guard<fl::mutex> lk(ctx->mutex); |
| 175 | ctx->exit_requested = true; |
| 176 | ctx->signaled = true; |
| 177 | } |
| 178 | ctx->cv.notify_all(); |
| 179 | |
| 180 | if (ctx->thread->joinable()) { |
| 181 | ctx->thread->join(); |
| 182 | } |
| 183 | ctx->thread.reset(); |
| 184 | } |
| 185 | delete ctx; // ok bare allocation - platform context lifetime |
| 186 | } |
| 187 | |
| 188 | void contextSwitch(void* from_ctx_p, void* to_ctx_p) FL_NOEXCEPT override { |
| 189 | auto* from = static_cast<PthreadCoroCtx*>(from_ctx_p); |
| 190 | auto* to = static_cast<PthreadCoroCtx*>(to_ctx_p); |
| 191 | if (!from || !to) return; |
nothing calls this directly
no test coverage detected