12 KiB
Design for Compatibility
[TOC]
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.
CHRE API
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 Compatibility Design Principles
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.
- Functionality must not be removed unless it was optional at the time of introduction, for example as indicated by a capabilities flag (an exception exists if it has no impact on the regular functionality of a nanoapp, for example a feature that only aids in debugging)
- Reserved fields must be set to 0 by the sender and ignored by the recipient
- Fields within a structure must not be reordered - new fields may only be introduced by reclaiming reserved fields (preferred), or adding to the end of a structure
- When reclaiming a reserved field, the default value of 0 must indicate a property that is guaranteed to hold for previous API versions, or “unknown”
- Arguments to a function must not be added or removed - introduce a new function instead
- The meaning of constants (e.g. event types) must never be changed, but may be deprecated and eventually replaced
Binary Backward Compatibility and the NSL
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.
Binary Forward Compatibility and Framework Requirements
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.
ABI Stability
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.
CHRE PALs
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()
.
Deprecation Strategy
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, versionN
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 versionN+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 againstN-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 causenanoappStart()
to return false on versionN
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.