Google is committed to advancing racial equity for Black communities. See how.

Getting started

G# Low-level C++ language FIDL tutorial

About this tutorial

This tutorial describes how to make client calls and write servers in C++ using the Low-Level C++ Bindings (LLCPP).

Getting Started has a walk-through of using the bindings with an example FIDL library. The reference section documents the detailed bindings interface and design.

See Comparing C, Low-Level C++, and High-Level C++ Language Bindings for a comparative analysis of the goals and use cases for all the C-family language bindings.

Two build setups exist in the source tree: the Zircon build and the Fuchsia build. The LLCPP code generator is not supported by the Zircon build. Therefore, the steps to use the bindings depend on where the consumer code is located:

  • Code is outside zircon/: Add //[library path]:[library name]_llcpp to the GN dependencies e.g. "//sdk/fidl/fuchsia.math:fuchsia.math_llcpp", and the bindings code will be automatically generated as part of the build.
  • Code is inside zircon/: Add a GN dependency of the form: "$zx/system/fidl/[library-name]:llcpp", e.g. "$zx/system/fidl/fuchsia-mem:llcpp", and the bindings code will be automatically generated as part of the build.

Preliminary concepts

  • Decoded message: A FIDL message in decoded form is a contiguous buffer that is directly accessible by reinterpreting the memory as the corresponding LLCPP FIDL type. That is, all pointers point within the same buffer, and the pointed objects are in a specific order defined by the FIDL wire-format. When making a call, a response buffer is used to decode the response message.

  • Encoded message: A FIDL message in encoded form is an opaque contiguous buffer plus an array of handles. The buffer is of the same length as the decoded counterpart, but pointers are replaced with placeholders, and handles are moved to the accompanying array. When making a call, a request buffer is used to encode the request message.

  • Message linearization: FIDL messages have to be in a contiguous buffer packed according to the wire-format. When making a call however, the arguments to the bindings code and out-of-line objects are usually scattered in memory, unless careful attention is spent to follow the wire-format order. The process of walking down the tree of objects and packing them is termed linearization, and usually involves O(message size) copying.

  • Message layout: The in-memory layout of LLCPP structures is the same as the layout of the wire format. The LLCPP objects can be thought of as a view over the encoded message..

  • Message ownership: LLCPP objects use tracking_ptr smart pointers to manage ownership and track whether an object is heap allocated and owned or user-managed and unowned.

Generated API overview

Low-Level C++ bindings are full featured, and support control over allocation as well as zero-copy encoding/decoding. (Note that contrary to the C bindings they are meant to replace, the LLCPP bindings cover non-simple messages.)

Let's use this FIDL protocol as a motivating example:

// fleet.fidl
library fuchsia.fleet;

struct Planet {
    string name;
    float64 mass;
    handle<channel> radio;

The following code is generated (simplified for readability):

// fleet.h
struct Planet {
  fidl::StringView name;
  double mass;
  zx::channel radio;

Note that string maps to fidl::StringView, hence the Planet struct will not own the memory associated with the name string. Rather, all strings point within some buffer space that is managed by the bindings library, or that the caller could customize. The same goes for the fidl::VectorView<Planet> in the code below.

Continuing with the FIDL protocol:

// fleet.fidl continued...
protocol SpaceShip {
    SetHeading(int16 heading);
    ScanForPlanets() -> (vector<Planet> planets);
    DirectedScan(int16 heading) -> (vector<Planet> planets);
    -> OnBeacon(int16 heading);

The following code is generated (simplified for readability):

// fleet.h continued...
class SpaceShip final {
  struct SetHeadingRequest final {
    fidl_message_header_t _hdr;
    int16_t heading;

  struct ScanForPlanetsResponse final {
    fidl_message_header_t _hdr;
    fidl::VectorView<Planet> planets;
  using ScanForPlanetsRequest = fidl::AnyZeroArgMessage;

  struct DirectedScanRequest final {
    fidl_message_header_t _hdr;
    int16_t heading;
  struct DirectedScanResponse final {
    fidl_message_header_t _hdr;
    fidl::VectorView<Planet> planets;

  class SyncClient final { /* ... */ };
  class Call final { /* ... */ };
  class Interface { /* ... */ };

  static bool TryDispatch(Interface* impl, fidl_msg_t* msg, fidl::Transaction* txn);
  static bool Dispatch(Interface* impl, fidl_msg_t* msg, fidl::Transaction* txn);

