# libasync and friends This set of libraries defines a C and C++ language interface for initiating asynchronous operations and dispatching their results to callback functions. The purpose of these libraries is to decouple clients which want to perform asynchronous operations from the message loop implementations which dispatch the results of those operations. This makes it an important building block for other abstractions such as the FIDL bindings. ## Libraries The async package consists of three libraries: - `libasync.a` provides the C client API which includes all of the function and structures declared in the following headers: - [async/dispatcher.h](include/lib/async/dispatcher.h) - [async/exception.h](include/lib/async/exception.h) - [async/receiver.h](include/lib/async/receiver.h) - [async/task.h](include/lib/async/task.h) - [async/time.h](include/lib/async/time.h) - [async/trap.h](include/lib/async/trap.h) - [async/wait.h](include/lib/async/wait.h) - `libasync-cpp.a` provides C++ wrappers: - [async/cpp/exception.h](include/lib/async/cpp/exception.h) - [async/cpp/receiver.h](include/lib/async/cpp/receiver.h) - [async/cpp/task.h](include/lib/async/cpp/task.h) - [async/cpp/time.h](include/lib/async/cpp/time.h) - [async/cpp/trap.h](include/lib/async/cpp/trap.h) - [async/cpp/wait.h](include/lib/async/cpp/wait.h) - `libasync-default.so` provides functions for getting or setting a thread-local default asynchronous dispatcher as declared in [async/default.h](include/lib/async/default.h). See also [libasync-loop.a](../async-loop/README.md) which provides a general-purpose implementation of `async_dispatcher_t`. ## Using the asynchronous dispatcher ### Waiting for signals To asynchronously wait for signals, the client prepares an `async_wait_t` structure then calls `async_begin_wait()` to register it with the dispatcher. When the wait completes, the dispatcher invokes the handler. The client can register handlers from any thread but dispatch will occur on a thread of the dispatcher's choosing depending on its implementation. The client is responsible for ensuring that the wait structure remains in memory until the wait's handler runs or the wait is successfully canceled using `async_cancel_wait()`. See [async/wait.h](include/lib/async/wait.h) for details. ```c #include // for async_begin_wait() #include // for async_get_default_dispatcher() void handler(async_dispatcher_t* async, async_wait_t* wait, zx_status_t status, const zx_packet_signal_t* signal) { printf("signal received: status=%d, observed=%d", status, signal ? signal->observed : 0); free(wait); } zx_status_t await(zx_handle_t object, zx_signals_t trigger, void* data) { async_dispatcher_t* async = async_get_default_dispatcher(); async_wait_t* wait = calloc(1, sizeof(async_wait_t)); wait->handler = handler; wait->object = object; wait->trigger = trigger; return async_begin_wait(async, wait); } ``` ### Waiting for exceptions To asynchronously wait for exceptions, the client prepares an `async_exception_t` structure then calls `async_bind_exception_port()` to register it with the dispatcher. When an exception happens, the dispatcher invokes the handler. The client can register handlers from any thread but dispatch will occur on a thread of the dispatcher's choosing depending on its implementation. The client is responsible for ensuring that the exception structure remains in memory, and the task handle contained therein remains valid, until the port is successfully unbound using `async_unbind_exception_port()`. See [async/exception.h](include/lib/async/exception.h) for details. ```c #include // for async_begin_wait() #include // for async_get_default_dispatcher() void handler(async_dispatcher_t* async, async_exception_t* exception, zx_status_t status, const zx_port_packet_t* exception_packet) { printf("signal received: status=%d, kind=0x%x", status, exception ? exception->type : 0); if (status == ZX_OK) { // ... process exception ... // N.B. If the port is attached to a process or job then we will // get exceptions for more than just |exception->task|. zx_handle_t task = handle_of(exception_packet->exception.tid); uint32_t options = exception_handled ? 0u : ZX_RESUME_TRY_NEXT; status = async_resume_from_exception(async, exception, task, options); // ... examine status ... } } zx_status_t ebind(async_exception_handler_t* handler, zx_handle_t task, uint32_t options, zx_async_exception_t* out_exception) { async_dispatcher_t* async = async_get_default_dispatcher(); async_exception_t* exception = calloc(1, sizeof(async_exception_t)); exception->handler = handler; exception->task = task; exception->options = options; zx_status_t status = async_bind_exception_port(async, exception); if (status == ZX_OK) { *out_exception = exception; } else } free(exception); } return status; } zx_status_t eunbind(async_exception_t* exception) { async_dispatcher_t* async = async_get_default_dispatcher(); zx_status_t status = async_unbind_exception_port(dispatcher, exception); free(exception); return status; } ``` ### Getting the current time The dispatcher represents time in the form of a `zx_time_t`. In normal operation, values of this type represent a moment in the `ZX_CLOCK_MONOTONIC` time base. However for unit testing purposes, dispatchers may use a synthetic time base instead. To make unit testing easier, prefer using `async_now()` to get the current time according the dispatcher's time base. See [async/time.h](include/lib/async/time.h) for details. ### Posting tasks and getting the current time To schedule asynchronous tasks, the client prepares an `async_task_t` structure then calls `async_post_task()` to register it with the dispatcher. When the task comes due, the dispatcher invokes the handler. The client can post tasks from any thread but dispatch will occur on a thread of the dispatcher's choosing depending on its implementation. The client is responsible for ensuring that the task structure remains in memory until the task's handler runs or the task is successfully canceled using `async_cancel_task()`. See [async/task.h](include/lib/async/task.h) for details. ```c #include // for async_post_task() #include // for async_now() #include // for async_get_default_dispatcher() typedef struct { async_task_t task; void* data; } task_data_t; void handler(async_dispatcher_t* async, async_task_t* task, zx_status_t status) { task_data_t* task_data = (task_data_t*)task; printf("task deadline elapsed: status=%d, data=%p", status, task_data->data); free(task_data); } zx_status_t schedule_work(void* data) { async_dispatcher_t* async = async_get_default_dispatcher(); task_data_t* task_data = calloc(1, sizeof(task_data_t)); task_data->task.handler = handler; task_data->task.deadline = async_now(async) + ZX_SEC(2); task_data->data = data; return async_post_task(async, &task_data->task); } ``` ### Delivering packets to a receiver Occasionally it may be useful to register a receiver which will be the recipient of multiple data packets instead of allocating a separate task structure for each one. The Zircon port takes care of storing the queued packet data contents until it is delivered. The client can queue packets from any thread but dispatch will occur on a thread of the dispatcher's choosing depending on its implementation. The client is responsible for ensuring that the receiver structure remains in memory until all queued packets have been delivered. See [async/receiver.h](include/lib/async/receiver.h) for details. ```c #include // for async_queue_packet() #include // for async_get_default_dispatcher() void handler(async_dispatcher_t* async, async_receiver_t* receiver, zx_status_t status, const zx_packet_user_t* data) { printf("packet received: status=%d, data.u32[0]=%d", status, data ? data.u32[0] : 0); } const async_receiver_t receiver = { .state = ASYNC_STATE_INIT, .handler = handler; } zx_status_t send(const zx_packet_user_t* data) { async_dispatcher_t* async = async_get_default_dispatcher(); return async_queue_packet(async, &receiver, data); } ``` ## The default async dispatcher As a client of the async dispatcher, where should you get your `async_dispatcher_t*` from? The ideal answer is for the `async_dispatcher_t*` to be passed into your code when it is initialized. However sometimes this becomes burdensome or isn't practical. For this reason, the `libasync-default.so` shared library provides functions for getting or setting a thread-local default `async_dispatcher_t*` using `async_get_default_dispatcher()` or `async_set_default_dispatcher()`. This makes it easy to retrieve the `async_dispatcher_t*` from the ambient environment by calling `async_get_default_dispatcher()`, which is used by many libraries. Message loop implementations should register themselves as the default dispatcher any threads they service. See [async/default.h](include/lib/async/default.h) for details. ## Using the C++ helpers `libasync-cpp.a` includes helper classes such as `Wait`, `Task`, and `Receiver` which wrap the C API with a more convenient type safe interface for use in C++. Note that the C API can of course be used directly from C++ for special situations which may not be well addressed by the wrappers. ## Implementing a dispatcher The `async_ops_t` interface is a low-level abstraction for asynchronous dispatchers. You can make custom implementations of this interface to integrate clients of this library with your own dispatcher. It is possible to implement only some of the operations but this may cause incompatibilities with certain clients. See [async/dispatcher.h](include/lib/async/dispatcher.h) for details.