The CHRE reference implementation (hereafter referred to just as “CHRE” or “the CHRE framework”) is developed primarily in C++17 using a modular object-oriented approach that separates common code from platform-specific code. CHRE is an event-based system, so CHRE is built around an event loop which executes nanoapp code as well as CHRE system callbacks. Per the CHRE API, nanoapps can’t execute in more than one thread at a time, so CHRE is structured around a single thread that executes the event loop, although there may be other threads in the system that support CHRE. The EventLoopManager is a Singleton object which owns the main state of the CHRE framework, including EventLoop and *Manager classes for the various subsystems supported by CHRE.
To get a better understanding of code structure and how it weaves between common and platform-specific components, it is helpful to trace the flow through a few example scenarios. Note that this is not meant to be an exhaustive list of everything that happens in each case (for that, refer to the code itself), but rather an overview of key points to serve as an introduction.
There are multiple ways by which a nanoapp can be loaded (see the relevant section below for details), but this example traces the flow for dynamically loading a nanoapp that has been passed in via the Context Hub HAL's
The nanoapp binary reaches the HAL implementation, and it is loaded into the processor where CHRE is running using a platform-specific method. While the path this takes can vary, one common approach is to transmit the binary into CHRE via the platform-specific HostLink implementation, then verify its digital signature, and parse the binary file format (e.g. ELF) to load and link the code.
Once the nanoapp code is loaded, the platform code calls
EventLoopManager::deferCallback() to switch context to the main CHRE thread (if needed), so it can complete loading and starting the nanoapp.
deferCallback() effectively posts an event to the main event loop which does not get delivered to any nanoapps. Instead, the purpose is to invoke the supplied callback from the CHRE thread once the event is popped off the queue.
The (platform-specific) callback finalizes the newly constructed
Nanoapp object as needed, and passes it to
EventLoop::startNanoapp() - this marks a transition from platform-specific to common code.
EventLoop takes ownership of the
Nanoapp object (which is a composite of common and platform-specific data and functions, as described in the Platform Abstractions section), includes it in the collection of loaded nanoapps to execute in the main event loop, updates
mCurrentNanoapp to reference the nanoapp it's about to execute, and calls into
Since the mechanism of supporting dynamic linkage and position independent code can vary by platform, transferring control from the framework to a nanoapp is considered part of the platform layer. So
PlatformNanoapp::start() performs any necessary tasks for this, and calls into the
nanoappStart() function defined in the nanoapp binary.
Let‘s assume the nanoapp we’ve loaded in the previous section calls the
chreSensorConfigure() CHRE API function within
The nanoapp invokes
chreSensorConfigure() with parameters to enable the accelerometer.
The Nanoapp Support Library (NSL) and/or the platform‘s dynamic linking module are responsible for handling the transition of control from the nanoapp binary to the CHRE framework. This can vary by platform, but we’ll assume that control arrives in the
chreSensorConfigure() implementation in
EventLoopManager::validateChreApiCall() is invoked to confirm that this function is being called from the context of a nanoapp being executed within the event loop (since associating the API call with a specific nanoapp is a requirement of this API and many others, and the majority of the CHRE framework code is only safe to execute from within the main CHRE thread), and fetch a pointer to the current
Nanoapp (i.e. it retrieves
mCurrentNanoapp set previosly by
EventLoopManager::getSensorRequestManager()) is called to process the nanoapp’s request - we transition to common code here.
The request is validated and combined with other nanoapp requests for the same sensor to determine the effective sensor configuration that should be requested from the platform, and the nanoapp is registered to receive broadcast accelerometer sensor events.
SensorRequestManager calls into
PlatformSensorManager::configureSensor(), which performs the necessary operations to actually configure the accelerometer to collect data.
Assuming success, the return value propagates back up to the nanoapp, and it continues executing.
Following the example from above, let's follow the case where an accelerometer sample has been generated and is delivered to the nanoapp for processing.
Starting in platform-specific code, likely in a different thread, the accelerometer sample is received from the underlying sensor framework - this typically happens in a different thread than the main CHRE thread, and within the fully platform-specific
As needed, memory is allocated to store the sample while it is being processed, and the data is converted into the CHRE format:
SensorRequestManager::handleSensorDataEvent() is invoked (common code) to distribute the data to nanoapps.
SensorRequestManager calls into
EventLoop to post an event containing the sensor data to all nanoapps registered for the broadcast event type associated with accelerometer data, and sets
sensorDataEventFree() as the callback invoked after the system is done processing the event.
EventLoop adds this event to its event queue and signals the CHRE thread.
Now, within the context of the CHRE thread, once the event loop pops this event off of its queue in
nanoappHandleEvent() function is invoked (via
PlatformNanoapp, as with
nanoappStart) for each nanoapp that should receive the event.
Once the event has been processed by each nanoapp, the free callback (
sensorDataEventFree()), is called to release any memory or do other necessary cleanup actions now that the event is complete.
CHRE follows the ‘compile time polymorphism’ paradigm, to allow for the benefits of
virtual functions, while minimizing code size impact on systems with tight memory constraints.
Each framework module as described in the previous section is represented by a C++ class in
core/, which serves as the top-level reference to the module and defines and implements the common functionality. This common object is then composed with platform-specific functionality at compile-time. Using the
SensorRequestManager class as an example, its role is to manage common functionality, such as multiplexing sensor requests from all clients into a single request made to the platform through the
PlatformSensorManager class, which in turn is responsible for forwarding that request to the underlying sensor system.
SensorRequestManager is fully common code,
PlatformSensorManager is defined in a common header file (under
platform/include/chre/platform), but implemented in a platform-specific source file. In other words, it defines the interface between common code and platform-specific code.
PlatformSensorManager inherits from
PlatformSensorManagerBase, which is defined in a platform-specific header file, which allows for extending
PlatformSensorManager with platform-specific functions and data. This pattern applies for all
Platform<Module> classes, which must be implemented for all platforms that support the given module.
Selection of which
PlatformSensorManagerBase implementation is instantiated is controlled by the build system, by setting the appropriate include path and source files. This includes the path used to resolve include directives appearing in common code but referencing platform-specific headers, like
To ensure compatibility across all platforms, common code is restricted in how it interacts with platform-specific code - it must always go through a common interface with platform-specific implementation, as described above. However, platform-specific code is less restricted, and can refer to common code, as well as other platform code directly.
This project follows the Google-wide style guide for C++ code, with the exception of Android naming conventions for methods and variables. This means 2 space indents, camelCase method names, an mPrefix on class members and so on. Style rules that are not specified in the Android style guide are inherited from Google. Additionally, this project uses clang-format for automatic code formatting.
This project uses C++17, but with two main caveats:
General considerations for using C++ in an embedded environment apply. This means avoiding language features that can impose runtime overhead, due to the relative scarcity of memory and CPU resources, and power considerations. Examples include RTTI, exceptions, overuse of dynamic memory allocation, etc. Refer to existing literature on this topic including this Technical Report on C++ Performance and so on.
Full support of the C++ standard library is generally not expected to be extensive or widespread in the embedded environments where this code will run. This means things like and should not be used, in favor of simple platform abstractions that can be implemented directly with less effort (potentially using those libraries if they are known to be available).