  class ResultOf final { /* ... */ };
  class UnownedResultOf final { /* ... */ };
  class InPlace final { /* ... */ };

  // Generated classes for thread-safe async-capable client.
  struct AsyncEventHandlers {
  class ScanForPlanetsResponseContext { /* ... */ };
  class DirectedScanResponseContext { /* ... */ };
  class ClientImpl { /* ... */ };

Notice that every request and response is modelled as a struct: SetHeadingRequest, ScanForPlanetsResponse, etc. In particular, ScanForPlanets() has a request that contains no arguments, and we provide a special type for that, fidl::AnyZeroArgMessage.

Following those, there are three related concepts in the generated code:

  • SyncClient: A class that owns a Zircon channel, providing methods to make requests to the FIDL server.
  • Call: A class that contains static functions to make sync FIDL calls directly on an unowned channel, avoiding setting up a SyncClient. This is similar to the simple client wrappers from the C bindings, which take a zx_handle_t.
  • Interface and [Try]Dispatch: A server should implement the Interface pure virtual class, which allows Dispatch to call one of the defined handlers with a received FIDL message.

[Unowned]ResultOf are "scoping" classes containing return type definitions of FIDL calls inside SyncClient and Call. This allows one to conveniently write ResultOf::SetHeading to denote the result of calling SetHeading.

InPlace is another "scoping" class that houses functions to make a FIDL call with encoding and decoding performed in-place directly on the user buffer. It is more efficient than those SyncClient or Call, but comes with caveats. We will dive into these separately.

Client API

Sync client (Protocol::SyncClient)

The following code is generated for SpaceShip::SyncClient. Each FIDL method always correspond to two overloads which differ in memory management strategies, termed flavors in LLCPP: managed flavor and caller-allocating flavor.

class SyncClient final {
  SyncClient(zx::channel channel);

  // FIDL: SetHeading(int16 heading);
  ResultOf::SetHeading SetHeading(int16_t heading);
  UnownedResultOf::SetHeading SetHeading(fidl::BytePart request_buffer, int16_t heading);

  // FIDL: ScanForPlanets() -> (vector<Planet> planets);
  ResultOf::ScanForPlanets ScanForPlanets();
  UnownedResultOf::ScanForPlanets ScanForPlanets(fidl::BytePart response_buffer);

  // FIDL: DirectedScan(int16 heading) -> (vector<Planet> planets);
  ResultOf::DirectedScan DirectedScan(int16_t heading);
  UnownedResultOf::DirectedScan DirectedScan(fidl::BytePart request_buffer, int16_t heading,
                                             fidl::BytePart response_buffer);

The one-way FIDL method SetHeading(int16 heading) maps to:

