Skip to content

Latest commit

 

History

History
196 lines (153 loc) · 8.39 KB

File metadata and controls

196 lines (153 loc) · 8.39 KB

LLCPP tutorials

This section helps learning how to use the Low-Level C++ FIDL bindings. See Getting started for a step-by-step guide to setting up the build and writing a simple client or server from scratch. See Topics for more involved guides and recommendations to using the bindings effectively. See Terminologies for names and concepts that come up frequently in code, and a quick explainer on how to make the right choices.

Getting started

  1. Include FIDL in a C++ project
  2. Write a server
  3. Write a client (async or synchronous)

Topics

Terminologies

Sync vs no sync in clients

Synchronous, or "sync" in short, applies to FIDL calls with a response (two-way calls), and means the call is blocking: a thread making such a call will not return from the call until the response comes back. For example, fidl::WireSyncClient is a client where all two-way calls are synchronous. Similarly, fidl::WireClient has a .sync() accessor which returns an interface for making synchronous calls.

One-way calls do not have a response, hence the concept of synchronousness do not apply to them.

Recommendation

If your code is self-contained (not used by many dependents), determine the level of concurrency required by its business needs:

  • If it does not manage lots of concurrent operations, you may use a synchronous client which leads to easy to read straight-line logic. For example, a short-running command line tool may use fidl::WireSyncClient.

  • If your code manages lots of concurrent operations, it typically has access to an asynchronous dispatcher (async_dispatcher_t*). When choosing between synchronous and asynchronous clients and calls in that case, prefer the asynchronous counterpart. For example, prefer fidl::WireClient without going through .sync() over fidl::WireSyncClient or .sync(). In particular, do not make synchronous calls on a dispatcher thread if the dispatcher is single threaded, to avoid deadlocks.

If your code is a library that's used by many other applications, it will require more careful thought regarding whether it should expose a synchronous or asynchronous interface, depending on the needs of its users. For example, a library using synchronous clients and exposing a synchronous interface will be more difficult to use by highly concurrent applications that schedules their work on asynchronous dispatchers.

The above is general advice, and different asynchronous runtimes may have their own more specific recommendations.

Shared vs no shared in clients

When a client type has "shared" in its name, it may be bound and destroyed on arbitrary threads. See WireSharedClient in the threading guide. It will have a counterpart without "shared", such as WireClient, that must be bound and destroyed on the dispatcher thread.

Recommendation

When choosing between WireClient and WireSharedClient, prefer WireClient unless the threading model or performance requirements of your application necessitates multi-threaded usage of clients. Refer to the threading guide for the many areas of caution when using WireSharedClient. The extra restrictions in WireClient are designed to reduce memory races and use-after-frees. For example, you may use WireClient if your objects all live on the same single-threaded async dispatcher.

Then vs ThenExactlyOnce in two-way calls

When an asynchronous call has a response, there are two ways to specify a callback to receive the result of that call:

  • When you use .ThenExactlyOnce(...), the callback is always called exactly once, delivering the result.
  • When you use .Then(...), the callback is silently discarded when the client object is destroyed, which is suitable for object-oriented code.

Motivation for Then

When making an asynchronous two-way call, the result of that call is delivered back to the application at a later time, after the execution had already left the original scope of making the call. The asynchronous dispatcher would later invoke the follow-up logic you specified when making the call, called a continuation. This means it's easy to use objects after they are destroyed, leading to memory corruptions:

// The following snippet shows an example of use-after-free
// occurring in asynchronous two-way calls.
void Foo(fidl::WireClient<MyProtocol>& client) {
  bool call_ok;
  client->SomeMethod().Then(
      // The following lambda function represents the continuation.
      [&call_ok] (fidl::WireUnownedResult<SomeMethod>& result) {
        // `call_ok` has already gone out of scope.
        // This would lead to memory corruption.
        call_ok = result.ok();
      });
}

A more insidious form of this corruption occurs when the continuation captures the this pointer, and said referenced object also owns the client. Destroying the outer object (in turn, destroying the client) causes all pending two-way calls to fail. As their continuation runs, the this pointer it captured is no longer valid.

Both Then and ThenExactlyOnce registers a continuation for a two-way call. However, Then is designed to mitigate corruption cases like the above. Specifically:

  • Then ensures the provided continuation will be called at most once, until the client is destroyed. You should choose Then if your continuation only captures objects with the same lifetime as the client (e.g. your user object owns the client). Destroying the user object passivates any outstanding callbacks. No concerns of use-after-free.

  • ThenExactlyOnce on the other hand guarantees to call the continuation exactly once. If the client object is destroyed, the continuation receives a cancellation error. You need to ensure any referenced objects are still alive by the time the continuation runs, which may be an unspecified time after the client object is destroyed. You should choose ThenExactlyOnce if your continuation must be called exactly once, such as when interfacing with fpromise completers or FIDL server completers, or during unit tests.

Recommendation

As a rule of thumb:

  • If your callback looks like client_->Foo([this], use Then (note that client_ is a member variable).
  • If your callback looks like
    • client->Foo([completer], or
    • client->Foo([], or
    • client->Foo([&] (common in unit tests),
    • callback captures a weak pointer or a strong pointer,
    • use ThenExactlyOnce.

Do not capture objects of differing lifetimes such that only a subset of the objects are alive when the continuation runs.

Zircon channel transport vs driver transport

A FIDL protocol is associated with a corresponding transport, specified in the FIDL definition, which determines the kinds of resources that may flow through the protocol, and may affect the generated API for sending and receiving messages. The C++ bindings support two transports:

The Zircon channel transport is represented by endpoint types fidl::ClientEnd<SomeProtocol> and fidl::ServerEnd<SomeProtocol>.

The driver transport uses endpoint types fdf::ClientEnd<SomeProtocol> and fdf::ServerEnd<SomeProtocol>.

Arenas

Arenas objects manage a pool of memory buffers and provide efficient allocation. They are used pervasively in wire domain objects and wire messaging to avoid expensive copies.

You may use fidl::Arena to create wire domain objects which live on that arena. See memory management.

When using protocols over the driver transport with wire domain objects, fdf::Arena objects should be used to allocate the buffers needed to encode messages.