This page collects the cross-cutting gotchas that bite new users. Each entry
links to the full explanation; keep the details on their home pages so this
page stays a lightweight index.
| Gotcha |
What happens |
Fix |
| Scanned API headers include third-party or dependency headers |
libclang parses every transitive include; an unparseable header silently degrades template types (std::vector, std::unordered_map, …) to int in generated bindings |
Only include C++ standard headers, dart_cpp_bridge/*, and async_simple/coro/Lazy.h; put heavy includes in native/api_impl/*.cpp. See Include Whitelist |
| Implementation code in API headers |
Generated bindings depend on implementation details and the headers become fragile |
Declarations only; implementations go in api_impl/*.cpp. See Declaration Hygiene |
Type aliases / using namespace in scanned headers |
codegen cannot resolve aliases and may generate wrong types |
Expand aliases and use fully qualified names |
Editing native/generated/ or lib/src/native_gen/ |
Files are overwritten on the next generation |
Treat them as build artifacts. See Project Structure |
| Expecting build hooks to re-run codegen |
Native Assets hooks only compile and link; stale generated code silently ships |
Run dcb_gen_tool generate after API header changes. See Native Assets Build Hooks |
| Gotcha |
What happens |
Fix |
| Blocking the io thread |
Freezes the whole event loop; every coroutine and callback stalls |
Offload blocking work to spawn_blocking / the thread pool. See Threading Rules |
syncAwait on the io thread |
Self-deadlock: the awaited coroutine needs the io thread to resume |
Call syncAwait only from non-io threads |
DartFn inside BRIDGE_SYNC |
Deadlock: Dart replies are delivered through the io thread |
Use async markers (BRIDGE_ASYNC / BRIDGE_NORMAL) or spawn_blocking. See Common Mistakes |
RescheduleLazy::detach() |
Exceptions surface on the io thread and crash the process |
Use dcb::spawn_detached |
| Coroutine lambdas on MSVC |
Captured values are corrupted after suspension |
Use static coroutine functions or pass values explicitly. See Common Mistakes |
| Gotcha |
What happens |
Fix |
Using async-simple’s uthread fibers in business code |
uthread is not built or linked by dart_cpp_bridge (CMake EXCLUDE_FROM_ALL), is not supported on Windows, and its Darwin assembly selection ignores CMAKE_OSX_ARCHITECTURES, breaking macOS cross-architecture slices |
Use only async-simple’s header-only surface (Lazy / Executor / Promise / Signal); if you need fibers, use Boost.Fiber. See Don’t use uthread |
| Gotcha |
What happens |
Fix |
| Expecting Dart to force-cancel a Future |
A Dart Future is only a listener; it cannot interrupt the running C++ coroutine |
Expose cooperative cancellation: register an async_simple::Signal per task id and emit Terminate from a cancelTask-style API. See Cancellation (Signal & Slot) |
sleep() that never wakes on cancel |
The timer keeps running to completion if no signal is bound |
Bind the coroutine chain with Lazy::setLazyLocal(signal) so sleep() is interruptible. See Cancellable sleep |
collectAll / collectAny without Terminate |
The losing tasks keep running after the first result |
Use collectAll<Terminate> / collectAny<Terminate> to cancel the losers. See Cancelling the losers |
| Cancelling a Stream subscription |
Only stops Dart-side delivery; C++ continues and late add() calls are silently dropped |
Stop the producer explicitly, or accept fire-and-forget semantics. See Streams |
| Gotcha |
What happens |
Fix |
Worker isolate calls shutdown() |
Closes the main isolate’s session and stops the runtime |
Only the main isolate may shut down. See Lifecycle Management |
| Sharing Opaque objects across isolates |
Handles are per-session; another isolate cannot use them |
Keep objects inside their owning isolate |
Using the bridge after dispose() |
Calls fail; the session is gone |
Re-init the session, or finish work before disposing |
| Gotcha |
What happens |
Fix |
Blocking inside a dcb_invoke_dart_fn callback |
The callback runs on the bridge io thread; blocking freezes the loop |
Do minimal work, then marshal. See Behavior |
| Reusing an async op after completion / cancel |
op_id is one-shot; later calls are no-ops |
Create a new op per operation. See Async Operation Primitives |
Expecting pure C code to co_await |
The C side only creates / completes / cancels ops; the awaiting side is a C++ coroutine |
Bridge via dcb::async_wait inside a Lazy |
| Gotcha |
What happens |
Fix |
| Calling timer APIs off the loop thread |
libuv / glib timer APIs are not thread-safe |
schedule_after is invoked on the loop thread; keep it that way |
cancel_after touching the timer directly |
Races with the loop thread |
Marshal cancellation to the loop; make it a safe no-op for fired or unknown handles. See Timer Flow |
| Unregistering with pending coroutines |
Suspended coroutines are not resumed |
Ensure channels are closed / tasks are finished before unregistering. See Worker Contract |