  • ResultOf::SetHeading SetHeading(int16_t heading): This is the managed flavor. Buffer allocation for requests and responses are entirely handled within this function, as is the case in simple C bindings. The bindings calculate a safe buffer size specific to this call at compile time based on FIDL wire-format and maximum length constraints. The buffers are allocated on the stack if they fit under 512 bytes, or else on the heap. Here is an example of using it:
// Create a client from a Zircon channel.
SpaceShip::SyncClient client(zx::channel(client_end));

// Calling |SetHeading| with heading = 42.
SpaceShip::ResultOf::SetHeading result = client.SetHeading(42);

// Check the transport status (encoding error, channel writing error, etc.)
if (result.status() != ZX_OK) {
  // Handle error...

In general, the managed flavor is easier to use, but may result in extra allocation. See ResultOf for details on buffer management.

  • UnownedResultOf::SetHeading SetHeading(fidl::BytePart request_buffer, int16_t heading): This is the caller-allocating flavor, which defers all memory allocation responsibilities to the caller. Here we see an additional parameter request_buffer which is always the first argument in this flavor. The type fidl::BytePart references a buffer address and size. It will be used by the bindings library to construct the FIDL request, hence it must be sufficiently large. The method parameters (e.g. heading) are linearized to appropriate locations within the buffer. If SetHeading had a return value, this flavor would ask for a response_buffer too, as the last argument. Here is an example of using it:
// Call SetHeading with an explicit buffer, there are multiple ways...

// 1. On the stack
fidl::Buffer<SetHeadingRequest> request_buffer;
auto result = client.SetHeading(request_buffer.view(), 42);

// 2. On the heap
auto request_buffer = std::make_unique<fidl::Buffer<SetHeadingRequest>>();
auto result = client.SetHeading(request_buffer->view(), 42);

// 3. Some other means, e.g. thread-local storage
constexpr uint32_t request_size = fidl::MaxSizeInChannel<SetHeadingRequest>();
uint8_t* buffer = allocate_buffer_of_size(request_size);
fidl::BytePart request_buffer(/* data = */buffer, /* capacity = */request_size);
auto result = client.SetHeading(std::move(request_buffer), 42);

// Check the transport status (encoding error, channel writing error, etc.)
if (result.status() != ZX_OK) {
  // Handle error...

// Don't forget to free the buffer at the end if approach #3 was used...

When the caller-allocating flavor is used, the result object borrows the request and response buffers (hence its type is under UnownedResultOf). Make sure the buffers outlive the result object. See UnownedResultOf.

The two-way FIDL method ScanForPlanets() -> (vector<Planet> planets) maps to:

  • ResultOf::ScanForPlanets ScanForPlanets(): This is the managed flavor. Different from the C bindings, response arguments are not returned via out-parameters. Instead, they are accessed through the return value. Here is an example to illustrate:
// It is cleaner to omit the |UnownedResultOf::ScanForPlanets| result type.
auto result = client.ScanForPlanets();

// Check the transport status (encoding error, channel writing error, etc.)
if (result.status() != ZX_OK) {
  // handle error & early exit...

// Obtains a pointer to the response struct inside |result|.
// This requires that the transport status is |ZX_OK|.
SpaceShip::ScanForPlanetsResponse* response = result.Unwrap();

// Access the |planets| response vector in the FIDL call.
for (const auto& planet : response->planets) {
  // Do something with |planet|...

When the managed flavor is used, the returned object (result in this example) manages ownership of all buffer and handles, while result.Unwrap() returns a view over it. Therefore, the result object must outlive any references to the response.

  • UnownedResultOf::ScanForPlanets ScanForPlanets(fidl::BytePart response_buffer): The caller-allocating flavor receives the message into response_buffer. Here is an example using it:
fidl::Buffer<ScanForPlanetsResponse> response_buffer;
auto result = client.ScanForPlanets(response_buffer.view());
if (result.status() != ZX_OK) { /* ... */ }
auto response = result.Unwrap();
// |response->planets| points to a location within |response_buffer|.

The buffers passed to caller-allocating flavor do not have to be initialized. A buffer may be re-used multiple times, as long as it is large enough for the calls involved.

Async-capable Client (fidl::Client<Protocol>)

This client is thread-safe and supports both synchronous and asynchronous calls as well as asynchronous event handling. It also supports use with a multi-threaded dispatcher.


A client is created with a client-end zx::channel, an async_dispatcher_t*, an optional hook (OnClientUnboundFn) to be invoked when the channel is unbound, and an optional AsyncEventHandlers containing hooks to be invoked on FIDL events.

Client<SpaceShip> client;
zx_status_t status = client.Bind(
    std::move(client_end), dispatcher,
    // OnClientUnboundFn
    [&](fidl::UnboundReason, zx_status_t, zx::channel) { /* ... */ },
    // AsyncEventHandlers
    { .on_beacon = [&](int16_t) { /* ... */ } });


The channel may be unbound automatically in case of the server-end being closed or due to an invalid message being received from the server. You may also actively unbind the channel through client.Unbind().

Unbinding is thread-safe. In any of these cases, ongoing and future operations will not cause a fatal failure, only returning ZX_ERR_CANCELED where appropriate.

If you provided an unbound hook, it is executed as task on the dispatcher, providing a reason and error status for the unbinding. You may also recover ownership of the client end of the channel through the hook. The unbound hook is guaranteed to be run.

Interaction with dispatcher

All asynchronous responses, event handling, and error handling are done through the async_dispatcher_t* provided on creation of a client. With the exception of the dispatcher being shutdown, you can expect that all hooks provided to the client APIs will be executed on a dispatcher thread (and not nested within other user code).

NOTE: If you shutdown the dispatcher while there are any active bindings, the unbound hook MAY be executed on the thread executing shutdown. As such, you MUST not take any locks which could be taken by hooks provided to fidl::Client APIs while executing async::Loop::Shutdown()/async_loop_shutdown(). (You should probably ensure that no locks are held around shutdown anyway since it joins all dispatcher threads, which may take locks in user code).

Outgoing FIDL methods

You can invoke outgoing FIDL APIs through the fidl::Client<SpaceShip> instance, e.g. client->SetHeading(0). The full generated API is given below:

class ClientImpl final {
  fidl::StatusAndError SetHeading(int16_t heading);
  fidl::StatusAndError SetHeading(fidl::BytePart _request_buffer,
                                  int16_t heading);

  fidl::StatusAndError ScanForPlanets(
      fit::callback<void(fidl::VectorView<Planet> planets)> _cb);
  fidl::StatusAndError ScanForPlanets(ScanForPlanetsResponseContext* _context);
  ResultOf::ScanForPlanets ScanForPlanets_Sync(int16_t heading);
  UnownedResultOf::ScanForPlanets ScanForPlanets_Sync(
      fidl::BytePart _response_buffer, int16_t heading);

  fidl::StatusAndError DirectedScan(fit::callback<void(fidl::VectorView<Planet> planets)> _cb);
  fidl::StatusAndError DirectedScan(DirectedScanResponseContext* _context);
  ResultOf::DirectedScan DirectedScan_Sync(int16_t heading);
  UnownedResultOf::DirectedScan DirectedScan_Sync(
      fidl::BytePart _request_buffer, int16_t heading,
      fidl::BytePart _response_buffer);

Note that the one-way and synchronous two-way FIDL methods have a similar API to the SyncClient versions. Aside from one-way methods directly returning fidl::StatusAndError and the added _Sync on the synchronous methods, the behavior is identical.

Asynchronous APIs

The managed flavor of the asynchronous two-way APIs simply takes a fit::callback hook which is executed on response in a dispatcher thread. The returned fidl::StatusAndError refers just to the status of the outgoing call.

auto status = client->DirectedScan(0, [&]{ /* ... */ });

The caller-allocated flavor enables you to provide the storage for the callback as well as any associated state. This is done through the generated virtual ResponseContext classes:

class DirectedScanResponseContext : public fidl::internal::ResponseContext {
  virtual void OnReply(fidl::DecodedMessage<DirectedScanResponse> msg) = 0;

You can derive from this class, implementing OnReply() and OnError() (inherited from fidl::internal::ResponseContext). You can then allocate an object of this type as required, passing a pointer to it to the API. The object must stay alive until either OnReply() or OnError() is invoked by the Client.

NOTE: If the Client is destroyed with outstanding asynchronous transactions, OnError() will be invoked for all of the associated ResponseContexts.

Static functions (Protocol::Call)

The following code is generated for SpaceShip::Call:

class Call final {
  static ResultOf::SetHeading
  SetHeading(zx::unowned_channel client_end, int16_t heading);
  static UnownedResultOf::SetHeading
  SetHeading(zx::unowned_channel client_end, fidl::BytePart request_buffer, int16_t heading);

  static ResultOf::ScanForPlanets
  ScanForPlanets(zx::unowned_channel client_end);
  static UnownedResultOf::ScanForPlanets
  ScanForPlanets(zx::unowned_channel client_end, fidl::BytePart response_buffer);

  static ResultOf::DirectedScan
  DirectedScan(zx::unowned_channel client_end, int16_t heading);
  static UnownedResultOf::DirectedScan
  DirectedScan(zx::unowned_channel client_end, fidl::BytePart request_buffer, int16_t heading,
               fidl::BytePart response_buffer);

These methods are similar to those found in SyncClient. However, they do not own the channel. This is useful if one is migrating existing code from the C bindings to low-level C++. Another use case is when implementing C APIs which take a raw zx_handle_t. For example:

// C interface which does not own the channel.
zx_status_t spaceship_set_heading(zx_handle_t spaceship, int16_t heading) {
  auto result = fuchsia::fleet::SpaceShip::Call::SetHeading(
      zx::unowned_channel(spaceship), heading);
  return result.status();

ResultOf and UnownedResultOf

For a method named Foo, ResultOf::Foo is the return type of the managed flavor. UnownedResultOf::Foo is the return type of the caller-allocating flavor. Both types define the same set of methods:

  • zx_status status() const returns the transport status. it returns the first error encountered during (if applicable) linearizing, encoding, making a call on the underlying channel, and decoding the result. If the status is ZX_OK, the call has succeeded, and vice versa.
  • const char* error() const contains a brief error message when status is not ZX_OK. Otherwise, returns nullptr.
  • (only for two-way calls) FooResponse* Unwrap() returns a pointer to the FIDL response message. For ResultOf::Foo, the pointer points to memory owned by the result object. For UnownedResultOf::Foo, the pointer points to a caller-provided buffer. Unwrap() should only be called when the status is ZX_OK.

Allocation strategy And move semantics

ResultOf::Foo stores the response buffer inline if the message is guaranteed to fit under 512 bytes. Since the result object is usually instantiated on the caller's stack, this effectively means the response is stack-allocated when it is reasonably small. If the maximal response size exceeds 512 bytes, ResultOf::Foo instead contains a std::unique_ptr to a heap-allocated buffer.

Therefore, a std::move() on ResultOf::Foo may be costly if the response buffer is inline: the content has to be copied, and pointers to out-of-line objects have to be updated to locations within the destination object. Consider the following snippet:

int CountPlanets(ResultOf::ScanForPlanets result) { /* ... */ }

auto result = client.ScanForPlanets();
SpaceShip::ScanForPlanetsResponse* response = result.Unwrap();
Planet* planet = &response->planets[0];
int count = CountPlanets(std::move(result));    // Costly
// In addition, |response| and |planet| are invalidated due to the move

It may be written more efficiently as:

int CountPlanets(fidl::VectorView<SpaceShip::Planet> planets) { /* ... */ }

auto result = client.ScanForPlanets();
int count = CountPlanets(result.Unwrap()->planets);

If the result object need to be passed around multiple function calls, consider pre-allocating a buffer in the outer-most function and use the caller-allocating flavor.

In-place calls

Both the managed flavor and the caller-allocating flavor will copy the arguments into the request buffer. When there is out-of-line data involved, message linearization is additionally required to collate them as per the wire-format. When the request is large, these copying overhead can add up. LLCPP supports making a call directly on a caller-provided buffer containing a request message in decoded form, without any parameter copying. The request is encoded in-place, hence the name of the scoping class InPlace.

class InPlace final {
  static ::fidl::internal::StatusAndError
  SetHeading(zx::unowned_channel client_end,
             fidl::DecodedMessage<SetHeadingRequest> params);

  static ::fidl::DecodeResult<ScanForPlanets>
  ScanForPlanets(zx::unowned_channel client_end,
                 fidl::DecodedMessage<ScanForPlanetsRequest> params,
                 fidl::BytePart response_buffer);

  static ::fidl::DecodeResult<DirectedScan>
  DirectedScan(zx::unowned_channel client_end,
               fidl::DecodedMessage<DirectedScanRequest> params,
               fidl::BytePart response_buffer);

These functions always take a fidl::DecodedMessage<FooRequest> which wraps the user-provided buffer. To use it properly, initialize the request buffer with a FIDL message in decoded form. In particular, out-of-line objects have to be packed according to the wire-format, and therefore any pointers in the message have to point within the same buffer.

When there is a response defined, the generated functions additionally ask for a response_buffer as the last argument. The response buffer does not have to be initialized.

// Allocate buffer for in-place call
fidl::Buffer<SetHeadingRequest> request_buffer;
fidl::BytePart request_bytes = request_buffer.view();
memset(, 0, request_bytes.capacity());

// Manually construct the message
auto msg = reinterpret_cast<SetHeadingRequest*>(;
msg->heading = 42;
// Here since our message is a simple struct,
// the request size is equal to the capacity.

// Wrap with a fidl::DecodedMessage
fidl::DecodedMessage<SetHeadingRequest> request(std::move(request_bytes));

// Finally, make the call.
auto result = SpaceShip::InPlace::SetHeading(channel, std::move(request));
// Check result.status(), result.error()

Despite the verbosity, there is actually very little work involved. The buffer passed to the underlying zx_channel_call system call is in fact request_bytes. The performance benefits become apparent when say the request message contains a large inline array. One could set up the buffers once, then make repeated calls while mutating the array by directly editing the buffer in between.

Server API

class Interface {
  virtual void SetHeading(int16_t heading,
                          SetHeadingCompleter::Sync completer) = 0;

  class ScanForPlanetsCompleterBase {
    void Reply(fidl::VectorView<Planet> planets);
    void Reply(fidl::BytePart buffer, fidl::VectorView<Planet> planets);
    void Reply(fidl::DecodedMessage<ScanForPlanetsResponse> params);

  using ScanForPlanetsCompleter = fidl::Completer<ScanForPlanetsCompleterBase>;

  virtual void ScanForPlanets(ScanForPlanetsCompleter::Sync completer) = 0;

  class DirectedScanCompleterBase {
    void Reply(fidl::VectorView<Planet> planets);
    void Reply(fidl::BytePart buffer, fidl::VectorView<Planet> planets);
    void Reply(fidl::DecodedMessage<DirectedScanResponse> params);

  using DirectedScanCompleter = fidl::Completer<DirectedScanCompleterBase>;

  virtual void DirectedScan(int16_t heading, DirectedScanCompleter::Sync completer) = 0;

bool TryDispatch(Interface* impl, fidl_msg_t* msg, fidl::Transaction* txn);

The generated Interface class has pure virtual functions corresponding to the method calls defined in the FIDL protocol. One may override these functions in a subclass, and dispatch FIDL messages to a server instance by calling TryDispatch. The bindings runtime would invoke these handler functions appropriately.

class MyServer final : fuchsia::fleet::SpaceShip::Interface {
  void SetHeading(int16_t heading,
                  SetHeadingCompleter::Sync completer) override {
    // Update the heading...
  void ScanForPlanets(ScanForPlanetsCompleter::Sync completer) override {
    fidl::VectorView<Planet> discovered_planets = /* perform planet scan */;
    // Send the |discovered_planets| vector as the response.
  void DirectedScan(int16_t heading, DirectedScanCompleter::Sync completer) override {
    fidl::VectorView<Planet> discovered_planets = /* perform a directed planet scan */;
    // Send the |discovered_planets| vector as the response.

Each handler function has an additional last argument completer. It captures the various ways one may complete a FIDL transaction, by sending a reply, closing the channel with epitaph, etc. For FIDL methods with a reply e.g. ScanForPlanets, the corresponding completer defines up to three overloads of a Reply() function (managed, caller-allocating, in-place), similar to the client side API. The completer always defines a Close(zx_status_t) function, to close the connection with a specified epitaph.

NOTE: Each Completer object must only be accessed by one thread at a time. Simultaneous access from multiple threads will result in a crash.

Responding asynchronously

Notice that the type for the completer ScanForPlanetsCompleter::Sync has ::Sync. This indicates the default mode of operation: the server must synchronously make a reply before returning from the handler function. Enforcing this allows optimizations: the bookkeeping metadata for making a reply may be stack-allocated. To asynchronously make a reply, one may call the ToAsync() method on a Sync completer, converting it to ScanForPlanetsCompleter::Async. The Async completer supports the same Reply() functions, and may out-live the scope of the handler function by e.g. moving it into a lambda capture.

void ScanForPlanets(ScanForPlanetsCompleter::Sync completer) override {
  // Suppose scanning for planets takes a long time,
  // and returns the result via a callback...
      .OnDone([completer = completer.ToAsync()] (auto planets) mutable {
        // Here the type of |completer| is |ScanForPlanetsCompleter::Async|.

Parallel message handling

NOTE: This use-case is currently possible only using the lib/fidl bindings.

By default, messages from a single binding are handled sequentially, i.e. a single thread attached to the dispatcher (run loop) is woken up if necessary, reads the message, executes the handler, and returns back to the dispatcher. The ::Sync completer provides an additional API, EnableNextDispatch(), which may be used to selectively break this restriction. Specifically, a call to this API will enable another thread waiting on the dispatcher to handle the next message on the binding while the first thread is still in the handler. Note that repeated calls to EnableNextDispatch() on the same Completer are idempotent.

void DirectedScan(int16_t heading, ScanForPlanetsCompleter::Sync completer) override {
  // Suppose directed scans can be done in parallel. It would be suboptimal to block one scan until
  // another has completed.
  fidl::VectorView<Planet> discovered_planets = /* perform a directed planet scan */;




  • Support encoding and decoding FIDL messages with C++17.
  • Provide fine-grained control over memory allocation.
  • More type-safety and more features than the C language bindings.
  • Match the size and efficiency of the C language bindings.
  • Depend only on a small subset of the standard library.
  • Minimize code bloat through table-driven encoding and decoding.
  • Reuse encoders, decoders, and coding tables generated for C language bindings.

Pointers and memory ownership

LLCPP objects use special smart pointers called tracking_ptr to keep track of memory ownership. With tracking_ptr, LLCPP makes it possible for your code to easily set a value and forget about ownership: tracking_ptr will take care of freeing memory when it goes out of scope.

These pointers have two states:

  • unowned (constructed from an unowned_ptr_t)
  • heap allocated and owned (constructed from a std::unique_ptr)

When the contents is owned, a tracking_ptr behaves like a unique_ptr and the pointer is deleted on destruction. In the unowned state, tracking_ptr behaves like a raw pointer and destruction is a no-op.

tracking_ptr is move-only and has an API closely matching unique_ptr.

Types of object allocation

tracking_ptr makes it possible to create LLCPP objects with several allocation strategies. The allocation strategies can be mixed and matched within the same code.

Heap allocation

To heap allocate objects, use the standard std::make_unique.

An example with an optional uint32 field represented as a tracking_ptr.

MyStruct s;
s.opt_uint32_field = std::make_unique<uint32_t>(123);

This applies to all union and table fields and data arrays within vectors and strings. Vector and string data arrays must use the array specialization of std::unique_ptr, which takes the element count as an argument.

VectorView<uint32_t> vec;

To copy a collection to a VectorView, use heap_copy_vec.

std::vector<uint32_t> vec;
fidl::VectorView<uint32_t> vv = heap_copy_vec(vec);

To copy a string to a StringView, use heap_copy_str.

std::string_view str = "hello world";
fidl::StringView sv = heap_copy_str(str);


FIDL provides an Allocator API that enables creating tracking_ptrs to LLCPP objects through a number of allocation algorithms. Currently, BufferThenHeapAllocator, UnsafeBufferAllocator, and HeapAllocator are available in fidl namespace.

The BufferThenHeapAllocator allocates from an in-band fixed-size buffer (can be used for stack allocation), but falls back to heap allocation if the in-band buffer has been exhausted (to avoid unnecessary unfortunate surprises). Be aware that excessive stack usage can cause its own problems, so consider using a buffer size that comfortably fits on the stack, or consider putting the whole BufferThenHeapAllocator on the heap if the buffer needs to be larger than fits on the stack, or consider using HeapAllocator. Allocations must be assumed to be gone upon destruction of the BufferThenHeapAllocator used to make them.

The HeapAllocator always allocates from the heap, and is unique among allocators (so far) in that any/all of the HeapAllocator allocations can out-live the HeapAllocator instance used to make them.

The UnsafeBufferAllocator is unsafe in the sense that it lacks heap failover, so risks creating unfortunate data-dependent surprises unless the buffer size is absolutely guaranteed to be large enough including the internal destructor-tracking overhead. If the internal buffer is exhausted, make<>() will panic the entire process. Consider using BufferThenHeapAllocator instead. Do not use UnsafeBufferAllocator without rigorously testing that the worst-case set of cumulative allocations made via the allocator all fit without a panic, and consider how the rigor will be maintained as code and FIDL tables are changed.


BufferThenHeapAllocator<2048> allocator;
MyStruct s;
s.opt_uint32_field = allocator.make<uint32_t>(123);

The arguments to allocator.make are identical to the arguments to std::make_unique. This also applies to VectorViews.

BufferThenHeapAllocator<2048> allocator;
fidl::VectorView<uint32_t> vec;

To copy a collection to a VectorView using an allocator, use copy_vec.

BufferThenHeapAllocator<2048> allocator;
std::vector<uint32_t> vec;
fidl::VectorView<uint32_t> vv = fidl::copy_vec(allocator, vec);

To create a copy of a string using an allocator, use copy_str.

BufferThenHeapAllocator<2048> allocator;
std::string_view str = "hello world";
fidl::StringView sv = fidl::copy_str(allocator, str);

Unowned pointers

In addition to the managed allocation strategies, it is also possible to directly create pointers to memory unowned by FIDL. This is discouraged, as it is easy to accidentally create use-after-free bugs. unowned_ptr exists to explicitly mark pointers to FIDL-unowned memory.

The unowned_ptr helper is the recommended way to create unowned_ptr_ts, which is more ergonomic than using the unowned_ptr_t constructor directly.

MyStruct s;
uint32_t i = 123;
s.opt_uint32_field = fidl::unowned_ptr(&i);

To create a VectorView from a collection using an unowned pointer to the collection's data array, use unowned_vec.

std::vector<uint32_t> vec;
fidl::VectorView<uint32_t> vv = fidl::unowned_vec(vec);

To create a StringView from unowned memory, use unowned_str.

const char arr[] = {'h', 'e', 'l', 'l', 'o'};
fidl::StringView sv = fidl::unowned_str(arr, 5);

A StringView can also be created directly from string literals without using unowned_ptr.

fidl::StringView sv = "hello world";

Code generator

Mapping FIDL types to low-level C++ types

This is the mapping from FIDL types to Low-Level C++ types which the code generator produces.

FIDL Low-Level C++
bool bool, (requires sizeof(bool) == 1)
int8 int8_t
uint8 uint8_t
int16 int16_t
uint16 uint16_t
int32 int32_t
uint32 uint32_t
int64 int64_t
uint64 uint64_t
float32 float
float64 double
handle, handle? zx::handle
handle<T>,handle<T>? zx::T (subclass of zx::object<T>)
string fidl::StringView
string? fidl::StringView
vector<T> fidl::VectorView<T>
vector<T>? fidl::VectorView<T>
array<T>:N fidl::Array<T, N>
protocol, protocol? zx::channel
request<Protocol>, request<Protocol>? zx::channel
struct Struct struct Struct
struct? Struct struct Struct*
table Table struct Table
union Union struct Union
union? Union struct Union*
union Union struct Union
union? Union struct Union*
enum Foo enum class Foo : data type


Defined in lib/fidl/llcpp/string_view.h

Holds a reference to a variable-length string stored within the buffer. C++ wrapper of fidl_string. Does not own the memory of the contents.

fidl::StringView may be constructed by supplying the pointer and number of UTF-8 bytes (excluding trailing \0) separately. Alternatively, one could pass a C++ string literal, or any value which implements [const] char* data() and size(). The string view would borrow the contents of the container.

It is memory layout compatible with fidl_string.


Defined in lib/fidl/llcpp/vector_view.h

Holds a reference to a variable-length vector of elements stored within the buffer. C++ wrapper of fidl_vector. Does not own the memory of elements.

fidl::VectorView may be constructed by supplying the pointer and number of elements separately. Alternatively, one could pass any value which supports std::data, such as a standard container, or an array. The vector view would borrow the contents of the container.

It is memory layout compatible with fidl_vector.

fidl::Array<T, N>

Defined in lib/fidl/llcpp/array.h

Owns a fixed-length array of elements. Similar to std::array<T, N> but intended purely for in-place use.

It is memory layout compatible with FIDL arrays, and is standard-layout. The destructor closes handles if applicable e.g. it is an array of handles.


The following example table will be used in this section:

table MyTable {
  1: uint32 x;
  2: uint32 y;

Tables can be built using the associated table builder. For MyTable, the associated builder would be MyTable::Builder which can be used as follows:

MyTable table = MyTable::Builder(std::make_unique<MyTable::Frame>())

MyTable::Frame is the table's Frame - essentially its internal storage. The internal storage needs to be allocated separately from the builder because LLCPP maintains the object layout of the underlying wire format.

In addition to assigning fields with std::unique_ptr, any of the allocation strategies previously metioned can be alternatively used.

Bindings library


The low-level C++ bindings depend only on a small subset of header-only parts of the standard library. As such, they may be used in environments where linking against the C++ standard library is discouraged or impossible.

Helper types


Defined in lib/fidl/llcpp/decoded_message.h

Manages a FIDL message in decoded form. The message type is specified in the template parameter T. This class takes care of releasing all handles which were not consumed (std::moved from the decoded message) when it goes out of scope.

fidl::Encode(std::move(decoded_message)) encodes in-place.


Defined in lib/fidl/llcpp/encoded_message.h Holds a FIDL message in encoded form, that is, a byte array plus a handle table. The bytes part points to an external caller-managed buffer, while the handles part is owned by this class. Any handles will be closed upon destruction.

fidl::Decode(std::move(encoded_message)) decodes in-place.

zx_status_t SayHello(const zx::channel& channel, fidl::StringView text,
                     zx::handle token) {
  assert(text.size() <= MAX_TEXT_SIZE);

  // Manually allocate the buffer used for this FIDL message,
  // here we assume the message size will not exceed 512 bytes.
  uint8_t buffer[512] = {};
  fidl::DecodedMessage<example::Animal::SayRequest> decoded(
      fidl::BytePart(buffer, 512));

  // Fill in header and contents

  decoded.message()->text = text;
  // Handle types have to be moved
  decoded.message()->token = std::move(token);

  // Encode the message in-place
  fidl::EncodeResult<example::Animal::SayRequest> encode_result =
  if (encode_result.status != ZX_OK) {
    return encode_result.status;

  fidl::EncodedMessage<example::Animal::SayRequest>& encoded =
  return channel.write(0, encoded.bytes().data(), encoded.bytes().size(),
                       encoded.handles().data(), encoded.handles().size());