This document describes the structure languages typically when supporting Fuchsia.
The lowest level of Fuchsia support in a language provides access to the Zircon system calls. Exposing these system calls lets programs written in the language interact with the kernel and, transitively, with the rest of the system.
Programs cannot issue system calls directly. Instead, they make system calls by calling functions in the vDSO, which is loaded into newly created processes by their creator.
The vast majority of Fuchsia programs act as servers. After startup, they wait in an event loop to receive messages, process those messages (potentially by sending messages to other processes), and then go back to sleep in their event loop.
The fundamental building block for event loops in Fuchsia is the port object. A thread can sleep in a port using
zx_port_wait. When the kernel wakes up the thread, the kernel provides a packet, which is a data structure that describes why the kernel woke up the thread.
Typically, each thread has a single port object in which it sleeps, which a significant amount of code written in your language will need to interact with. Rather than expose the port directly, language mantainers usually provide a library that abstracts over a port and provides asynchronous wait operations.
Most asynchronous wait operations bottom out in
zx_object_wait_async. Typically, the
key arguments are provided by the library and the
signals arguments are provided by the clients. When establishing a wait, the clients also typically provide an upcall (e.g., a closure) for the library to invoke when the wait completes, at which point the library uses the
key to recover the upcall (e.g., from a hash table).
No additional kernel object is needed to wake a thread up from another thread. You can wake up a thread by simply queuing a user packet to the thread's port using zx_port_queue.
The Zircon kernel itself largely provides memory management, scheduling, and interprocess communication. Rather than being provided directly by the kernel, the bulk of the system interface is actually provided through interprocess communication, typically using channels. The protocols used for interprocess communication are defined in Fuchsia Interface Definition Language (FIDL).
FIDL support for a language typically involves two pieces:
These pieces are usually not built into the language implementation or runtime. Instead, the libraries are part of the developer's program and versioned independently from the language runtime. The stable interface between the program and the language runtime should be the system calls rather than the FIDL protocols so that developers can pick the versions of their FIDL protocols and the version of their language runtimes independently.
In some cases, the language runtime might need to use FIDL internally. If that happens, prefer to hide this implementation detail from the developer's program if possible in your language. The developer might wish to use newer versions of the same FIDL protocols without conflicting with the version used internally by the language runtime.
The FIDL compiler has a single frontend that is used for all languages and multiple backends that support a diverse assortment of languages. The frontend produces a JSON intermediate format that is consumed by the language-specific backends.
You should create a new backend for the FIDL compiler for your language. The backend can be written in whatever language you prefer. Typically, language maintainer choose either Go or the target language.
The generated FIDL code varies substantially from one language to another. Typically the generated code will contain the following types of code:
Some languages offer multiple options for some of these types of generated code. For example, a common pattern is to offer both synchronous and asynchronous proxy objects. The synchronous proxies make use of
zx_channel_call to efficiently write a message, block waiting for a response, and then read the response, whereas asynchronous proxies use
zx_channel_read to avoid blocking on the remote end of the channel.
Generally, we prefer to use asynchronous code whenever possible. Many FIDL protocols are designed to be used in an asynchronous, feed-forward pattern.
When designing the generated code for your language, pay particular attention to binary size. Sophisticated program often interact with a large number of FIDL protocols, each of which might define many data structures and protocols.
One important technique for reducing binary size is to factor as much code as possible into a FIDL support library. For example, the C bindings, all the serialization and deserialization logic is performed by a routine in a support library. The generate code contains only a table that describes the wire format in a compact form.
Typically, the support library is layered on top of the async library, which itself has no knowledge of FIDL. For example, most support libraries contain a reader object, which manages the asynchronous waiting and reading operations on channels. The generated code can then be restricted to serialization, deserialization, and dispatch.
POSIX-style IO operations (e.g.,
write) are layered on top of FIDL. If your language has C interop, you can use the FDIO library, which translates familiar POSIX operations into the underlying
fuchsia.io FIDL protocol. If your language does not have C interop, you will need to interface directly with
fuchsia.io to provide POSIX-style IO.
You can recover the underlying Zircon handles for file descriptors using
lib/fdio/unsafe.h. Typically, languages have a tiny library that layers on top of the async library to perform asynchronous waits on file descriptors. This library typically provides a less error-prone interface that abstracts these “unsafe” FDIO functions.