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.
- Data-type definition file called
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.
- 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
- 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 asandroid.hardware.bar@1.0::S
, and is found inbar/1.0/types.hal
(becausetypes.hal
is automatically imported).IFooCallback
is interpolated asandroid.hardware.bar@1.0::IFooCallback
using rule 2, but it cannot be found becausebar/1.0/IFooCallback.hal
isn't imported automatically (astypes.hal
is). Thus, rule 3 resolves it toandroid.hardware.foo@1.0::IFooCallback
instead, which is imported viaimport 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 itstypes.hal
) types.hal
fromandroid.hardware.baz@1.0::types
(interfaces inandroid.hardware.baz@1.0
aren't imported)IQux.hal
andtypes.hal
fromandroid.hardware.qux@1.0
Quuz
fromandroid.hardware.quuz@1.0
(assumingQuuz
is defined intypes.hal
, the entiretypes.hal
file is parsed, but types other thanQuuz
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.
|
---|
Rule B | All of the following is true:
|
---|
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 directoryhardware/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:
- All top-level interfaces of the parent package are inherited from by interfaces in the child package.
- New interfaces can also be added the new package (no restrictions about relationships to other interfaces in other packages).
- 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.