Interface versioning

HIDL requires every interface written in HIDL be versioned. After a HAL interface is published, it is frozen and any further changes must be made to a new version of that interface. While a given published interface can't be modified, it can be extended by another interface.

HIDL code structure

HIDL code is organized in user-defined types, interfaces, and packages:

  • User-defined types (UDTs). HIDL provides access to a set of primitive data types that can be used to compose more complex types via structures, unions, and enumerations. UDTs are passed to methods of interfaces, and can be defined at the level of a package (common to all interfaces) or locally to an interface.
  • Interfaces. As a basic building block of HIDL, an interface consists of UDT and method declarations. Interfaces can also inherit from another interface.
  • Packages. Organizes related HIDL interfaces and the data types on which they operate. A package is identified by a name and a version and includes the following:
    • Data-type definition file called types.hal.
    • Zero or more interfaces, each in their own .hal file.

The data-type definition file types.hal contains only UDTs (all package-level UDTs are kept in a single file). Representations in the target language are available to all interfaces in the package.

Versioning philosophy

A HIDL package (such as android.hardware.nfc), after being published for a given version (such as 1.0), is immutable; it cannot be changed. Modifications to the interfaces in the package or any changes to its UDTs can take place only in another package.

In HIDL, versioning applies at the package level, not at the interface level, and all interfaces and UDTs in a package share the same version. Package versions follow semantic versioning without the patch level and build-metadata components. Within a given package, a minor version bump implies the new version of the package is backwards-compatible with the old package and a major version bump implies the new version of the package isn't backwards-compatible with the old package.

Conceptually, a package can relate to another package in one of several ways:

  • Not at all.
  • Package-level backwards-compatible extensibility. This occurs for new minor-version uprevs (next incremented revision) of a package; the new package has the same name and major version as the old package, but a higher minor version. Functionally, the new package is a superset of the old package, meaning:
    • Top-level interfaces of the parent package are present in the new package, though the interfaces might have new methods, new interface-local UDTs (the interface-level extension described below), and new UDTs in types.hal.
    • New interfaces can also be added to the new package.
    • All data types of the parent package are present in the new package and can be handled by the (possibly reimplemented) methods from the old package.
    • New data types can also be added for use by either new methods of uprev'ed existing interfaces, or by new interfaces.
  • Interface-level backwards-compatible extensibility. The new package can also extend the original package by consisting of logically separate interfaces that simply provide additional functionality, and not the core one. For this purpose, the following might be desirable:
    • Interfaces in the new package need recourse to the data types of the old package.
    • Interfaces in new package can extend interfaces of one or more old packages.
  • Extend the original backwards-incompatibility. This is a major-version uprev of the package and there need not be any correlation between the two. To the extent that there is, it can be expressed with a combination of types from the older version of the package, and inheritance of a subset of old-package interfaces.

Interface structuring

For a well structured interface, adding new types of functionality that aren't part of the original design should require a modification to the HIDL interface. Conversely, if you can or expect to make a change on both sides of the interface that introduces new functionality without changing the interface itself, then the interface isn't structured.

Treble supports separately compiled vendor and system components in which the vendor.img on a device and the system.img can be compiled separately. All interactions between vendor.img and system.img must be explicitly and thoroughly defined so they can continue to work for many years. This includes many API surfaces, but a major surface is the IPC mechanism HIDL uses for interprocess communication on the system.img/vendor.img boundary.

Requirements

All data passed through HIDL must be explicitly defined. To ensure an implementation and client can continue to work together even when compiled separately or developed on independently, data must adhere to the following requirements:

  • Can be described in HIDL directly (using structs enums, etc.) with semantic names and meaning.
  • Can be described by a public standard such as ISO/IEC 7816.
  • Can be described by a hardware standard or physical layout of hardware.
  • Can be opaque data (such as public keys, ids, etc.) if necessary.

