Compatibility is an important attribute of CHRE, which is accomplished through a combination of thoughtful API and framework design. When we refer to compatibility within the scope of CHRE, there are two main categories:
Code compatibility, which means that a nanoapp can be recompiled to run on a new platform without needing any code changes. CHRE provides this cross-device compatibility for all nanoapps which are written in a supported programming language (C99 or C++11), and reference only the standard CHRE APIs and mandatory standard library elements (or have these standard library functions statically linked into their binary).
Binary compatibility, which means that a nanoapp binary which has been compiled against a particular version of the CHRE API can run on a CHRE framework implementation which was compiled against a different version of the API. This is also called cross-version compatibility. Note that this does not mean that a nanoapp compiled against one version of the CHRE API can be compiled against a different version of the CHRE API without compiler errors - although rare, compile-time breakages are permitted with sufficient justification, since nanoapp developers can update their code at the time they migrate to the new API version.
This section provides an overview of the mechanisms used to ensure compatibility.
The CHRE API is a native C API that defines the interface between a nanoapp and any underlying CHRE implementation to provide cross-platform and cross-version compatibility. It is designed to be supportable even in very memory-constrained environments (total system memory in the hundreds of kilobytes range), and is thoroughly documented to clearly indicate the intended behavior.
The CHRE API follows semantic versioning principles to maintain binary compatibility. In short, this means that the minor version is incremented when new features and changes are introduced in a backwards compatible way, and the major version is only incremented on a compatibility-breaking change. One key design goal of the CHRE API is to avoid major version changes if at all possible, through use of compatibility-preserving code in the framework and Nanoapp Support Library (NSL).
Minor version updates to the CHRE API typically occur alongside each Android release, but the CHRE version and Android version are not intrinsically related. Nanoapps should be compiled against the latest version to be able to use any newly added features, though nanoapp binaries are compatible across minor version changes.
API design principles applied within CHRE to ensure compatibility include the following (not an exhaustive list). These are recommended to be followed for any vendor-specific API extensions as well.
This is where we want a nanoapp compiled against e.g. v1.2 to run on a CHRE v1.1 or older implementation. This is done through a combination of runtime feature discovery, and compatibility behaviors included in the Nanoapp Support Library (NSL).
Runtime feature discovery involves a nanoapp querying for the support of a feature (e.g. RTT support indicated in chreWifiGetCapabilities()
, or querying for a specific sensor in chreSensorFindDefault()
), which allows it determine whether the associated functionality is expected to work. The nanoapp may also query chreGetApiVersion()
to find out the version of the CHRE API supported by the platform it is running on. If a nanoapp has a hard requirement on some missing functionality, it may choose to return false from nanoappStart()
to abort initialization.
However, a CHRE implementation cannot anticipate all future API changes and automatically provide compatibility. So the NSL serves as a transparent shim which is compiled into the nanoapp binary to ensure this compatibility. For example, a nanoapp compiled against v1.2 must be able to reference and call chreConfigureHostSleepStateEvents()
when running on a CHRE v1.1 or earlier, although such a function call would have no effect in that case. Typical dynamic linking approaches would find an unsatisfied dependency and fail to load the nanoapp, even if it does not actually call the function, for example by wrapping it in a condition that first checks the CHRE version. In platform/shared/nanoapp/nanoapp_support_lib_dso.cc
, this is supported by intercepting CHRE API function calls and either calling through to the underlying platform if it’s supported, or replacing it with stub functionality.
Along similar lines, if new fields are added to the end of a structure without repurposing a reserved field in an update to the CHRE API, as was the case with bearing_accuracy
in chreGnssLocationEvent
, the nanoapp must be able to reference the new field without reading uninitialized memory. This is enabled by the NSL, which can intercept the event, and copy it into the new, larger structure, and set the new fields to their default values.
Since these NSL compatibility behaviors carry some amount of overhead (even if very slight), they can be disabled if it is known that a nanoapp will never run on an older CHRE version. This may be the case for a nanoapp developed for a specific device, for example. The NSL may also limit its compatibility range based on knowledge of the API version at which support for given hardware was introduced. For example, if a new hardware family first added support for the CHRE framework at API v1.1, then NSL support for v1.0 is unnecessary.
Outside of these cases, the NSL must provide backwards compatibility for at least 3 previous versions, and is strongly recommended to provide support for all available versions. This means that if the first API supported by a target device is v1.0, then a nanoapp compiled against API v1.4 must have NSL support for v1.1 through v1.4, and should ideally also support v1.0.
Conversely, this is where we want a nanoapp compiled against e.g. v1.1 to run against CHRE v1.2 or later implementations. The NSL cannot directly provide this kind of compatibility, so it must be ensured through a combination of careful CHRE API design, and compatibility behaviors in the CHRE framework.
Similar to how Android apps have a “target SDK” attribute, nanoapps have a “target API version” which indicates the version of the CHRE API they were compiled against. The framework can inspect this value and provide compatibility behavior as needed. For example, chreGetSensorInfo()
populates memory provided by the nanoapp with information about a given sensor. In CHRE API v1.1, this structure was extended with a new field, minInterval
. Therefore, the framework must check if the nanoapp’s target API is v1.1 or later before writing this field.
To avoid carrying forward compatibility code indefinitely, it is permitted for a CHRE implementation to reject compatibility with nanoapps compiled against an API minor version that is 2 or more generations older. For example, a CHRE v1.4 implementation may reject attempts to load a nanoapp compiled against CHRE API v1.2, but it must ensure compatibility with v1.3. However, providing the full range of compatibility generally does not require significant effort on behalf of the CHRE implementation, so this is recommended for maximum flexibility.
CHRE does not define a standard Application Binary Interface (ABI) - this is left as a platform responsibility in order to provide maximum flexibility. However, CHRE implementations must ensure that binary compatibility is maintained with nanoapps, by choosing a design that provides this property. For example, if a syscall-like approach is used (with the help of the NSL) to call from position-independent nanoapp code into fixed-position CHRE API functions (e.g. in a statically linked monolithic firmware image), syscall IDs and their calling conventions must remain stable. It is not acceptable to require all nanoapps to be recompiled to be able to work with an updated CHRE implementation.
Since the PAL APIs are largely based on the CHRE APIs, they benefit from many of the compatibility efforts by default. Overall, binary compatibility in the CHRE PAL APIs are less involved than the CHRE APIs, because we expect CHRE and CHRE PAL implementations to be built into the vendor image together, and usually run at the same version except for limited periods during development. However, a PAL implementation can simultaneously support multiple PAL API versions from a single codebase by adapting its behavior based on the requestedApiVersion
parameter in the *GetApi method, e.g. chrePalWifiGetApi()
.
In general, nanoapp compilation may be broken in a minor update (given sufficient justification - this is not a light decision to make, considering the downstream impact to nanoapp developers), but deprecation of functionality at a binary level occurs over a minimum of 2 years (minor versions). The general process for deprecating a function in the CHRE API is as follows:
In a new minor version N
of the CHRE API, the function is marked with @deprecated
, with a description of the recommended alternative, and ideally the justification for the deprecation, so nanoapp developers know why it's important to update.
Depending on the severity of impact, the function may also be tagged with a compiler attribute to generate a warning (e.g. CHRE_DEPRECATED
) that may be ignored. Or, version N
or later, an attribute or other method may be used to break compilation of nanoapps using the deprecated function, forcing them to update. If not considered a high severity issue and compatibility is easy to maintain, it is recommended to break compilation only in version N+2
or later.
Binary compatibility at this stage must be maintained. For example the NSL should map the new functionality to the deprecated function when running on CHRE N-1
or older, or a suitable alternative must be devised. Likewise, CHRE must continue to provide the deprecated function to support nanoapps built against N-1
.
Impacts to binary compatibility on the CHRE side may occur 2 versions after the function is made compilation-breaking for nanoapps, since forward compatibility is guaranteed for 2 minor versions. If done, the nanoapp must be rejected at load time.
Impacts to binary compatibility on the nanoapp side may occur 4 versions after the function is marked deprecated (at N+4
), since backward compatibility is guaranteed for 4 minor versions. If done, the NSL must cause nanoappStart()
to return false on version N
or older.
For example, if a function is marked deprecated in N
, and becomes a compilation-breaking error in N+2
, then a CHRE implementation at N+4
may remove the deprecated functionality only if it rejects a nanoapp built against N+1
or older at load time. Likewise, the NSL can remove compatibility code for the deprecated function at N+4
. CHRE and NSL implementations must not break compatibility in a fragmented, unpredictable, or hidden way, for example by replacing the deprecated function with a stub that does nothing. If it is possible for CHRE and/or the NSL to detect only nanoapps that use the deprecated functionality, then it is permissible to block loading of only those nanoapps, but otherwise this must be a blanket ban of all nanoapps compiled against the old API version.