This document captures strategies, hints, and best practices for solving typical challenges enountered when converting existing Chromium code to services. It is assumed that you have already read the high-level documentation on what a service is.
Note that throughout the below document we link to CLs to illustrate the strategies being made. Over the course of time code tends to shift, so it is likely that the code on trunk does not exactly match what it was at the time of the CLs. When necessary, use the CLs as a starting point for examining the current state of the codebase with respect to these issues (e.g., exactly where a service is embedded within the content layer).
For the basic nuts and bolts of how to create a new service, see the documentation on adding a new service. This section gives questions that you should answer in order to shape the design of your service, as well as hints as to which answers make sense given your situation.
The Service Manager can either:
Which of these policies the Service Manager employs is determined by the contents of your service manifest: the former is the default, while the latter is selected by informing the Service Manager that your service requires the service_manager:all_users capability(example).
In practice, there is one user ID per-BrowserContext, so the question becomes: Is your Service a global or keyed by BrowserContext? In considering this question, there is one obvious hint: If you are converting per-Profile classes (e.g., KeyedServices), then your service is almost certainly going to be per-user. More generally, if you envision needing to use any state related to the user (e.g., you need to store files in the user's home directory), then your service should be per-user.
Conversely, your service could be a good fit for being global if it is a utility that is unconcerned with the identity of the requesting client (e.g., the data decoder service, which simply decodes untrusted data in a separate process.
At the start (and potentially even long-term), your service will likely not actually run in its own process but will rather be embedded in the browser process. This is especially true in the common case where you are converting existing browser-process code.
You then have a question: Where should it be embedded? The answer to this question hinges on the nature and location of the code that you are converting:
//content is the obvious choice if you are converting existing //content code (e.g., the Device Service). Global services are embedded by content::ServiceManagerContext, while per-user services are naturally embedded by content::BrowserContext.
If your service is converting existing //chrome code, then you will need to embed your service in //chrome rather than //content. Global services are embedded by ChromeContentBrowserClient, while per-user services are embedded by ProfileImpl.
If you are looking to convert all or part of a component (i.e., a feature in //components) into a service, the question arises of whether your new service is worthy of being in //services (i.e., is it a foundational service?). If not, then it can be placed in //components/services. See this document for discussion of this point.
If your service is embedded in the browser process, it will run on the IO thread by default. You can change that by specifying a task runner as part of the information for constructing your service. In particular, if the code that you are converting is UI-thread code, then you likely want your service running on the UI thread. Look at the changes to profile_impl.cc in this CL to see an example of setting the task runner that a service should be run on as part of the factory for creating the service.
In creating your service, you likely have two goals:
Those two goals are not the same, and to some extent are at tension:
To satisfy the first, you need to build out the API surface of the service to a sufficient degree for the anticipated use cases.
To satisfy the second, you need to convert all clients of the code that you are servicifying to instead use the service, and then fold that code into the internal implementation of the service.
Whatever your goals, you will need to proceed incrementally if your project is at all non-trivial (as they basically all are given the nature of the effort). You should explicitly decide what your approach to incremental bringup and conversion will be. Here some approaches that have been taken for various services:
These all have tradeoffs:
Which makes sense depends both on the nature of the existing code and on the priorities for doing the servicification. The first two enable making the service available for new use cases sooner at the cost of leaving legacy code in place longer, while the last is most suitable when you want to be very exacting about doing the servicification cleanly as you go.
As you servicify code running on Android, you might find that you need to port interfaces that are served in Java. Here is an example CL that gives a basic pattern to follow in doing this.
You also might need to register JNI in your service. That is simple to set up, as illustrated in this CL. (Note that that CL is doing more than just enabling the Device Service to register JNI; you should take the register_jni.cc file added there as your starting point to examine the pattern to follow).
Finally, it is possible that your feature will have coupling to UI process state (e.g., the Activity) via Android system APIs. To handle this challenging issue, see the section on Coupling to UI.
Services are supported on iOS, with the usage model in //ios/web being very close to the usage model in //content. More specifically:
If you have a use case or need for services on iOS, contact firstname.lastname@example.org. For general information on the motivations and vision for supporting services on iOS, see the high-level servicification design doc (in particular, search for the mentions of iOS within the doc).
Connecting to services directly from Blink is fully supported. This CL gives a basic example of connecting to an arbitrary service by name from Blink (look at the change to SensorProviderProxy.cpp as a starting point).
Below, we go through strategies for some common challenges encountered when servicifying features that have Blink as a client.
It is a common pattern in Blink's layout tests to mock a remote Mojo interface in JS. This CL illustrates the basic pattern for porting such mocking of an interface hosted by //content/browser to an interface hosted by an arbitrary service (see the changes to mock-battery-monitor.js).
In the course of servicifying a feature that has Blink as a client, you might encounter cases where the feature implementation has dependencies on Blink public headers (e.g., defining POD structs that are used both by the client and by the feature implementation). These dependencies pose a challenge:
To meet this challenge, you have two options:
You must think carefully about the scoping of the connection being made from Blink. In particular, some feature requests are necessarily scoped to a frame in the context of Blink (e.g., geolocation, where permission to access the interface is origin-scoped). Servicifying these features is then challenging, as Blink has no frame-scoped connection to arbitrary services (by design, as arbitrary services have no knowledge of frames or even a notion of what a frame is).
After a long discussion, the policy that we have adopted for this challenge is the following:
AFTER SERVICIFYING THE FEATURE IN QUESTION
Notably, from the renderer's POV essentially nothing changes here.
In the longer term, this will still be the basic model, only with “the browser” replaced by “the Navigation Service” or “the web permissions broker”.
Some feature implementations have hard constraints on coupling to UI on various platforms. An example is NFC on Android, which requires the Activity of the view in which the requesting client is hosted in order to access the NFC platform APIs. This coupling is at odds with the vision of servicification, which is to make the service physically isolatable. However, when it occurs, we need to accommodate it.
The high-level decision that we have reached is to scope the coupling to the feature and platform in question (rather than e.g. introducing a general-purpose FooServiceDelegate), in order to make it completely explicit what requires the coupling and to avoid the coupling creeping in scope.
The basic strategy to support this coupling while still servicifying the feature in question is to inject a mechanism of mapping from an opaque “context ID” to the required context. The embedder (e.g., //content) maintains this map, and the service makes use of it. The embedder also serves as an intermediary: It provides a connection that is appropriately context-scoped to clients. When clients request the feature in question, the embedder forwards the request on along with the appropriate context ID. The service impl can then map that context ID back to the needed context on-demand using the mapping functionality injected into the service impl.
To make this more concrete, see this CL.
You might find that your feature includes singletons that are shut down as part of //content's shutdown process. As part of decoupling the feature implementation entirely from //content, the shutdown of these singletons must be either ported into your service or eliminated:
See this thread for more discussion of this issue.
It is often the case that browsertests reach directly into what will become part of the internal service implementation to either inject mock/fake state or to monitor private state.
This poses a challenge: As part of servicification, no code outside the service impl should depend on the service impl. Thus, these dependencies need to be removed. The question is how to do so while preserving testing coverage.
To answer this question, there are several different strategies. These strategies are not mutually-exclusive; they can and should be combined to preserve the full breadth of coverage.
To emphasize one very important point: it is in general necessary to leave some test of end-to-end functionality, as otherwise it is too easy for bustage to slip in via e.g. changes to how services are registered. See this thread for further discussion of this point.