If opaque data is used, it must be read only by one side of the HIDL interface. For example, if vendor.img code gives a component on the system.img a string message or vec<uint8_t> data, that data cannot be parsed by the system.img itself; it can only be passed back to vendor.img to interpret. When passing a value from vendor.img to vendor code on system.img or to another device, the format of the data and how it is to be interpreted must be exactly described and is still part of the interface.

Guidelines

You should be able to write an implementation or client of a HAL using only the .hal files (i.e. you shouldn't need to look at the Android source or public standards). We recommend specifying the exact required behavior. Statements such as "an implementation might do A or B" encourage implementations to become intertwined with the clients they are developed with.

HIDL code layout

HIDL includes core and vendor packages.

Core HIDL interfaces are those specified by Google. The packages they belong to start with android.hardware. and are named by subsystem, potentially with nested levels of naming. For example, the NFC package is named android.hardware.nfc and the camera package is android.hardware.camera. In general, a core package has the name android.hardware.[name1].[name2]…. HIDL packages have a version in addition to their name. For example, the package android.hardware.camera might be at version 3.4; this is important, as the version of a package affects its placement in the source tree.

All core packages are placed under hardware/interfaces/ in the build system. The package android.hardware.[name1].[name2]… at version $m.$n is under hardware/interfaces/name1/name2//$m.$n/; package android.hardware.camera version 3.4 is in directory hardware/interfaces/camera/3.4/. A hard-coded mapping exists between the package prefix android.hardware. and the path hardware/interfaces/.

Non-core (vendor) packages are those produced by the SoC vendor or ODM. The prefix for non-core packages is vendor.$(VENDOR).hardware. where $(VENDOR)refers to an SoC vendor or OEM/ODM. This maps to the path vendor/$(VENDOR)/interfaces in the tree (this mapping is also hard-coded).

Fully qualified user-defined-type names

In HIDL, every UDT has a fully qualified name that consists of the UDT name, the package name where the UDT is defined, and the package version. The fully qualified name is used only when instances of the type are declared and not where the type itself is defined. For example, assume package android.hardware.nfc, version 1.0 defines a struct named NfcData. At the site of the declaration (whether in types.hal or within an interface's declaration), the declaration simply states:

struct NfcData {
    vec<uint8_t> data;
};

When declaring an instance of this type (whether within a data structure or as a method parameter), use the fully qualified type name:

android.hardware.nfc@1.0::NfcData

The general syntax is PACKAGE@VERSION::UDT, where:

  • PACKAGE is the dot-separated name of a HIDL package (e.g., android.hardware.nfc).
  • VERSION is the dot-separated major.minor-version format of the package (e.g., 1.0).
  • UDT is the dot-separated name of a HIDL UDT. Since HIDL supports nested UDTs and HIDL interfaces can contain UDTs (a type of nested declaration), dots are used to access the names.

For example, if the following nested declaration was defined in the common types file in package android.hardware.example version 1.0:

// types.hal
package android.hardware.example@1.0;
struct Foo {
    struct Bar {
        // …
    };
    Bar cheers;
};

The fully qualified name for Bar is android.hardware.example@1.0::Foo.Bar. If, in addition to being in the above package, the nested declaration were in an interface called IQuux:

// IQuux.hal
package android.hardware.example@1.0;
interface IQuux {
    struct Foo {
        struct Bar {
            // …
        };
        Bar cheers;
    };
    doSomething(Foo f) generates (Foo.Bar fb);
};

The fully qualified name for Bar is android.hardware.example@1.0::IQuux.Foo.Bar.

In both cases, Bar can be referred to as Bar only within the scope of the declaration of Foo. At the package or interface level, you must refer to Bar via Foo: Foo.Bar, as in the declaration of method doSomething above. Alternatively, you could declare the method more verbosely as:

// IQuux.hal
doSomething(android.hardware.example@1.0::IQuux.Foo f) generates (android.hardware.example@1.0::IQuux.Foo.Bar fb);

Fully qualified enumeration values

If a UDT is an enum type, then each value of the enum type has a fully qualified name that starts with the fully qualified name of the enum type, followed by a colon, then followed by the name of the enum value. For example, assume package android.hardware.nfc, version 1.0 defines an enum type NfcStatus:

enum NfcStatus {
    STATUS_OK,
    STATUS_FAILED
};

When referring to STATUS_OK, the fully qualified name is:

android.hardware.nfc@1.0::NfcStatus:STATUS_OK

The general syntax is PACKAGE@VERSION::UDT:VALUE, where:

  • PACKAGE@VERSION::UDT is the exact same fully qualified name for the enum type.
  • VALUE is the value's name.

Auto-inference rules

A fully qualified UDT name doesn't need to be specified. A UDT name can safely omit the following:

  • The package, e.g. @1.0::IFoo.Type
  • Both package and version, e.g. IFoo.Type

HIDL attempts to complete the name using auto-interference rules (lower rule number means higher priority).

Rule 1

If no package and version is provided, a local name lookup is attempted. Example:

interface Nfc {
    typedef string NfcErrorMessage;
    send(NfcData d) generates (@1.0::NfcStatus s, NfcErrorMessage m);
};

NfcErrorMessage is looked up locally, and the typedef above it is found. NfcData is also looked up locally, but as it is not defined locally, rule 2 and 3 are used. @1.0::NfcStatus provides a version, so rule 1 doesn't apply.

Rule 2

If rule 1 fails and a component of the fully qualified name is missing (package, version, or package and version), the component is autofilled with information from the current package. The HIDL compiler then looks in the current file (and all imports) to find the autofilled fully qualified name. Using the example above, assume the declaration of ExtendedNfcData was made in the same package (android.hardware.nfc) at the same version (1.0) as NfcData, as follows:

struct ExtendedNfcData {
    NfcData base;
    // … additional members
};

The HIDL compiler fills out the package name and version name from the current package to produce the fully qualified UDT name android.hardware.nfc@1.0::NfcData. As the name exists in the current package (assuming it is imported properly), it is used for the declaration.

A name in the current package is imported only if one of the following is true:

  • It is imported explicitly with an import statement.
  • It is defined in types.hal in the current package

The same process is followed if NfcData was qualified by only the version number:

struct ExtendedNfcData {
    // autofill the current package name (android.hardware.nfc)
    @1.0::NfcData base;
    // … additional members
};

Rule 3

If rule 2 fails to produce a match (the UDT isn't defined in the current package), the HIDL compiler scans for a match within all imported packages. Using the above example, assume ExtendedNfcData is declared in version 1.1 of package android.hardware.nfc, 1.1 imports 1.0 as it should (see Package-Level Extensions), and the definition specifies only the UDT name:

struct ExtendedNfcData {
    NfcData base;
    // … additional members
};

The compiler looks for any UDT named NfcData and finds one in android.hardware.nfc at version 1.0, resulting in a fully qualified UDT of android.hardware.nfc@1.0::NfcData. If more than one match is found for a given partially qualified UDT, the HIDL compiler throws an error.

Example

Using rule 2, an imported type defined in the current package is favored over an imported type from another package:

// hardware/interfaces/foo/1.0/types.hal
package android.hardware.foo@1.0;
struct S {};

// hardware/interfaces/foo/1.0/IFooCallback.hal
package android.hardware.foo@1.0;
interface IFooCallback {};

// hardware/interfaces/bar/1.0/types.hal
package android.hardware.bar@1.0;
typedef string S;

// hardware/interfaces/bar/1.0/IFooCallback.hal
package android.hardware.bar@1.0;
interface IFooCallback {};

// hardware/interfaces/bar/1.0/IBar.hal
package android.hardware.bar@1.0;
import android.hardware.foo@1.0;
interface IBar {
    baz1(S s); // android.hardware.bar@1.0::S
    baz2(IFooCallback s); // android.hardware.foo@1.0::IFooCallback
};
  • S is interpolated as android.hardware.bar@1.0::S, and is found in bar/1.0/types.hal (because types.hal is automatically imported).
  • IFooCallback is interpolated as android.hardware.bar@1.0::IFooCallback using rule 2, but it cannot be found because bar/1.0/IFooCallback.hal isn't imported automatically (as types.hal is). Thus, rule 3 resolves it to android.hardware.foo@1.0::IFooCallback instead, which is imported via import android.hardware.foo@1.0;).

types.hal

Every HIDL package contains a types.hal file containing UDTs that are shared among all interfaces participating in that package. HIDL types are always public; regardless of whether a UDT is declared in types.hal or within an interface declaration, these types are accessible outside of the scope where they are defined. types.hal isn't meant to describe the public API of a package, but rather to host UDTs used by all interfaces within the package. Due to the nature of HIDL, all UDTs are a part of the interface.

types.hal consists of UDTs and import statements. Because types.hal is made available to every interface of the package (it is an implicit import), these import statements are package-level by definition. UDTs in types.hal can also incorporate UDTs and interfaces thus imported.

For example, for an IFoo.hal:

package android.hardware.foo@1.0;
// whole package import
import android.hardware.bar@1.0;
// types only import
import android.hardware.baz@1.0::types;
// partial imports
import android.hardware.qux@1.0::IQux.Quux;
// partial imports
import android.hardware.quuz@1.0::Quuz;

The following are imported:

  • android.hidl.base@1.0::IBase (implicitly)
  • android.hardware.foo@1.0::types (implicitly)
  • Everything in android.hardware.bar@1.0 (including all interfaces and its types.hal)
  • types.hal from android.hardware.baz@1.0::types (interfaces in android.hardware.baz@1.0 aren't imported)
  • IQux.hal and types.hal from android.hardware.qux@1.0
  • Quuz from android.hardware.quuz@1.0 (assuming Quuz is defined in types.hal, the entire types.hal file is parsed, but types other than Quuz aren't imported).

Interface-level versioning

Each interface within a package resides in its own file. The package the interface belongs to is declared at the top of the interface using the package statement. Following the package declaration, zero or more interface-level imports (partial or whole-package) might be listed. For example:

package android.hardware.nfc@1.0;

In HIDL, interfaces can inherit from other interfaces using the extends keyword. For an interface to extend another interface, it must have access to it via an import statement. The name of the interface being extended (the base interface) follows the rules for type-name qualification explained above. An interface can inherit only from one interface; HIDL doesn't support multiple inheritance.

The uprev versioning examples below use the following package:

// types.hal
package android.hardware.example@1.0
struct Foo {
    struct Bar {
        vec<uint32_t> val;
    };
};

// IQuux.hal
package android.hardware.example@1.0
interface IQuux {
    fromFooToBar(Foo f) generates (Foo.Bar b);
}

Uprev rules

To define a package package@major.minor, either A or all of B must be true:

Rule A "Is a start minor version": All previous minor versions, package@major.0, package@major.1, …, package@major.(minor-1) must not be defined.
OR
Rule B

All of the following is true:

  1. "Previous minor version is valid": package@major.(minor-1) must be defined and follow the same rule A (none of package@major.0 through package@major.(minor-2) are defined) or rule B (if it is an uprev from @major.(minor-2));

    AND

  2. "Inherit at least one interface with the same name": There exists an interface package@major.minor::IFoo that extends package@major.(minor-1)::IFoo (if the previous package has an interface);

    AND

  3. "No inherited interface with a different name": There must not exist package@major.minor::IBar that extends package@major.(minor-1)::IBaz, where IBar and IBaz are two different names. If there is an interface with the same name, package@major.minor::IBar must extend package@major.(minor-k)::IBar such that no IBar exists with a smaller k.

Because of rule A:

  • The package can start with any minor version number (for example, android.hardware.biometrics.fingerprint starts at @2.1.)
  • The requirement "android.hardware.foo@1.0 isn't defined" means the directory hardware/interfaces/foo/1.0 shouldn't even exist.

However, rule A doesn't affect a package with the same package name but a different major version (for example, android.hardware.camera.device has both @1.0 and @3.2 defined; @3.2 doesn't need to interact with @1.0.) Hence, @3.2::IExtFoo can extend @1.0::IFoo.

Provided the package name is different, package@major.minor::IBar can extend from an interface with a different name (for example, android.hardware.bar@1.0::IBar can extend android.hardware.baz@2.2::IBaz). If an interface doesn't explicitly declare a super type with the extend keyword, it extends android.hidl.base@1.0::IBase (except IBase itself).

B.2 and B.3 must be followed at the same time. For example, even if android.hardware.foo@1.1::IFoo extends android.hardware.foo@1.0::IFoo to pass rule B.2, if an android.hardware.foo@1.1::IExtBar extends android.hardware.foo@1.0::IBar, this is still not a valid uprev.

Uprev interfaces

To uprev android.hardware.example@1.0 (defined above) to @1.1:

// types.hal
package android.hardware.example@1.1;
import android.hardware.example@1.0;

// IQuux.hal
package android.hardware.example@1.1
interface IQuux extends @1.0::IQuux {
    fromBarToFoo(Foo.Bar b) generates (Foo f);
}

This is a package-level import of version 1.0 of android.hardware.example in types.hal. While no new UDTs are added in version 1.1 of the package, references to UDTs in version 1.0 are still needed, hence the package-level import in types.hal. (The same effect could have been achieved with an interface-level import in IQuux.hal.)

In extends @1.0::IQuux in the declaration of IQuux, we specified the version of IQuux that is being inherited (disambiguation is required because IQuux is used to declare an interface and to inherit from an interface). As declarations are simply names that inherit all package and version attributes at the site of the declaration, the disambiguation must be in the name of the base interface; we could have used the fully qualified UDT as well, but that would have been redundant.

The new interface IQuux doesn't re-declare method fromFooToBar() it inherits from @1.0::IQuux; it simply lists the new method it adds fromBarToFoo(). In HIDL, inherited methods can not be declared again in the child interfaces, so the IQuux interface cannot declare the fromFooToBar() method explicitly.

Uprev conventions

Sometimes interface names must rename the extending interface. We recommend that enum extensions, structs, and unions have the same name as what they extend unless they are sufficiently different to warrant a new name. Examples:

// in parent hal file
enum Brightness : uint32_t { NONE, WHITE };

// in child hal file extending the existing set with additional similar values
enum Brightness : @1.0::Brightness { AUTOMATIC };

// extending the existing set with values that require a new, more descriptive name:
enum Color : @1.0::Brightness { HW_GREEN, RAINBOW };

If a method can have a new semantic name (for instance fooWithLocation) then that is preferred. Otherwise, it should be named similarly to what it is extending. For example, the method foo_1_1 in @1.1::IFoo can replace the functionality of the foo method in @1.0::IFoo if there is no better alternative name.

Package-level versioning

HIDL versioning occurs at the package level; after a package is published, it is immutable (its set of interfaces and UDTs cannot be changed). Packages can relate to each other in several ways, all of which are expressible via a combination of interface-level inheritance and building of UDTs by composition.

However, one type of relationship is strictly defined and must be enforced: Package-level backwards-compatible inheritance. In this scenario, the parent package is the package being inherited from and the child package is the one extending the parent. Package-level backwards-compatible inheritance rules are as follows:

  1. All top-level interfaces of the parent package are inherited from by interfaces in the child package.
  2. New interfaces can also be added the new package (no restrictions about relationships to other interfaces in other packages).
  3. New data types can also be added for use by either new methods of uprev'ed existing interfaces, or by new interfaces.

These rules can be implemented using HIDL interface-level inheritance and UDT composition, but require meta-level knowledge to know these relationships constitute a backwards-compatible package extension. This knowledge is inferred as follows:

If a package meets this requirement, hidl-gen enforces backwards-compatibility rules.