Overview

The Service Manager is a component which large applications like Chromium can use support a cross-platform, multi-process, service-oriented, hyphenated-adjective-laden architecture.

This document covers how to embed the Service Manager into an application as well as how to define and register services for it to manage. If you just want to read about defining services and using common service APIs, skip to the main Services section.

Embedding the Service Manager

To embed the Service Manager, an application should link against the code in //services/service_manager/embedder. This defines a main entry point for most platforms, with a relatively small service_manager::MainDelegate interface for the application to implement. In particular, the application should at least implement GetServiceManifests to provide metadata about the full set of services comprising the application.

Note that Chromium does not currently implement GetServiceManifests for production use of the Service Manager. This is because a bunch of process launching and management logic still lives at the Content layer. As more of this code moves into Service Manager internals, Chromium will start to look more like any other Service Manager embedder.

TODO: Improve embedder documentation here, and include support for in-process service launching once MainDelegate supports it.

Services

A service in this context can be defined as any self-contained body of application logic which satisfies all of the following constraints:

It defines a single implementation of Service to receive interface requests brokered by the Service Manager, and it maintains a connection between this object and the Service Manager using a ServiceBinding.

Its API surface in from or out to other services is restricted exclusively to Mojo interfaces and self-contained client libraries built on those Mojo interfaces. This means no link-time or run-time exposure of the service implementation's internal heap or global state.

It defines a service manifest to declare how the Service Manager should identify and manage instances of the service, as well as what interfaces are exposed to or required from other services in the system.

The Service Manager is responsible for managing the creation and interconnection of individual service instances, whether they are embedded within an existing process or each isolated within dedicated processes. Managed service processes may be sandboxed with any of various supported sandbox configurations.

This section walks through important concepts and APIs throughout service development, and builds up a small working example service in the process.

A Brief Note About Service Granularity

Many developers fret over what the right “size” or granularity is for a service or set of services. This makes sense, and there is always going to be some design tension between choosing a simpler and potentially more efficient, monolithic implementation, versus choosing a more modular but often more complex one.

One classic example of this tension is in the origins of Chromium's device service. The service hosts a number of independent device interfacing subsystems for things like USB, Bluetooth, HID, battery status, etc. You could easily imagine justifying separate services for each of these features, but it was ultimately decided keep them merged together as one service thematically related to hardware device capabilities. Some factors which played into this decision:

There was no clear security benefit to keeping the features isolated from each other.

There was no clear code size benefit to keeping the features isolated from each other -- environments supporting any one of the device capabilities are fairly likely to support several others and would thus likely include all or most of the smaller services anyway.

There isn't really any coupling between the different features in the service, so there would be few code health benefits to building separate services.

Given all of the above conditions, opting for a smaller overall number of services seems likely to have been the right decision.

When making these kinds of decisions yourself, use your best judgment. When in doubt, start a bike-shedding centithread on services-dev@chromium.org.

Implementation

The central fixture in any service implementation is, well, its Service implementation. This is a small interface with really only three virtual methods of practical interest, all optional to implement:

Services implement a subclass of this to work in conjunction with a ServiceBinding so the Service Manager can call into the service with lifecycle events and interface requests from other services.

NOTE: As discussed in Instance Sharing below, your service configuration may allow for the Service Manager to manage many concurrent instances of your service. Whether these instances run in the same shared process or in separate processes, each instance is comprised of exactly one dedicated instance of your actual Service subclass.

Through the rest of this document we‘ll build out a basic working service implementation, complete with a manifest and simple tests. We’ll call it the storage service, and it will provide the basis for all persistent storage capabilities in our crappy operating system hobby project that is doomed to languish forever in an unfinished state.

NOTE: Sheerly for the sake of brevity, example code written here is inlined in headers where it would typically be moved out-of-line.

The first step is usually to imagine and define some mojom API surface to start with. We‘ll limit this example to two mojom files. It’s conventional to keep important constants defined in a separate constants.mojom file:

// src/services/storage/public/mojom/constants.mojom
module storage.mojom;// This string will identify our service to the Service Manager. It will be used// in our manifest when registering the service, and clients can use it when// sending interface requests to the Service Manager if they want to reach our// service.const string kServiceName ="storage";// We'll use this later, in service manifest definitions.const string kAllocationCapability ="allocation";

