blob: 264636f74c294424addd0791f95ce91db08a5d23 [file] [log] [blame] [view]
# Structure of new C++ FIDL Bindings
**Author**: yifeit@google.com
**Date proposed**: Dec 3, 2021
This document describes a structural overview of the new C++ bindings: how
we can build it from parts of the existing C++ bindings, and how the API looks
like down the road for the collection of C++ bindings.
## Summary of changes
Rather than a monolithic whole, we view bindings as made up of two layers:
* The domain objects (the generated FIDL structures e.g. struct, table, ...
**data**). The domain objects are [usable standalone][standalone-fidl-rfc].
* The messaging layer (the necessary code to speak methods over a protocol,
receive events, .... **behavior**).
Gluing those two layers together we have a **message dispatcher**. A message
dispatcher is responsible for reading and writing messages over a transport, and
invoking the corresponding logic in the messaging layer. The message dispatcher
is not responsible for implementing encoding/decoding; that is reserved for the
domain objects and the messaging layer.
Over time, gradually move away from the "LLCPP" name, and call it "wire", e.g.
wire C++ bindings, wire types, wire messaging layer.
The new C++ bindings is an extension on top of the wire bindings, supporting
the domain types similar to those from HLCPP, henceforth termed "natural types",
over a messaging layer API that has the same shape as the wire messaging layer,
and the same thread-safety properties.
In doing so, we would allow the two set of domain objects (wire/natural) to each
further specialize in their intended niche in performance/ergonomics trade-offs,
instead of trying to design one set of domain object to satisfy conflicting
needs.
## Domain objects
The unified bindings support two types of domain objects: **wire** objects and
**natural** objects:
* **wire** objects are optimized for in-place decoding. They are layout
compatible with the FIDL wire format. In practice, wire objects are imported
from LLCPP generated code.
* This allows LLCPP to be "rebranded" as the wire subset of C++ bindings.
* They are generated in the `fuchsia_my_lib::wire` namespace.
* **natural** objects are high level domain objects that optimize for
ergonomics. We'll generate slightly different domain object types in the
unified bindings compared to those found in the HLCPP bindings, since we would
like to make quite a few breaking API changes.
* We could start prototyping the messaging layer with the HLCPP domain
objects, then move to natural objects as they're implemented.
* They are generated in the `fuchsia_my_lib` namespace (vs HLCPP domain
objects which are generated in the `fuchsia::my::lib` namespace). This
reflects our intention to users that the natural types are the default,
since they are easier to use and reasonably performant. One should only
turn to the wire objects when optimizing logic in the critical path, or
when needing to precisely control memory allocation.
### Wire domain objects should not depend on HLCPP nor natural domain objects
The wire domain objects are a popular choice among drivers, some of which place
tight controls over memory allocations. By avoiding depending on HLCPP or
natural domain objects, we offer an option to driver authors to rule out FIDL
objects which implicitly allocate at build time. Those users could continue to
depend on the wire subset of the C++ bindings.
### Sharing types between wire and natural domain objects
For numerical types and types with equivalent layouts such as enums and bits, we
should use the same C++ type for wire and natural objects, since the space for
performance/ergonomics trade-offs there are negligible, and any differences are
more accidental than purposeful.
### Optional conversions to/from HLCPP
To ease migration, we may consider supporting constructing a natural object from
the corresponding HLCPP object, and conversions to the HLCPP objects. We should
not plan to do this until we have a compelling use case.
We propose externalizing all the conversion logic to a third set of libraries,
for example generated under `#include <fidl/my.library/hlcpp_migration.h>`, that
is not imported by either wire or unified libraries. This makes conversions
explicit hence marginally harder to use (e.g.
`fidl::ToHLCPP<NaturalType>(natural_types)` from library
`fuchsia_my_lib_hlcpp_migration`) but easier to implement. The purpose of these
conversion libraries is only to assist in piecewise migration. Over the long
run, users should fully migrate their applications to natural types, thus
dropping any HLCPP dependencies.
We should not generate user-defined conversion operators directly in the
definition of natural objects. Doing so implies that the natural objects code
would depend on the HLCPP domain objects code. However as discussed above, we'd
like to avoid introducing a dependency edge from wire objects to HLCPP, so we
would not be able to add those conversion operators to bits and enums. We could
imagine using `fidl::ToHLCPP` or the like for bits and enums, and conversion
constructors/operators for the rest, but that can be more confusing than always
using `fidl::ToHLCPP`.
### Dependency structure
From the perspective of build dependencies, this results in the following
conceptual dependency graph:
![Figure: natural domain objects depend on both wire bindings and HLCPP domain
objects](images/cpp-fidl-bindings-dependencies.png).
Naming-wise, we'll adopt the following:
* **wire domain objects/wire types**: the subset of C++ domain objects that are
layout compatible with the FIDL wire format.
* **natural domain objects/natural types**: the subset of C++ domain objects
that focus on ergonomics and have recursive ownership semantics.
* **wire bindings**: the subset of C++ FIDL bindings API that work with wire
domain objects.
* **new C++ bindings**: the full set of C++ FIDL bindings API that support
both wire domain objects and natural domain objects.
* **HLCPP domain objects**: the existing domain objects generated from
`fidlgen_hlcpp`.
* **HLCPP bindings**: the existing HLCPP bindings that use HLCPP domain objects.
### New features in the natural domain objects
#### Non-resource types are copy & move constructible
In C++ it is idiomatic to use the copy constructor for deep copying, and provide
a move constructor for users to optimize away the copy.
#### Resource types are move constructible
Resource types would have a guaranteed deleted copy constructor to prevent API
breakages when it later acquires handles. Together with copy constructors above,
we can remove `fidl::Clone` from natural domain objects - the remaining use case
would be duplicating the handles within resource types, a fallible and rare
operation.
#### Struct API sketch
Do not expose raw fields directly; prefer setters instead (except when
supporting designated initializers). This has several benefits:
* We can swap out the internal representation to be more efficient without
breaking source compatibility.
* We could add validation as fields are being set. For example, instead of
triggering a late constraint validation failure on sending, we could panic
from the setter in debug mode to catch some of these illegal states earlier.
```c++
Foo foo;
foo
.set_foo(42)
.set_bar(...);
// To support designated initializers, we could generate an auxiliary struct
// that is an aggregate, and which can be used to initialize the `Foo` struct.
// See full idea at https://godbolt.org/z/f53j3KfPG.
Foo foo{{
// The inner brace constructs a |Foo::Storage_| which the users are
// discouraged from spelling out directly.
.foo = ...,
.bar = ...
}};
```
## Messaging layer
The messaging layer builds on top of the domain objects and adds APIs to
send and receive FIDL messages over protocol endpoints.
Given the representative protocol below:
```fidl
library fuchsia.example;
type GreetError = enum {
NOT_UNDERSTOOD = 1;
};
protocol Speak {
Greet(struct { msg string; }) -> (struct { s int32; foo string; });
GreetTwo(struct { msg1 string; msg2 string; }) -> (struct { s int32; foo string; });
Ask() -> (struct { answers vector<string>; });
OneWay(struct { a int32; });
EmptyAck() -> ();
TryGreet(struct { msg string; }) -> (struct { reply string; }) error GreetError;
TryEmptyAck() -> () error int32;
-> OnWordSpoken(struct { word string; });
};
```
We propose the following generated New C++ bindings API.
### Client
Below are the possible APIs on an asynchronous client. In all cases, the user
may create a client like so:
```c++
#include <fidl/fuchsia.example/cpp/fidl.h>
fidl::Client<fuchsia_example::Speak> client(std::move(client_end), dispatcher);
```
#### Result callbacks vs response callbacks
In asynchronous two-way calls, the user passes a continuation in the form of a
callback. There are some choices for surfacing errors:
- **Response callbacks** are only invoked when the client receives a response
from the server. They are suitable when there is no need to propagate framework
error information on a per-call basis. The async client callbacks in HLCPP are
response callbacks.
- **Result callbacks** are invoked with a result object containing either a
success or an error. They are suitable when the user needs to propagate errors
for each FIDL call to their originators. For example, a server may need to make
another FIDL call as part of fulfilling an existing FIDL call, and need to fail
the original call in case of errors.
**Result callbacks** are the only option in the unified bindings, since they
convey strictly more information than response callbacks, and meshes better with
newer FIDL features such as unknown interactions. The user is free to return
early if they are not interested in framework errors.
#### Natural API
Overall guiding principles:
* Use `fit::result` in return values. `fit::result` is widely used in newer
Fuchsia C++ code, and we should integrate with where the overall ecosystem is
heading.
* Do not flatten method requests/responses into multiple arguments. For example,
the generated method for `GreetTwo` should not take multiple input arguments
for `msg1` and `msg2`. Rather, it should take a single struct
(`SpeakGreetTwoRequest`) representing the request body. This makes things
consistent when we support table and unions as top level request/responses.
#### Natural API, asynchronous
We define a `fidl::Result<MethodMarker>` templated class:
`fidl::Result<FooMethod>` represents the result of calling method `FooMethod`:
```c++
template <typename Method>
class fidl::Result { ... };
```
The precise definition that it expands to depends on the shape of the method:
* When the method does not use the error syntax:
- When the method response has no body: `fidl::Result<FooMethod>` inherits
`fit::result<fidl::Error>`.
- When the method response has a body: `fidl::Result<FooMethod>` inherits
`fit::result<fidl::Error, FooMethodPayload>`, where `fidl::Error` is a type
representing any framework error or protocol level terminal errors such as
epitaphs.
* When the method uses the error syntax:
- When the method response payload is an empty struct:
`fidl::Result<FooMethod>` inherits
`fit::result<fidl::ErrorsIn<FooMethod>>` (see `ErrorsIn` below).
- When the method response payload is not an empty struct:
`fidl::Result<FooMethod>` inherits
`fit::result<fidl::ErrorsIn<FooMethod>, FooMethodPayload>`.
`ErrorsIn` is used to implement [error folding][error-folding] of framework
and domain errors, such that one may query `is_ok()` once on the result
object to determine whether the call succeeded at all layers of abstractions:
```c++
// |ErrorsIn<Method>| represents the set of all possible errors during
// |Method|:
// - Framework errors
// - Domain errors in the |Method| error syntax
class fidl::ErrorsIn<fuchsia_example::Speak::TryGreet> {
public:
bool is_framework_error();
fidl::Error framework_error();
bool is_domain_error();
fuchsia_example::GreetError domain_error();
// Prints a description of the error.
std::string FormatDescription();
};
```
A **result callback** has the following signature:
```c++
[] (fidl::Result<Method>& result) { ... }
```
Now we present some examples using the types and FIDL definition above:
```c++
//
// Examples without error syntax
//
// Natural API, async, response has a body.
client->Greet({std::string("hi")}).Then(
[] (fidl::Result<fuchsia_example::Speak::Greet>& result) {
// fidl::Result<fuchsia_example::Speak::Greet> =
// fit::result<
// fidl::Error,
// fuchsia_example::Speak::GreetPayload>;
assert(result.is_ok());
// Access the payload.
result->foo();
result->bar();
});
// Natural API, async, response has no body.
client->EmptyAck().Then(
[] (fidl::Result<fuchsia_example::Speak::Greet>& result) {
// fidl::Result<fuchsia_example::Speak::Greet> =
// fit::result<fidl::Error>;
assert(result.is_ok());
// No payload to access...
});
//
// Error syntax and error folding example
//
// Natural API, async, response has a body that's not empty struct
client->TryGreet({std::string("hi")}).Then(
[] (fidl::Result<fuchsia_example::Speak::TryGreet>& result) {
// fidl::Result<fuchsia_example::Speak::TryGreet> =
// fit::result<
// fidl::ErrorsIn<fuchsia_example::Speak::TryGreet>,
// fuchsia_example::SpeakTryGreetPayload>;
// Check both framework and domain error.
if (!result.is_ok()) {
FX_LOGS(ERROR) << "TryGreet failed: " << result.error_value();
// Digging deeper, if desired.
if (result.error_value().is_domain_error()) {
FX_LOGS(ERROR) << "TryGreet failed with domain error: "
<< result.error_value().domain_error();
}
return;
}
// Access the payload...
result->reply();
});
// Natural API, async, response has a body that's empty struct
client->TryEmptyAck().Then(
[] (fidl::Result<fuchsia_example::Speak::TryGreet>& result) {
// fidl::Result<fuchsia_example::Speak::TryGreet> =
// fit::result<fidl::ErrorsIn<fuchsia_example::Speak::TryEmptyAck>>;
// Check both framework and domain error.
if (!result.is_ok()) {
FX_LOGS(ERROR) << "TryEmptyAck failed: " << result.error_value();
return;
}
// No payload to access...
});
```
#### Natural API, synchronous
For reference, there are many more uses of [async clients][hlcpp-async-clients]
compared to [sync clients][hlcpp-sync-clients] in HLCPP. Support for synchronous
clients in the natural messaging API will be hinging on strong user needs.
Nonetheless, we could imagine how a synchronous client could work, to not design
ourselves into a corner should the need for synchronous clients arise. It can
use the same `fidl::Result<Method>` treatment and error folding approach. The
main difference is the delivery of the outcome (return value in case of sync vs
callback in case of async).
```c++
// Construct a sync client.
// |fidl::SyncClient| is the natural counterpart to |fidl::WireSyncClient|.
fidl::SyncClient client(std::move(client_end));
// Natural API, sync.
fidl::Result<fuchsia_example::Speak::Greet> result =
client->Greet({std::string("hi")});
// Alternatively, one may elide the template argument if using |fit::result|.
// Note that |fidl::Result| is just an alias.
fit::result result = client->Greet({std::string("hi")});
// Check error
bool ok = result.is_ok();
fidl::Error error = result.error_value();
// Use fields
int32_t s = result->s();
std::string foo = result->foo();
// Example for responses with absent bodies:
fit::result<fidl::Error> result = client->EmptyAck({std::string("hi")});
// Example for one way calls:
fit::result<fidl::Error> result = client->OneWay({42});
// Example for error syntax:
fit::result<fidl::ErrorsIn<TryEmptyAck>> result = client->TryEmptyAck();
```
Note that two-way methods with absent response bodies have the same return value
as one-way methods.
#### Using wire domain objects for individual calls
In order to make method calls with wire domain types, the user would write
`.wire()` in front of the method calls. `.wire()` returns a pointer to the wire
client implementation used to make calls.
#### Wire API, synchronous
```c++
fidl::SyncClient client(std::move(client_end));
// Wire API, sync
fidl::WireResult<fuchsia_example::Greet> result =
client.wire()->Greet(fidl::StringView("hi"));
// Check error
bool ok = result.ok();
fidl::Error error = result.error();
// Use fields
int32_t s = result->s;
fidl::StringView foo = result->foo;
```
#### Wire API, asynchronous
```c++
fidl::Client client(std::move(client_end), dispatcher);
// Wire API, async
client.wire()->Greet(fidl::StringView("hi")).Then(
[] (fidl::WireUnownedResult<fuchsia_examples::Speak::Greet>& result) {
// Check error
bool ok = result.ok();
fidl::Error error = result.error();
// Use fields
int32_t s = result->s;
fidl::StringView foo = result->foo;
});
```
Note that the wire API subset should be the identical as the API found in the
LLCPP bindings, with the only difference being the additional `.wire()` to go
from a client interface speaking natural types to a client interface speaking
wire types.
### Input argument optimizations
For a function that send a domain object of type `T`:
- Take `T` if the domain object is a resource type, to always consume the
object.
- Take `const T&` otherwise, to skip a copy.
### Out-of-scope features
#### No wire sync call flavors in C++14
C++14 does not support guaranteed return value optimization (RVO), which is
necessary in the design of wire synchronous calls to avoid any copies. We can
consider alternative approaches to supporting wire sync calls if a strong need
arises.
The practical implication is that functions like
`client.wire().sync()->Greet(...)` won't exist when compiling under C++14.
#### No caller-allocating flavors with natural objects
The use cases that call for controlling allocation don't intersect with those
served by ergonomic but less performant natural domain objects.
### Event handling
We define `fidl::Event<FooMethod>` to represent an event:
```c++
template <typename Method>
class fidl::Event<Method> { ... };
```
* When the event does not use the error syntax:
- When the event has a body, `fidl::Event<FooMethod>` inherits
`FooMethodPayload`, i.e. the domain object type representing the event body.
See [https://fxbug.dev/42171535](https://fxbug.dev/42171535).
- When the event has no body, `fidl::Event<FooMethod>` is empty.
* When the event uses the error syntax:
- When the success payload is not an empty struct, `fidl::Event<FooMethod>`
inherits `fit::result<DomainError, FooMethodPayload>`, where
`DomainError` is the corresponding domain error type for that
event.
- When the success payload is an empty struct, `fidl::Event<FooMethod>`
inherits `fit::result<DomainError>`.
Note: an alternative to the second bullet is to simply omit the corresponding
`fidl::Event<FooMethod>` in case of events with absent bodies. Here we choose to
always leave in the argument since it matches the existing convention in LLCPP,
and could make for less special cases in typing out a list of event handlers.
We'll generate interface classes like the following:
```c++
class fidl::AsyncEventHandler<fuchsia_example::Speak> {
virtual void OnWordSpoken(
fidl::Event<fuchsia_example::Speak::OnWordSpoken>&) {}
};
```
If needed, the cascading inheritance pattern could be used in event handlers to
support choosing the domain object family on a per-event basis (wire or
natural):
```c++
class fidl::AsyncEventHandler<fuchsia_example::Speak> : public
fidl:WireAsyncEventHandler<fuchsia_example::Speak> {
public:
// Users may override this method to receive natural types...
virtual void OnWordSpoken(
fidl::Event<fuchsia_example::Speak::OnWordSpoken>& event) {}
// Or override this method to receive wire types...
virtual void OnWordSpoken(
fidl::WireEvent<fuchsia_example::Speak::OnWordSpoken>* event) override {
OnWordSpoken(fidl::Event<fuchsia_example::Speak::OnWordSpoken>{
/* Logic to convert wire types to natural types, potentially */
/* using a decoder. */
});
}
};
```
### Server
We define `fidl::Request<FooMethod>` to represent the request of method
`FooMethod`:
```c++
template <typename Method>
class fidl::Request<Method> { ... };
```
* When the method request has a body, `fidl::Request<FooMethod>` inherits
`FooMethodRequest`, i.e. the domain object type representing the request body.
* When the method has a request but the request has no body,
`fidl::Request<FooMethod>` is empty.
We'll generate interface classes like the following:
```c++
class fidl::Server<fuchsia_example::Speak> {
using GreetRequest = fidl::Request<fuchsia_example::Speak::Greet>;
// fuchsia_example::Speak::GreetCompleter speaks wire and natural types
//
// Exposed methods:
// completer.Reply(fuchsia_example::SpeakGreetResponse);
// completer.wire().Reply(int32_t s, fidl::StringView foo);
// completer.wire().buffer(MemoryResource resource).Reply(int32_t s, fidl::StringView foo);
virtual void Greet(GreetRequest& request, GreetCompleter::Sync& completer) = 0;
};
```
Sending natural domain objects as replies and events can be supported in a way
that's similar to the client API, by adding methods that take natural types and
hiding wire APIs behind a `.wire()` accessor.
Example server:
```c++
class MyServer : public fidl::Server<fuchsia_example::Speak> {
public:
void Greet(GreetRequest& request, GreetCompleter::Sync& completer) {
// Access request.
std::string msg = request.msg();
// Send response.
completer.Reply({ZX_OK, msg});
}
};
```
## Alternatives, unknowns, future work
### Unknown interaction handling considerations
[RFC-0138: Handling unknown interactions][rfc-0138] extends the result union
used in method responses with a third variant:
```
type result = union {
1: response struct { pong Pong; };
2: err uint32;
3: framework_err zx.Status; // used to communication unknown method errors.
};
```
When exposing this feature in the unified bindings, we should aim for the
following to make it safe and ergonomic:
* Server authors should not be able to manually respond with the `framework_err`
variant from inside a method handler. By definition, the method is known to
the server if the code reaches a particular method handler.
* Client users should still be able to use `fit::result` types, which has two
variants, even when the result union on the wire has three variants.
To achieve these, our idea is to decouple the physical shape of the result union
from the user API. On the server side, the `framework_err` is completely hidden
from the user - the binding runtime knows when to response with that error. On
the client side, the `framework_err` combined into other kinds of framework
errors.
This means for example the server completer of a flexible two-way method using
error syntax will take `fit::result<DomainError, FooPayload>`, and that of
a flexible two-way method not using the error syntax will simply take
`FooPayload`.
On the client side, users can ask the framework error object if the method was
unknown:
```c++
class fidl::ErrorsIn<ErrorSyntaxFlexible> {
bool is_framework_error();
fidl::Error framework_error();
// Potential API:
// framework_error().is_unknown_interaction();
bool is_domain_error();
fuchsia_example::GreetError domain_error();
std::string FormatDescription();
};
client->ErrorSyntaxFlexible().Then(
[] (fidl::Result<ErrorSyntaxFlexible>& result) {
if (result.is_error()) {
if (result.error_value().is_unknown_interaction()) {
// The server doesn't recognize this flexible method.
}
}
});
```
### Domain object compile time validations
Potentially worth exploring, but not part of our MVP, is the accepted [RFC-0051:
Safer structs for C++][rfc-0051] which was never implemented in HLCPP.
### Alternative client API ideas
#### Combine both wire and natural APIs onto the same client object
Earlier iterations of the design called for `fidl::Client` to support both wire
and natural flavors on the same object, via function overloading. A corner case
is that FIDL method calls with zero request arguments or with numerical request
arguments may result in identical function parameters in one-way and synchronous
functions. Because C++ doesn't support overload selection via return type, there
is no way for the user to explicitly indicate whether the natural flavor or the
wire flavor should be used.
```c++
// This is ambiguous. Should |result| contain a wire response or a natural response?
auto result = client->Ask_Sync();
// Same here.
auto result = client->OneWay(42);
```
There are a couple of solutions to this. We could use marker types to
disambiguate between those ([see source #1][godbolt-alt-client]):
```c++
auto result_of_std_string = c->Ask(Sync, WithStdTypes);
auto result_of_char_start = c->Ask(Sync, WithWireTypes);
```
This approach has the unfortunate downside of adding even more runtime
delegation and generated code: the `WithWireTypes` overload needs to forward its
arguments and return values to the wire client implementation.
We could use some base class trickery to put the wire flavors under a `LowLevel`
namespace ([see source #2][godbolt-alt-client]):
```c++
auto result_of_natural = c.Foo();
auto result_of_wire b = c.LowLevel::Foo();
```
This is feasible but requires adding `LowLevel` to the list of dangerous
identifiers for name collision handling. Note that we probably wouldn't want to
use `c.Wire::Foo()` since `Wire` is way more likely to collide with a user
defined FIDL method called `Wire` (think wiring funds or wiring memory for
example).
In contrast, as found in the earlier proposal, we could expose an accessor
`.wire()` to return a pointer to the object for making wire requests ([see
source #3][godbolt-alt-client]):
```c++
auto result_of_natural = c.Foo();
auto result_of_wire b = c.wire()->Foo();
```
Users who only wish to use the wire subset may create a
`fidl::WireClient<MyProtocol>` instead. Calling wire flavors on a unified client
is thus only reserved for cases where the majority of the calls benefit from the
ergonomics of natural objects, but some particular methods would like to use the
wire flavor for performance. Therefore, it looks like the last option is the
easiest path to disambiguate, while only slightly burdening the wire flavor
invocation (add `.wire()`).
### Receiving mixed wire/natural objects in a server
A challenge here is receiving both types of requests. Users could implement the
wire interface, then convert the arguments to natural types in a subset of
methods in that interface via an adaptor that we would provide.
<!-- link labels -->
[error-folding]: https://fxbug.dev/42144195
[godbolt-alt-client]: https://godbolt.org/z/z6cTWsqe4
[hlcpp-async-clients]: https://cs.opensource.google/search?q=fidl::InterfacePtr%20-f:golden&ss=fuchsia%2Ffuchsia
[hlcpp-sync-clients]: https://cs.opensource.google/search?q=fidl::SynchronousInterfacePtr%20-f:golden&ss=fuchsia%2Ffuchsia
[rfc-0051]: /docs/contribute/governance/rfcs/0051_safer_structs_for_cpp.md
[rfc-0138]: /docs/contribute/governance/rfcs/0138_handling_unknown_interactions.md
[standalone-fidl-rfc]: /docs/contribute/governance/rfcs/0120_standalone_use_of_fidl_wire_format.md