跳转到内容

Architecture

This content is for v1. Switch to the latest version for up-to-date documentation.

此内容尚不支持你的语言。

Dart / Flutter App
└─ dart_cpp_bridge Dart package (DartCppBridge, codec, FFI bindings)
└─ FFI binary frames (wire protocol)
Native library
├─ Runtime (asio io_context + AsioExecutor + thread_pool)
├─ Session registry (per-Isolate reply port, DartFn closures)
├─ Wire dispatch (frame routing, method_id → user code)
├─ Codec (ByteReader/Writer, frame + payload encoding/decoding)
├─ Channel / ForeignExecutor / Cbridge adapters
└─ User code (BRIDGE_SYNC / BRIDGE_ASYNC / BRIDGE_NORMAL functions)

The code generator dcb_gen_tool scans BRIDGE_* markers in user headers and generates wire dispatch, Dart FFI bindings, and the Dart API layer.

Process-wide singleton managing:

  • asio::io_context single-threaded event loop
  • dcb::AsioExecutor scheduling async-simple coroutines onto io_context
  • asio::thread_pool blocking work pool
  • Session registry

Business code can use the utilities Runtime provides directly: spawn, spawn_blocking, spawn_detached, channel, sleep, etc. See Basic Runtime.

Each Dart Isolate that calls DartCppBridge.init() gets one Session:

  • A dedicated long-lived reply port
  • A DartFn closure registry (fn_id → closure)
  • A generation counter; late messages arriving after dispose() are discarded
  • Lifecycle managed by NativeFinalizer; automatically closed when the Dart object becomes unreachable or the isolate exits

Generated by dcb_gen_tool or written by hand. It is responsible for:

  • Receiving binary frames from Dart and validating magic / version / payload length
  • Routing by method_id to the user C++ function
  • Catching std::exception thrown by user code and encoding it as a responseErr frame returned to Dart
  • Forwarding sync / async / stream / DartFn calls to the appropriate handling path

The low-level binary frame format is documented in Wire Protocol. ByteReader / ByteWriter handle encoding/decoding of primitive types, strings, containers, optionals, and data classes inside the payload. Both sides must read and write in exactly the same order.

C++ objects marked with BRIDGE_OPAQUE are passed by handle: each instance is assigned a handle and stored in a per-Session ObjectHandleRegistry. Dart invokes instance methods through the handle, and Dart GC calls dcb_drop_object via NativeFinalizer to release the C++ object.

Dart function call
→ FFI dcb_invokeSyncMethod
→ wire dispatch decodes arguments
→ user C++ function (runs synchronously on the io_context thread, must not block)
→ encodes return value
→ responseOk frame
→ Dart returns synchronously
Dart Future call
→ FFI dcb_invokeAsyncMethod
→ wire dispatch decodes arguments
→ starts an async_simple::Lazy<T> coroutine on the io_context thread
→ coroutine can co_await and suspend (no thread is held)
→ coroutine completes
→ responseOk / responseErr frame posted to the Dart reply port
→ Dart Completer completes
Dart Future call
→ FFI dcb_invokeNormalMethod
→ wire dispatch posts the blocking task to the thread_pool
→ user function runs on the thread pool (may block)
→ encodes result
→ responseOk / responseErr frame posted to the Dart reply port
→ Dart Completer completes
Dart subscribe
→ FFI creates a C++ StreamSink
→ C++ side sink.add(item) sends a streamData frame
→ Dart StreamController receives the data
→ C++ side sink.end() sends a streamEnd frame
→ Dart Stream ends

Unsubscription only stops receiving on the Dart side; the C++ side continues to run, and subsequent add() calls are silently dropped.

Dart passes a closure as an argument to C++
→ C++ stores the fn_id
→ when needed, C++ sends a DartFnCall frame to Dart
→ Dart executes the closure
→ Dart sends a DartFnReply frame
→ C++ oneshot channel completes
→ waiting coroutine resumes

co_await callback(args) truly suspends on the io thread without occupying the thread pool.

  • io_context thread: event loop, coroutine scheduling, Dart frame send/receive, DartFn callback triggering
  • thread_pool: blocking business logic for BRIDGE_NORMAL and spawn_blocking
  • External runtime threads: non-asio event loops integrated via the ForeignExecutor adapter
  • Dart Isolate thread: Dart code execution and Dart closure execution context

Prefer co::oneshot / co::mpsc channels for cross-thread / cross-runtime communication instead of raw locks + condition variables.

The bridge supports integrating non-asio event loops (libuv, glib, custom loops, etc.) into the coroutine system through the ForeignExecutor adapter, enabling cross-runtime non-blocking communication and Dart callback invocation. See Foreign Runtime Integration.

For pure C code or scenarios that do not depend on async-simple, the Pure C Bridge API is provided (callback style, zero C++ dependency).

For a typical dcb_gen_tool project, generated artifacts include:

  • native/generated/wire_dispatch.{hpp,cpp} — routing and codec
  • lib/src/native_gen/dcb_bindings.dart — FFI function signatures
  • lib/src/native_gen/api/{api}.dart — top-level function entry points
  • lib/src/native_gen/dcb_generated.dart — internal implementation (impl + singleton)

Business logic remains in user-written .cpp files; the generated layer is only responsible for routing Dart calls to the correct C++ functions.