For Developers‎ > ‎Design Documents‎ > ‎Mojo‎ > ‎

Versioning

Overview

Note: You don't need to worry about versioning if you don't care about backward compatibility. Specifically, Chrome is updated atomically, therefore there is no need to consider versioning for Mojo interfaces used internally between different components (blink, network, etc.).

Services extend their interfaces to support new functionalities over time and clients try to use those new functionalities when they are available. If services and clients are not updated at the same time, it is important for them to be able to communicate with each other using different snapshots (versions) of interfaces.

This document shows how to extend Mojo interfaces in a backward-compatible way. Changing interfaces in a non-backward-compatible way is not discussed, because in that case communications using different snapshots of interfaces are not possible anyway. You can make whatever changes to interfaces you want.

Mojo structs

You can use the MinVersion=k attribute to indicate from which version a field is introduced. Assume you have the following struct:

struct Employee {
  uint64 employee_id;
  string name;
};

And you would like to add a birthday field to it, you can do:

struct Employee {
  uint64 employee_id;
  string name;
  [MinVersion=1] Date? birthday;
};

By default fields belong to version 0. New fields should be appended, with the MinVersion attribute set to a number greater than existing versions. “Append” here means the ordinal numbers of existing fields should not be changed.

What is ordinal number? Ordinal numbers determine memory layout of structs and also used as method IDs for interfaces. Implicitly, ordinal numbers are assigned to fields according to their position. In the example above, employee_id’s ordinal number is 0, name is 1. You can explicitly specify ordinal numbers:
  • For any struct or interface, either every field/method is explicitly assigned an ordinal value or none of them is.
  • For an N-field struct or N-method interface, the ordinal values (if explicitly  assigned) should be 0 ~ N-1.

You can reorder fields if you want, but you will have make sure those of existing fields unchanged. For example, the following struct is also backward-compatible:

// Reorder fields with explicit ordinal numbers.
struct Employee {
  uint64 employee_id@0;
  [MinVersion=1] Date? birthday@2;
  string name@1;
};

Please also note that if a newly-added field is Mojo object or handle, it must be nullable. (The “?” suffix. Please see more details about nullable types here.)

Assume old_app is built against Employee version 0 while new_app is built against Employee version 1. When old_app passes an Employee struct to new_app, the birthday field is set to the default value (null in this case). When an Employee struct is passed in the opposite direction, the birthday field is silently discarded. If you do pass new version structs to where old version definitions are used, you must design APIs carefully so that discarding fields does not result in semantic difference or confusion!

Mojo interfaces

There are two dimensions to extend a Mojo interface:
  • Appending new parameters to existing methods or method callbacks.
Parameter lists are treated as Mojo structs internally. So the rules of extending structs apply to parameter lists, too. The only difference is that the version number is scoped to the whole interface, instead of individual parameter lists.

Please note that adding callback to a method which doesn’t have callback previously is non-backward-compatible.

  • Appending new methods.
Similarly, you can reorder methods with ordinal numbers explicitly specified, as long as the ordinal numbers of existing methods are not changed.

For example:

// Old version:
interface HumanResourceDatabase {
  AddEmployee(Employee employee) => (bool success);
  QueryEmployee(uint64 id) => (Employee? employee);
};

// New version:
interface HumanResourceDatabase {
  AddEmployee(Employee employee) => (bool success);

  QueryEmployee(uint64 id, [MinVersion=1] bool retrieve_finger_print)
      => (Employee? employee,
          [MinVersion=1] array<uint8>? finger_print);

  [MinVersion=1]
  AttachFingerPrint(uint64 id, array<uint8> finger_print)
      => (bool success);
};

Similar to structs, when you pass the parameter list of a method call/callback to someone using an old version, unrecognized fields are silently discarded. However, if the method call itself is not recognized, it is considered a fatal error and causes the message pipe to be closed. For example, when an app makes an AttachFingerPrint() call to another app built against HumanResourceDatabase version 0, the underlying message pipe is closed.

In C++ bindings, InterfacePtr<> defines the following methods to query/assert interface version:

// Queries the max version that the remote side supports. On completion, the
// result will be returned as the input of |callback|. The version number will
// also be cached by this interface pointer.
void QueryVersion(const Callback<void(uint32_t)>& callback);

// If the remote side doesn't support the specified version, it will close the
// message pipe.
//
// After calling RequireVersion() with a version not supported by the remote
// side, it is guaranteed all subsequent calls to interface methods, no matter
// what versions they belong to, will not be executed.
void RequireVersion(uint32_t version);

You can find similar methods in other language bindings.

Mojo enums

By default, enums are not extensible, which means they are not supposed to be extended in the future. When the bindings see incoming unknown values for non-extensible enum types, it will automatically close the pipe. Users don’t have to manually check received enum values.

If you want an enum to be extensible in the future, you should add [Extensible] attribute:

[Extensible]
enum Department {
  SALES,
  DEV
};

And later you can extend it without breaking backward compatibility:

[Extensible]
enum Department {
  SALES,
  DEV,
  [MinVersion=1]RESEARCH
};

With extensible enums, you may receive unknown enum values and you are responsible to handle that gracefully. Assume old_app is built against Department version 0 while new_app is built against Department version 1. During development of old_app, you should keep in mind that the app may receive unknown values for Department. Otherwise, when new_app sends RESEARCH to old_app, things will break. There is an auto-generated helper function to help you:

// In the same namespace as Department.
inline bool IsKnownEnumValue(Department value);

As you can see, [MinVersion=1] here is really just for documentation purpose. It doesn’t make a behavioral difference if you build new_app against:

[Extensible]
enum Department {
  SALES,
  DEV,
  RESEARCH
};

Other tips

How to deprecate methods / struct fields / enum values?

Their names actually don’t matter at all. As long as the ordinal number (for methods and struct fields) or value (for enums) remains the same, you can modify the name without breaking backward compatibility. Therefore, say you would like to deprecate the method AddEmployee() in HumanResourceDatabase, you can simply rename the method:

interface HumanResourceDatabase {
  Deprecated_AddEmployee(Employee employee) => (bool success);
  …
};

Then at the service side you can provide a dummy/error-reporting implementation for it.
Comments