PSA: Modularizing the LLCPP API

147 views
Skip to first unread message

Yifei Teng

unread,
Oct 11, 2021, 4:53:50 PM10/11/21
to fidl-dev, anno...@fuchsia.dev

~ If you don't use the low-level C++ bindings you may stop reading now ~


TLDR: Over the next few weeks, we're making the syntax for FIDL calls, events, and replies more consistent and extensible.

Tracking bug: fxbug.dev/85688

Motivation

LLCPP has a relatively large API surface compared to other bindings due to its extended features, in particular different ways to initiate an interaction (asynchronous, synchronous, managed allocation, caller allocation, …). They are currently exposed somewhat arbitrarily using overloads and method name suffixes, leading to bloated and complex interfaces. Take for example a FIDL protocol MyProtocol with a simple two-way method Foo. In a fidl::WireClient<MyProtocol>, there are more than 4 different ways to make the call:


fidl::WireClient<MyProtocol> client(...);

// Asynchronous, managed allocation flavor, taking response callback

client->Foo(arg, [] (fidl::WireResponse<Foo>*) { ... });

// Asynchronous, managed allocation flavor, taking result callback

client->Foo(arg, [] (fidl::UnownedWireResult<Foo>&) { ... });

// Asynchronous, caller-allocating flavor (caller provides request/response buffers)

client->Foo(fidl::BufferSpan(some_buffer, len), arg, &response_context);

// Synchronous, managed allocation flavor

fidl::WireResult result = client->Foo_Sync(arg);

// Synchronous, caller-allocating flavor

fidl::WireUnownedResult result = client->Foo_Sync(

fidl::BufferSpan(some_buffer, len), arg);

// ...


Ahead of exposing LLCPP to the SDK and out-of-tree users, we would like to rationalize the way different flavors as presented to the user, in particular:


  • Reduce the amount of future API churn by strategically placing extension points in the LLCPP API.

  • Increase the similarity between a driver in-process messaging layer and the regular IPC messaging layer.

  • Harmonize caller-allocated flavors which take a byte buffer and FIDL arenas which similarly is a tool for reducing heap allocation.

Summary of changes

See the design described in fxbug.dev/85688 for details. Here we present the most significant changes:

Always use "->" to make calls

Right now some client types use dot (".") to make calls, while others expose it behind a dereference operator ("->"). We're changing the syntax for all client calls, and for sending events, to use the arrow:


fidl::WireSyncClient<MyProtocol> client;

// Before

client.Foo(arg);

// After

client->Foo(arg);


this in turn allows us to…

Hide more advanced messaging flavors behind an accessor

For example, instead of differentiating between managed flavors and caller-allocating flavors purely based on overloads, the caller-allocating flavors will be made available on an interface returned by a .buffer(...) accessor:


// Before...

client.Foo(

    fidl::BufferSpan(request_bytes, request_len),

    arg,

    fidl::BufferSpan(response_bytes, response_len));


// After...

// If a buffer span is used, the same buffer span is used to send the request

// as well as to receive the response in the case of two-way sync calls.

client.buffer(fidl::BufferSpan(bytes, len))->Foo();


// If an arena is used, the encoding/decoding buffers will be requested from the arena.

// The arena is not reset - the user could making multiple calls using the same arena,

// in which case the responses of all those calls would be preserved until the user

// explicitly resets or destroys the arena.

client.buffer(arena)->Foo();


Similarly, the synchronous calls in a fidl::WireClient would be hidden under a .sync() accessor:


fidl::WireClient<MyProtocolClient> client;

// Before

fidl::WireResult result = client->Foo_Sync(arg);

// After

fidl::WireResult result = client.sync()->Foo(arg);


As a bonus point, the accessors compose, so one could write:


// Use the synchronous and caller-allocating flavor from a fidl::WireClient.

fidl::WireUnownedResult result = client.sync().buffer(some_arena)->Foo(arg);


Similar changes would be made to server completers and event senders.

Action Required

No proactive migration/changes are required on your part. The FIDL team will be migrating code on users' behalf. We may ping code maintainers for review or context in specific cases.


