Field | Value |
---|---|
Status | Accepted |
Authors | abdulla@google.com, jeffbrown@google.com, with contributions from pascallouis@google.com and abarth@google.com |
Submitted | 2019-04-08 |
Reviewed | 2019-04-23 |
Introduce the notion of a service — a collection of protocols, where there may be one or more instances of the collection.
Today, within the component framework, a service is defined as a single protocol, and only one instance of that protocol may exist in the namespace of a process under /svc
. This prevents us from describing more complex relationships:
FontProvider
and FontProviderV2
Directory
and DirectoryAdmin
, where the latter provides privileged accessPower
for power management, and Ethernet
for network stacksAudioRenderer
, or multiple printers exposing Printer
Providing this flexibility allows a service to be more clearly expressed, without resorting to the use of workarounds like service hubs. With that flexibility, we can define devices as services. Concretely, we plan to evolve /svc/
$Protocol
which implies “only one protocol per process namespace” to:
/svc/$Service/$Instance/$Member
Which instead introduces two additional indirections: a service (e.g., printer, ethernet), and an instance (e.g., default, deskjet_by_desk, e80::d189:3247:5fb6:5808). A path to a protocol will then consist of the following parts:
$Service
— the fully-qualified type of the service, as declared in FIDL$Instance
— the name of an instance of the service, where “default” is used by convention to indicate the preferred (or only) instance made available$Member
— a service member name, as declared in FIDL, where the declared type of that member indicates the intended protocolLet's first consider various flavours of service we aim to support:
A single, unique protocol: ONE instance, ONE protocol:
/svc/fuchsia.Scheduler/default/profile_provider
A composite of multiple protocols: ONE instance, MANY protocols:
/svc/fuchsia.Time/default/network .../rough
Multiple instances of a service, with a single protocol: MANY instances, ONE protocol:
/svc/fuchsia.hardware.Block/0/device .../1/device
Multiple instances, with different sets of protocols: MANY instances, MANY protocols:
/svc/fuchsia.Wlan/ff:ee:dd:cc:bb:aa/device .../power .../00:11:22:33:44:55/access_point .../power
To introduce the notion of a service to FIDL and support the various flavours, we will make the following changes to the FIDL language:
service
keyword.Discoverable
attribute.The service
keyword will allow us to write a service declaration, which we can use to define a set of protocols as members of a service. For example, we can declare the different flavours of service as follows:
A single, unique protocol: ONE instance, ONE protocol:
service Scheduler { fuchsia.scheduler.ProfileProvider profile_provider; };
A composite of multiple protocols: ONE instance, MANY protocols:
service Time { fuchsia.time.Provider network; fuchsia.time.Provider rough; };
Multiple instances of a service, with a single protocol: MANY instances, ONE protocol:
service Block { fuchsia.hardware.block.Device device; };
Multiple instances, with different sets of protocols: MANY instances, MANY protocols
service Wlan { fuchsia.hardware.ethernet.Device device; fuchsia.wlan.AccessPoint access_point; fuchsia.hardware.Power power; };
A service declaration may have multiple members that use the same protocol, but each member declaration must use a different identifier. See “a composite of multiple protocols” above.
When an instance of a service may contain a different set of protocols from another instance, the service declaration declares all possible protocols that may be present in any instance. See “multiple instances, with different sets of protocols” above.
A service declaration makes no mention of the names of specific instances of a service or the URI of the components that offer the service, this is left to the purview of the component framework based on component manifest declarations and use of its APIs at runtime.
Language bindings will be modified to make connecting to a service more convenient. Specifically, they will become more service-oriented, for example:
Connect to the “default” instance of a service, with a single protocol: ONE instance, ONE protocol:
C++:
Scheduler scheduler = Scheduler::Open(); ProfileProviderPtr profile_provider; scheduler.profile_provider().Connect(profile_provider.NewRequest());
Rust:
let scheduler = open_service::<Scheduler>(); let profile_provider: ProfileProviderProxy = scheduler.profile_provider();
Connect to the “default” instance of a service, with multiple protocols: ONE instance, MANY protocols:
C++:
Time time = Time::Open(); ProviderPtr network; time.network().Connect(&network); ProviderPtr rough; time.rough().Connect(&rough);
Rust:
let time = open_service::<Time>(); let network = time.network(); let rough = time.rough();
Connect to multiple instances of a service, with a single protocol: MANY instances, ONE protocol:
C++:
Block block_0 = Block::OpenInstance("0"); DevicePtr device_0; block_0.device().Connect(&device_0); Block block_1 = Block::OpenInstance("1"); DevicePtr device_1; block_1.device().Connect(&device_1);
Rust:
let block_0 = open_service_instance::<Block>("0"); let device_0 = block_0.device(); let block_1 = open_service_instance::<Block>("1"); let device_1 = block_1.device();
Connect to multiple instances of a service, with multiple protocols: MANY instances, MANY protocols:
C++:
Wlan wlan_a = Wlan::OpenInstance("ff:ee:dd:cc:bb:aa"); DevicePtr device; wlan_a.device().Connect(&device); Power power_a; wlan_a.power().Connect(&power_a); Wlan wlan_b = Wlan::OpenInstance("00:11:22:33:44:55"); AccessPoint access_point; wlan_b.access_point().Connect(&access_point); Power power_b; wlan_b.power().Connect(&power_b);
Rust:
let wlan_a = open_service_instance::<Wlan>("ff:ee:dd:cc:bb:aa"); let device = wlan_a.device(); let power_a = wlan_a.power(); let wlan_b = open_service_instance::<Wlan>("00:11:22:33:44:55"); let access_point = wlan_b.access_point(); let power_b = wlan_b.power();
The following illustrates the proposed function signatures.
Note that the Open()
and OpenInstance()
methods also accept an optional parameter to specify the namespace. By default, the process's global namespace will be used (can be retrieved using fdio_ns_get_installed).
// Generated code. namespace my_library { class MyService final { public: // Opens the "default" instance of the service. // // |ns| the namespace within which to open the service or nullptr to use // the process's "global" namespace as defined by |fdio_ns_get_installed()|. static MyService Open(fdio_ns_t* ns = nullptr) { return OpenInstance(fidl::kDefaultInstanceName, ns); } // Opens the specified instance of the service. // // |name| the name of the instance, must not be nullptr // |ns| the namespace within which to open the service or nullptr to use // the process's "global" namespace as defined by |fdio_ns_get_installed()|. static MyService OpenInstance(const std::string& instance_name, fdio_ns_t* ns = nullptr); // Opens the instance of the service located within the specified directory. static MyService OpenAt(zxio_t* directory); static MyService OpenAt(fuchsia::io::DirectoryPtr directory); // Opens a directory of available service instances. // // |ns| the namespace within which to open the service or nullptr to use // the process's "global" namespace as defined by |fdio_ns_get_installed()|. static fidl::ServiceDirectory<MyService> OpenDirectory(fdio_ns_t* ns = nullptr) { return fidl::ServiceDirectory<MyService>::Open(ns); } // Gets a connector for service member "foo". fidl::ServiceConnector<MyService, MyProtocol> foo() const; // Gets a connector for service member "bar". fidl::ServiceConnector<MyService, MyProtocol> bar() const; /* more stuff like constructors, destructors, etc... */ }
And the bindings code:
/// FIDL bindings code. namespace fidl { constexpr char[] kDefaultInstanceName = "default"; // Connects to a particular protocol offered by a service. template <typename Service, typename Protocol> class ServiceConnector final { public: zx_status_t Connect(InterfaceRequest<Protocol> request); }; // A directory of available service instances. template <typename Service> class ServiceDirectory final { public: // Opens a directory of available service instances. // // |ns| the namespace within which to open the service or nullptr to use // the process's "global" namespace as defined by |fdio_ns_get_installed()|. static ServiceDirectory Open(fdio_ns_t* ns = nullptr); // Gets the underlying directory. zxio_t* directory() const; // Gets a list of all available instances of the service. std::vector<std::string> ListInstances(); // Opens an instance of the service. Service OpenInstance(const std::string& name); // Begins watching for services to be added or removed. // // Invokes the provided |callback| to report all currently available services // then reports incremental changes. The callback must outlive the returned // |Watcher| object. // // The watch ends when the returned |Watcher| object is destroyed. [[nodiscard]] Watcher Watch(WatchCallback* callback, async_dispatcher_t* dispatcher = nullptr); // Keeps watch. // // This object has RAII semantics. The watch ends once the watcher has // been destroyed. class Watcher final { public: // Ends the watch. ~Watcher(); }; // Callback which is invoked when service instances are added or removed. class WatchCallback { public: virtual void OnInstanceAdded(std::string name) = 0; virtual void OnInstanceRemoved(std::string name) = 0; virtual void OnError(zx_status_t error) = 0; }; }
Language bindings will further expand upon these by offering convenient methods of iterating through instances of a service, and watching for new instances to become available.
To evolve a service, we can add new protocols to it over time. In order to maintain source compatibility, existing protocols should not be removed, otherwise source compatibility may be broken as users may depend on the code generated from the service by language bindings.
As all protocols within a service are effectively optional, they may or may not be provided at runtime and components should be built for that eventuality, it simplifies the set of problems we face when evolving a service:
To evolve a service itself, we have a similar set of restrictions. A service is not guaranteed to exist within a component's namespace, and a service can be visible at multiple different locations within a namespace, therefore:
We expect service
instances to eventually become ‘first class’ and be allowed to be part of messages, just like protocol P
handles can be passed around as P
or request<P>
. This might take the form of something like service_instance<S>
for a service S
. We will make sure that this extension is possible, without putting working behind it today.
We leave the door open to (and plan on) expanding the kinds of members possible beyond solely allowing protocols. For instance, we may want to have a VMO (handle<vmo>
) exposed by a service:
service DesignedService { ... handle<vmo>:readonly logo; // gif87a };
This proposal should be implemented in phases, so as not to break existing code.
Discoverable
attribute, create appropriate service declarations.Note: at this stage, we should verify that there are no name collisions possible between the old and new directory schemas for services.
Discoverable
attributes from FIDL files.Discoverable
from FIDL and the language bindings.We would need to expand the FIDL tutorial to explain the use of service declarations, and how they interact with protocols. We would then explain the different structures of a service: singleton vs multi-instance, and how the language bindings can be used.
A protocol declaration describes a set of messages that may be sent or received over a channel and their binary representation.
A service declaration describes a capability that is offered as a unit by a service provider. It consists of a service name and zero-or-more named member protocols that clients use to interact with the capability.
The same protocol may appear more than once as a member of a service declaration, with the member's name indicating the intended interpretation of a protocol:
service Foo { fuchsia.io.File logs; fuchsia.io.File journal; };
A component declaration describes a unit of executable software, including the location of the component's binaries and the capabilities (such as services) that it intends to use, expose, or offer to other components.
This information is typically encoded as a component manifest file within a package:
// frobinator.cml { "uses": [{ "service": "fuchsia.log.LogSink" }], "exposes": [{ "service": "fuchsia.frobinator.Frobber" }], "offers": [{ "service": "fuchsia.log.LogSink", "from": "realm", "to": [ "#child" ] }], "program": { "binary": ... } "children": { "child": ... } }
A service instance is a capability that conforms to a given service declaration. On Fuchsia, it is represented as a directory. Other systems may use different service discovery mechanisms.
A component instance is a particular instance of a component with its own private sandbox. At runtime, it uses service instances offered by other components through opening directories in its incoming namespace. Conversely, it exposes its own service instances to other components by presenting them in its outgoing directory. The component manager acts as a broker for service discovery.
This proposal will deprecate, and eventually remove the Discoverable
attribute from FIDL.
There are no changes to the wire format.
If you are introducing a new data type or language feature, consider what changes you would expect users to make to FIDL definitions without breaking users of the generated code. If your feature places any new source compatibility restrictions on the generated language bindings, list those here.
This should have no impact on IPC performance when connecting to the default instance of a service, or an instance known a priori.
To connect to a different instance, where the instance ID is not known a priori, will require the user to list the service's directory and locate the instance before connecting.
There will be a minimal impact on build and binary size, as service definitions must be generated by backends for particular language bindings.
This proposal will allow us to enforce more fine-grained access control, as we can split a service into separate protocols with different access rights.
This proposal has no other effect on security.
Unit tests in the compiler, and changes to the compatibility test suite to check that protocols contained within services can be connected to.
The following questions are explored:
A protocol declaration describes a set of messages that may be sent or received over a channel and their binary representation.
A service declaration describes a capability that is offered as a unit by a service provider. It consists of a service name and zero-or-more named member protocols that clients use to interact with the capability.
service Foo { fuchsia.io.File logs; fuchsia.io.File journal; };
A component declaration describes a unit of executable software, including the location of the component's binaries and the capabilities (such as services) that it intends to use, expose, or offer to other components.
This information is typically encoded as a component manifest file within a package. Example:
// frobinator.cml { "uses": [{ "service": "fuchsia.log.LogSink" }], "exposes": [{ "service": "fuchsia.frobinator.Frobber" }], "offers": [{ "service": "fuchsia.log.LogSink", "from": "realm", "to": [ "#child" ]}], "program": { "binary": ... } "children": { "child": ... } }
A service instance is a capability that conforms to a given service declaration. On Fuchsia, it is represented as a directory. Other systems may use different service discovery mechanisms.
A component instance is a particular instance of a component with its own private sandbox. At runtime, it uses service instances offered by other components through opening directories in its incoming namespace. Conversely, it exposes its own service instances to other components by presenting them in its outgoing directory. The component manager acts as a broker for service discovery.
/svc/fuchsia.Ethernet/
rack.5,port.9/packet_receiver
.In version 1...
service Fonts { FontProvider provider; }; protocol FontProvider { GimmeDaFont(string font_name) -> (fuchsia.mem.Buffer ttf); };
In version 2, an incremental update...
service Fonts { FontProvider provider; FontProvider2 provider2; }; protocol FontProvider2 { compose FontProvider; GetDefaultFontByFamily(string family) -> (string family); };
In version 3, a complete redesign...
service Fonts { [Deprecated] FontProvider provider; [Deprecated] FontProvider provider2; TypefaceChooser typeface_chooser; } protocol TypefaceChooser { GetTypeface(TypefaceCriteria criteria); }; table TypefaceCriteria { 1: Family family; 2: Style style; 3: int weight; };
A component would define multiple services that are exposed through its component manifest. Example:
// frobinator.cml { ... "exposes": [ { "service": "fuchsia.frobinator.Fooer" }, { "service": "fuchsia.frobinator.Barer" }, ], ... }
The component would then implement these services on top of the single underlying resource, but users of these services need not be aware of that fact.