And some useful interface definitions:

// src/services/storage/public/mojom/block.mojom
module storage.mojom;
interface BlockAllocator{// Allocates a new block of persistent storage for the client. If allocation// fails, |request| is discarded.Allocate(uint64 num_bytes,Block& request);};
interface Block{// Reads and returns a small range of bytes from the block.Read(uint64 byte_offset,uint16 num_bytes)=>(array<uint8> bytes);// Writes a small range of bytes to the block.Write(uint64 byte_offset, array<uint8> bytes);};

Great. This is a basic service implementation. It does nothing useful, but we can come back and fix that some other time.

First, notice that the StorageService constructor takes a service_manager::mojom::ServiceRequest and immediately passes it to the service_binding_ constructor. This is a nearly universal convention among service implementations, and your service will probably do it too. The ServiceRequest is an interface pipe that the Service Manager uses to drive your service, and the ServiceBinding is a helper class which translates messages from the Service Manager into the simpler interface methods of the Service class you've implemented.

StorageService also implements OnBindInterface, which is what the Service Manager invokes (via your ServiceBinding) when it has decided to route another service‘s interface request to your service instance. Note that because this is a generic API intended to support arbitrary interfaces, the request comes in the form of an interface name and a raw message pipe handle. It is the service’s responsibility to inspect the name and decide how (or even if) to bind the pipe. Here we recognize only incoming BlockAllocator requests and drop anything else.

NOTE: Because interface requests are just strongly-type message pipe endpoint wrappers, you can freely construct any kind of interface request over a raw message pipe handle. If you‘re planning to pass the endpoint around, it’s good to do this as early as possible (i.e. as soon as you know the intended interface type) to benefit from your compiler's type-checking and avoid having to pass around both a name and a pipe.

The last piece of our service that we need to lay down is its manifest.

Manifests

A service‘s manifest is a simple static data structure provided to the Service Manager early during its initialization process. The Service Manager combines all of the manifest data it has in order to form a complete picture of the system it’s coordinating. It uses all of this information to make decisions like:

When service X requests interface Q from service Y, should it be allowed?

Were all of the constraints specified in X's request valid, and is X allowed to specify them as such?

Do I need to spawn a new instance of Y to satisfy this request or can I re-use an existing one (assuming there are any)?

If I have to spawn a new process for a new Y instance, how should I configure its sandbox, if at all?

All of this metadata is contained within different instances of the Manifest class.

A Basic Manifest

The most common way to define a service‘s manifest is to place it in its own source target within the service’s C++ client library. To combine the convenience of inline one-time initialization with the avoidance of static initializers, typically this means using a function-local static with base::NoDestructor and service_manager::ManifestBuilder as below. First the header:

Here we've specified only the service name, matching the constant defined in constants.mojom so that other services can easily locate us without a hard-coded string.

With this manifest definition there is no way for our service to reach other services, and there's no way for other services to reach us; this is because we neither expose nor require any capabilities, thus the Service Manager will always block any interface request from us or targeting us.

Exposing Interfaces

Let's expose an “allocator” capability that grants permission to bind a BlockAllocator pipe. We can augment the above manifest definition as follows:

This declares the existence of an "allocator" capability exposed by our service, and specifies that granting a client this capability means granting it the privilege to send our service storage.mojom.BlockAllocator interface requests.

You can list as many interfaces as you like for each exposed capability, and multiple capabilities may list the same interface.

NOTE: You only need to expose an interface through a capability if you want other services to be able to be able to request it through the Service Manager (see Connectors) -- that is, if you handle requests for it in your Service::OnBindInterface implementation.

Contrast this with interfaces acquired transitively, like Block above. The Service Manager does not mediate the behavior of existing interface connections, so once a client has a BlockAllocator they can use BlockAllocator.Allocate to send as many Block requests as they like. Such requests go directly to the service-side implementation of BlockAllocator to which the pipe is bound, and so manifest contents are irrelevant to their behavior.

Getting Access to Interfaces

We don‘t need to add anything else to our storage manifest, but if another service wanted to enjoy access to our amazing storage block allocation facilities, they would need to declare in their manifest that they require our "allocation" capability. For ease of maintenance they would utilitize our publicly defined constants to do this. It’s pretty straightforward:

// src/services/some_other_pretty_cool_service/public/cpp/manifest.cc...// Somewhere along the chain of ManifestBuilder calls....RequireCapability(storage::mojom::kServiceName,
storage::mojom::kAllocationCapability)...

Now some_other_pretty_cool_service can use its Connector to ask the Service Manager for a BlockAllocator from us, like so:

Other Manifest Elements

There are a handful of other optional elements in a Manifest structure which can affect how your service behaves at runtime. See the current Manifest definition and comments as well as ManifestBuilder for the most complete and current information, but some of the more common properties specified by manifests are:

Display Name - This is the string the Service Manager will use to name any new process created to run your service. This string would appear in the Windows Task Manager to identify the service process, for example.

Preloaded Files - On Android and Linux platforms, the Service Manager can open specified files on the service's behalf and pass the corresponding open file descriptor(s) to each new service process on launch.

Packaged Services - A service may declare that it packages another service by including a copy of that service's own manifest. See Packaging for details.

Running the Service

Hooking the service up so that it can be run in a production environment is actually outside the scope of this document at the moment, only because it still depends heavily on the environment in which the Service Manager is embedded. For now, if you want to get your great little service hooked up in Chromium for example, you should check out the sections on this in the very Chromium-centric Intro to Mojo & Services and/or Servicifying Chromium Features documents.

For the sake of this document, we'll focus on running the service in test environments with the service both in-process and out-of-process.

Testing

There are three primary approaches used when testing services, applied in varying combinations:

Standard Unit-testing

This is ideal for covering details of your service's internal components and making sure they operate as expected. There is nothing special here regarding services. Code is code, you can unit-test it.

Out-of-process End-to-end Tests

These are good for emulating a production environment as closely as possible, with your service implementation isolated in a separate process from the test (client) code.

The main drawback to this approach is that it limits your test's ability to poke at or observe internal service state, which can sometimes be useful in test environments (for e.g. faking out some behavior in a predictable manner). In general, supporting such controls means adding test-only interfaces to your service.

And finally in your test code, use TestServiceManager to create a real Service Manager instance within your test environment, configured to know about your storage service.

TestServiceManager allows you to inject an artificial service instance to treat your test suite as an actual service instance. You can provide a manifest for your test to simulate requiring (or failing to require) various capabilities and get a Connector with which to reach your service-under-test. This looks something like:

#include"services/service_manager/public/cpp/manifest_builder.h"#include"services/service_manager/public/cpp/test/test_service.h"#include"services/service_manager/public/cpp/test/test_service_manager.h"#include"services/storage/public/cpp/manifest.h"#include"services/storage/public/mojom/constants.mojom.h"#include"services/storage/public/mojom/block.mojom.h"...
TEST(StorageServiceTest,AllocateBlock){constchar kTestServiceName[]="my_inconsequentially_named_test_service";
service_manager::TestServiceManager service_manager(// Make sure the Service Manager knows about the storage service.{storage::GetManifest,// Also make sure it has a manifest for our test service, which this// test will effectively act as an instance of.
service_manager::ManifestBuilder().WithServiceName(kTestServiceName).RequireCapability(storage::mojom::kServiceName,
storage::mojom::kAllocationCapability).Build()});
service_manager::TestService test_service(
service_manager.RegisterTestInstance(kTestServiceName));
storage::mojom::BlockAllocatorPtr allocator;// This Connector belongs to the test service instance and can reach the// storage service through the Service Manager by virtue of the required// capability above.
test_service.connector()->BindInterface(storage::mojom::kServiceName,
mojo::MakeRequest(&allocator));// Verify that we can request a small block of storage.
storage::mojom::BlockPtr block;
allocator->Allocate(64, mojo::MakeRequest(&block));// Do some stuff with the block, etc...}

In-Process Service API Tests

Sometimes you want to poke at your service primarily through its client API, but you also want to be able to -- either for convenience or out of necessity -- observe or manipulate its internal state within the test code. Running the service in-process is ideal in this case, and in that case there's not much use in involving the Service Manager or dealing with manifests.

Instead you can use a TestConnectorFactory to give yourself a working Connector object which routes interface requests directly to specific service instances which you wire up directly. For a quick example, suppose we had some client library helper function for allocating a block of storage when given a Connector:

Connectors

While the Service interface is what the Service Manager uses to drive a service instance's behavior, a Connector is what the service instance uses to send requests to the Service Manager. This interface is connected when your instance is launched, and ServiceBinding maintains and exposes it on your behalf.

Sending Interface Requests

By far the most common and useful method on Connector is BindInterface, which allows your service to send an interface request to another service in the system, configuration permitting.

Supposing the storage service actually depended on an even lower-level storage service to get at its disk, you could imagine its block allocation code doing something like:

Note that the first argument to this particular overload of BindInterface is a string, but the more generalized form of BindInterface takes a ServiceFilter. See more about these in the section on Service Filters.

Registering Service Instances

One of the superpowers services can be granted is the ability to forcibly inject new service instances into the Service Manager‘s universe. This is done via Connector::ServiceInstance and is still used pretty heavily by Chromium’s browser process. Most services don't need to touch this API.

Usage in Multithreaded Environments

Connectors are not thread-safe, but they do support cloning. There are two useful ways you can associate a new Connector with an existing one on a different thread.

You can Clone the Connector on its own thread and then pass the clone to another thread:

Or you can fabricate a brand new Connector right from where you're standing, and asynchronously associate it with one on another thread:

service_manager::mojom::ConnectorRequest request;
std::unique_ptr<service_manager::Connector> new_connector =
service_manager::Connector::Create(&request);// |new_connector| can be used to start issuing calls immediately, despite not// yet being associated with the establshed Connector. The calls will queue as// long as necessary.
base::PostTaskWithTraits(...[over to the correct thread]...,
base::BindOnce([](service_manager::ConnectorRequest request){
service_manager::Connector* connector =GetMyConnectorForThisThread();
connector->BindConnectorRequest(std::move(request));}));

Identity

Every service instance started by the Service Manager is assigned a globally unique (across space and time) identity, encapsulated by the Identity type. This value is communicated to the service and retained and exposed by ServiceBinding immediately before Service::OnStart is invoked.

There are four components to an Identity:

Service name

Instance ID

Instance group ID

Globally unique ID

You're already quite familiar with the service name: this is whatever the service declared in its manifest, e.g., "storage".

Instance ID

Instance ID is a base::Token qualifier which is simply used to differentiate multiple instances of the service if multiple instances are desired for whatever arbitrary reason. By default instances get an instance ID of zero when started unless a connecting client explicitly requests a specific instance ID. Doing so requires a special manifest-declared capability covered by Additional Capabilities.

A good example of how instance ID can be useful: the "unzip" service in Chrome is used to safely unpack untrusted Chrome extensions (CRX) archives, but we don't want multiple extensions being unpacked by the same process. To support this, Chrome generates a random base::Token for the instance ID it uses when connecting to the "unzip" service, and this elicits the creation of a new service instance in a new isolated process for each such connection. See Service Filters for how this can be done.

Instance Group ID

All created service instances implicitly belong to an instance group, which is also identified by a base::Token. Unless either specially privileged by Additional Capabilities, or the target service is a singleton or shared across groups, the service sending out an interface request can only reach other service instances in the same instance group. See Instance Groups for more information.

Globally Unique ID

Finally, the globally unique ID is a cryptographically secure, unguessably random base::Token value which can be considered unique across all time and space. This can never be controlled by an instance or even by a highly privileged service, and its sole purpose is to ensure that Identity itself can be treated as unique across time and space. See Service Filters and Observing Service Instances for why this property of uniqueness is useful and sometimes necessary.

Instance Sharing

Assuming the Service Manager has decided to allow an interface request due to sufficient capability requirements, it must consider a number of factors to decide where exactly to route the request. The first factor is the instance sharing policy of the target service, declared in its manifest. There are three supported policies:

No sharing - This means the precise identity of the target instance depends on both the instance ID provided by the request‘s ServiceFilter, as well as the instance group either provided by the ServiceFilter or inherited from the source instance’s group.

Shared across groups - This means the precise identity of the target instance still depends on the instance ID provided by the request's ServiceFilter, but the instance group of both the ServiceFilter and the source instance are completely ignored.

Singleton - This means there can be only one instance of the service at a time, no matter what. Instance ID and group are always ignored when connecting to the service.

Based on one of the policies above, the Service Manager determines whether or not an existing service instance matches the parameters specified by the given ServiceFilter in conjunction with the source instance's own identity. If so, that Service Manager will forward the interface request to that instance via Service::OnBindInterface. Otherwise, it will spawn a new instance which sufficiently matches the constraints, and it will forward the request to that new instance.

Instance Groups

Service instances are organized into instance groups. These are arbitrary partitions of instances which can be used by the host application to impose various kinds of security boundaries.

Most services in the system do not have the privilege of specifying the instance group they want to connect into when passing a ServiceFilter to Connector::BindInterface (see Additional Capabilities). As such, most BindInterface calls implicitly inherit the group ID of the caller and only cross outside of the caller's instance group when targeting a service which adopts either a singleton or shared-across-groups sharing policy in its manifest.

Singleton and shared-across-groups services are themselves always run in their own isolated groups.

Service Filters

The most common form of BindInterface calls passes a simple string as the first argument. This is essentially telling the Service Manager that the caller doesn‘t care about any details regarding the target instance’s identity -- it only cares about talking to some instance of the named service.

When a client does care about other details, they can explicitly construct and pass a ServiceFilter object, which essentially provides some subset of the desired target instance's total Identity.

Specifying an instance group or instance ID in a ServiceFilter requires a service to declare additional capabilities in its manifest options.

A ServiceFilter can also wrap a complete Identity value, including the globally unique ID. This filter always only matches a specific instance unique in space and time. So if the identified instance has died and been replaced by a new instance with the same service name, same instance ID, and same instance group, the request will still fail, because the globally unique ID component will never match this or any future instance.

One useful property of targeting a specific Identity is that the client can connect without any risk of eliciting new target instance creation: either the target exists and the request can be routed, or the target doesn't exist and the request will be dropped.

Additional Capabilities

Service manifests can use ManifestOptionsBuilder to set a few additional boolean options controlling their Service Manager privileges:

CanRegisterOtherServiceInstances - If this is true the service can call RegisterServiceInstance on its Connector to forcibly introduce new service instances into the environment.

CanConnectToInstancesWithAnyId - If this is true the service can specify an instance ID in any ServiceFilter it passes to BindInterface.

CanConnectToInstancesInAnyGroup - If this is true the service can specify an instance group ID in any ServiceFilter it passes to BindInterface.

Packaging

A service can declare that it packages another service by nesting that service's manifest within its own.

This signals to the Service Manager that it should defer to the packaging service when it needs a new instance of the packaged service. For example, if we offered up the manifest:

And someone wanted to connect to a new instance of the "banana_stand" service (there's always money in the banana stand), the Service Manager would ask an appropriate "fruit_vendor" instance to do this on its behalf.

NOTE: If an appropriate instance of "fruit_vendor" wasn't already running -- as determined by the rules described in Instance Sharing above -- one would first be spawned by the Service Manager.

In order to support this operation, the fruit_vendor must expose a capability named exactly "service_manager:service_factory" which includes the "service_manager.mojom.ServiceFactory" interface. Then it must handle requests for the service_manager.mojom.ServiceFactory interface in its implementation of Service::OnBindInterface. The implementation of ServiceFactory provided by the service must then handle the CreateService that will be sent by the Service Manager. This call will include the name of the service and the ServiceRequest the new service instance will need to bind.

NOTE: It is this complicated for historical reasons. Expect it to be less complicated soon.

Services can use this for example if, in certain runtime environments, they want to share their process with another service.

FUN FACT: This is actually how Chromium manages all services today, because the Content layer still owns much of the production-ready process launching logic. We have a singleton content_packaged_services service which packages nearly all other registered services in the system, and so the Service Manager defers (via ServiceFactory) nearly all service instance creation operations to Content.

Sandbox Configurations

Service manifests support specifying a fixed sandbox configuration for the service to be launched with when run out-of-process. Currently these values are strings which must match one of the defined constants here.

The most common and default value is "utility", which is a restrictive sandbox configuration and generally a safe choice. For services which must run unsandboxed, use the value "none". Use of other sandbox configurations should be done under the advisory of Chrome's security reviewers.

Observing Service Instances

Services which require the "service_manager:service_manager" capability from the "service_manager" service can connect to the "service_manager" service to request a ServiceManager interface. This can in turn be used to register a new ServiceManagerListener to observe lifecycle events pertaining to all service instances hosted by the Service Manager.