If you have in-flight CLs that abruptly stop compiling, it could be due to one of the in-progress changes here. For example, changing "." to "->" when making calls or sending events may help. As of this email, fidl::WireCall have switched over to arrow operators, and fxrev.dev/591661 is an in-progress change migrating caller-allocating flavors to .buffer().


Please let us know on fidl...@fuchsia.dev if you have questions or suggestions. Thanks.


Cheers,

Yifei


Yifei Teng

unread,
Oct 21, 2021, 5:42:38 PM10/21/21
to fidl-dev, anno...@fuchsia.dev
Update: over in fxrev.dev/588834, we are switching the syntax for making FIDL calls with `fidl::WireCall` with caller-allocated buffers to `.buffer(...)`.

The number of caller-allocating uses are quite small in our tree. Here's an example of the delta from a snippet in zxio:

Before:

// Explicitly allocating message buffers to avoid heap allocation.

fidl::Buffer<fidl::WireRequest<fio2::File::Read>> request_buffer;
fidl::Buffer<fidl::WireResponse<fio2::File::Read>> response_buffer;
auto result = fidl::WireCall(fidl::UnownedClientEnd<fio2::File>(control))
                  ->Read(request_buffer.view(), capacity, response_buffer.view());


After:

// Explicitly allocating message buffers to avoid heap allocation.
fidl::SyncClientBuffer<fio2::File::Read> fidl_buffer;
auto result = fidl::WireCall(fidl::UnownedClientEnd<fio2::File>(control))
                  .buffer(fidl_buffer.view())
                  ->Read(capacity);


Notable improvements:
  • Whereas previously one needed to separately allocate two buffers, now we provide a `SyncClientBuffer<Method>` helper that automatically instantiates an inline buffer with an appropriate size.
  • Before one had to separately pass a request/response buffer into the call at different locations. Now all buffer concerns are factored out into the `.buffer(...)` accessor, which takes either a buffer span or an arena and returns an interface for making calls using the provided buffer/arena.
This unlocks some pretty convenient use cases:
  • If your class already has a pretty large arena for building wire domain objects, the same arena may also be used to make the FIDL call without extra allocation.
  • Note that the arena is not reset after making the call. This allows returning a pointer to the response message and consuming the response elsewhere, as long as the buffer/arena lives on. The request and response would live in memory regions allocated from the buffer span or arena.
  • Refer to the method documentation on `.buffer(...)` for more detailed semantics.
Please let us know on fidl...@fuchsia.dev if you have questions or suggestions. Thanks.

Cheers,
Yifei

Yifei Teng

unread,
Oct 29, 2021, 5:18:11 PM10/29/21
to fidl-dev, anno...@fuchsia.dev
Update: over in fxrev.dev/598333, we're extending `fidl::WireSyncClient<P>` (the synchronous client) in preparation for migrating to the published design:
  • Use the dereference operator (i.e. an arrow "->") to make FIDL calls with managed memory allocation.
  • Use the `.buffer(...)` accessor to make FIDL calls with caller-controlled allocation.
An example of the new API:

fidl::WireSyncClient<MyProtocol> client;

// Before

fidl::WireResult result = client.Foo(arg);

// Now

fidl::WireResult result = client->Foo(arg);

// Use |some_arena| to provide the encoding/decoding buffers.

fidl::WireUnownedResult result = client.buffer(some_arena)->Foo(arg);


The behavior of `.buffer(...)` in `fidl::WireSyncClient` is the same as the one from `fidl::WireCall` detailed in the thread above. For example, `SyncClientBuffer<Method>` helper could also be used to create an inline buffer for calling `Method`.

Additionally, we are limiting the number of ways one would access the channel within a `fidl::WireSyncClient<P>`, to make it easy to add features that require exclusive management of channels, without breaking APIs in the future:
  • Removing accessors (e.g. mutable_channel()) that expose a raw pointer/reference to the channel, instead providing a `Bind` function to initialize a default constructed client, and a `TakeClientEnd` function to de-initialize it.
  • Frequently used in tests, the recommended way to "reset" a client and discard the channel within is to simply assign it to "{}", i.e. the following:

