| # 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: |
| |
| . |
| |
| 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 |