fidl::WireSyncClient<MyProtocol> client = ...;

// Make some calls

fidl::WireResult result = client->Foo(arg);

// Close the channel

client = {};


Because the synchronous client is used in a lot of places, we intend to take a staged approach to roll out the user code migrations.

We would be sending out migration CLs to each library/area, keeping maintainers in the loop of API changes. Here's the first one of the batch that is fdio.

Please let us know on fidl...@fuchsia.dev if you have questions or suggestions. Thanks.

Cheers,
Yifei

Yifei Teng

unread,
Nov 5, 2021, 5:33:23 AM11/5/21
to fidl-dev, anno...@fuchsia.dev
Small update: thanks to all who had helped us review and land ~100 migration CLs very quickly over the past week. With fxrev.dev/600185 and fxrev.dev/600058, the deprecated APIs in WireSyncClient have been fully removed.

If you have in-progress CLs that suddenly stopped compiling, that is likely due to one of these API changes:
  • Instead of writing "myclient.Foo()", we need to use the dereference operator (i.e. an arrow, "myclient->Foo()") to make FIDL calls.
  • We removed accessors (e.g. mutable_channel()) that expose a raw pointer/reference to the channel.
Please refer to the immediately preceding thread for more details and ways to migrate. Alternatively, picking one of the landed migration CLs should also give a good idea of the changes.

Please let us know on fidl...@fuchsia.dev if you have questions or suggestions. Thanks.

Cheers,
Yifei

Yifei Teng

unread,
Jan 6, 2022, 6:35:37 PM1/6/22
to fidl-dev, anno...@fuchsia.dev
~ If you don't use the low-level C++ bindings you may stop reading now ~

TLDR:
We added `.buffer(...)` to `fidl::WireClient` and `fidl::WireSharedClient`, to similarly support making FIDL calls with caller-controlled allocation.

How does it work:
Given a `fidl::WireClient client`, when making a FIDL call with `client->SomeMethod(...)`, memory allocation for encoding and transaction bookkeeping is managed automatically by the runtime. This means that the code may internally heap-allocate to deal with large messages, and in the worst case allocating 64 KiB (the maximum on a Zircon channel) for unbounded messages.

When latency and determinism is crucial, sometimes one needs to completely avoid heap allocation in hot paths. LLCPP offers caller-allocating method flavors for this, and `client.buffer(...)` is the way to select those method flavors in an async FIDL client.

This interface is suitable when one needs complete control over memory allocation. Instead of implicitly heap allocating the necessary bookkeeping for in-flight operations, the methods take a raw pointer to a `fidl::WireResponseContext<Method>`, which may be allocated via any means as long as it outlives the duration of this async FIDL call. In addition, necessary encoding buffers are requested from the memory resource passed to `.buffer(...)`, failing the call if there is insufficient space, instead of implicitly allocating from the heap.

You may refer to the documentation on `client.buffer(memory_resource)` and `fidl::WireResponseContext<Method>` for detailed explanation, or check out the unit tests.

Example:

fidl::WireClient<fuchsia_io::File> client(...);


// Create an object to receive the result.

class ResponseContext : public fidl::WireResponseContext<fuchsia_io::File::Read> {

 public:

  void OnResult(fidl::WireUnownedResult<fuchsia_io::File::Read>& result) override {

    // Handle the result.

  }

};

// This object needs to live until the reply comes back.

// In practice we could reserve a pool of contexts, or get it from a

// custom allocator.

ResponseContext response_context;


// Explicitly allocating message buffers to avoid heap allocation.

fidl::AsyncClientBuffer<fuchsia_io::File::Read> fidl_buffer;

client.buffer(fidl_buffer.view())->Read(args, &response_context);


// Arenas may also be used. Note that arenas may heap allocate again

// if the initial buffer is exhausted, so it could be helpful to specify

// sufficient initial space.

fidl::Arena<kSomeInitialSize> arena; 

client.buffer(arena)->Read(args, &response_context);


Please let us know on fidl...@fuchsia.dev if you have questions or suggestions. Thanks.

Cheers, Yifei

Yifei Teng

unread,
Jan 14, 2022, 7:04:57 PM1/14/22
to fidl-dev, anno...@fuchsia.dev

~ If you don't use the low-level C++ bindings you may stop reading now ~


TLDR:

We added .sync() to fidl::WireClient and fidl::WireSharedClient, to support making synchronous calls over the same endpoint that is managed by the client.


Today a WireClient or WireSharedClient exposes both synchronous and asynchronous calls behind the same "->" operator - the synchronous ones are affixed with _Sync in their name. If you have been writing code with ->Foo_Sync(...) calls, those will be changed to .sync()->Foo(...) over the next few days:


fidl::WireClient client(...);

// Before

fidl::WireResult result = client->SomeMethod_Sync(...);

// After

fidl::WireResult result = client.sync()->SomeMethod(...);


Why are we doing this:

When an application already has an async_dispatcher_t, synchronous calls should be avoided where possible, since they introduce blocking behavior. However, since synchronous calls have a straightforward control flow and cleaner syntax compared to async code, they may be preferable in unit tests, and we currently have ~14 files using them in-tree.


This migration captures the above principle in the LLCPP API: when writing my_client->SomeMethod(...), the functions exposed are always asynchronous (or one-way). The synchronous methods are still supported, but they are now tucked away behind a .sync() accessor, and we could later decide whether to enable/disable this feature on different clients.



Please let us know on fidl...@fuchsia.dev if you have questions or suggestions. Thanks.


Cheers, Yifei



Yifei Teng

unread,
Jan 24, 2022, 1:54:43 PM1/24/22
to fidl-dev, anno...@fuchsia.dev

~ If you don't use the low-level C++ bindings you may stop reading now ~


TLDR:

We added fidl::WireSendEvent(const fidl::ServerEnd<Protocol>& endpoint) and fidl::WireSendEvent(const fidl::ServerBindingRef<Protocol>& binding_ref) to support sending events over an endpoint or a server binding reference.


The existing usages of sending events will be migrated over the next few days.



Example:


// Case 1: server endpoint

fidl::ServerEnd<P> server_end(...);


// Before

// Put the server endpoint inside an owning EventSender type

fidl::WireEventSender event_sender{std::move(server_end)};

fidl::Result result = event_sender.SomeEvent(...);

// Put back the server end if it's needed later

server_end = std::move(event_sender.channel());


// After

fidl::Result result = fidl::WireSendEvent(server_end)->SomeEvent(...);


-------------------------------------------


// Case 2: server binding reference

fidl::ServerBindingRef<P> binding_ref = fidl::BindServer(...);


// Before

fidl::Result result = binding_ref->SomeEvent(...);


// After

fidl::Result result = fidl::WireSendEvent(binding_ref)->SomeEvent(...);


There's now one consistent syntax for sending events: wrap the corresponding endpoint/binding reference in fidl::WireSendEvent to obtain an interface for sending events. One may also write fidl::WireSendEvent(..).buffer(..) to send events using caller-controlled allocation, similar to fidl::WireCall.



Why are we doing this:

Apart from more consistent syntax, the stronger underlying motivation is to decouple the LLCPP runtime from particular flavors of domain object types. For instance, fidl::ServerBindingRef previously exposed an operator->() to send events using the wire domain object types (those are generated under the fuchsia_my_lib::wire::... namespace). We are working on a more ergonomic set of domain objects, called "natural types", similar to those found in HLCPP, and it should be possible to send events using those types over the same binding reference, without coupling fidl::ServerBindingRef to either.


Using a free function syntax (fidl::WireSendEvent(...)) allows us to subsequently introduce other free functions to work with natural types.


Please let us know on fidl...@fuchsia.dev if you have questions or suggestions. Thanks.


Cheers, Yifei


Reply all
Reply to author
Forward
0 new messages