Nanoapp Developer Guide

Since CHRE is an open platform, anyone can write nanoapps. However, deploying to a device requires cooperation of the device manufacturer, because CHRE is a security-sensitive trusted environment, similar to other low-level firmware, and it typically has tight resource constraints. This section assumes you have a basic understanding of what a nanoapp is (if not, see the Nanoapp Overview section), and provides some simple instructions to help you get started with developing your own nanoapp.

Getting Started

When starting a new nanoapp, it’s helpful to start with the skeleton of an existing nanoapp. The simplest example can be found at apps/hello_world. Start by copying this folder to the location where you will develop the nanoapp - it can be outside of the system/chre project, for example in a vendor-specific Git repository in the vendor/ folder of the Android tree.

If you don’t plan to use this nanoapp as a static nanoapp (see the Nanoapp Overview for details), remove the hello_world.mk file and delete the code blocks wrapped in #ifdef CHRE_NANOAPP_INTERNAL. Rename the remaining files to match your nanoapp.

Picking a Nanoapp ID

Nanoapps are uniquely identified by a 64-bit number. The most significant 5 bytes of this number are the vendor identifier, and the remaining bytes identify the nanoapp within the vendor’s namespace. The vendor identifier is usually devised from an ASCII representation of the vendor’s name, for example Google uses 0x476F6F676C (“Googl”). The remaining portion of the ID is typically just an incrementing value for each nanoapp.

Refer to system/chre/chre_api/include/chre_api/chre/common.h and util/include/chre/util/nanoapp/app_id.h for some examples and utilities.

Be sure to pick a unique nanoapp ID when creating a new nanoapp.

Picking a Language

CHRE guarantees support for nanoapps written in C11 or C++17, though not all standard library functions are supported (see below for details). For a device-specific nanoapp, additional programming languages/versions may be supported, but this can impact portability.

Building the Nanoapp Binary

While it’s possible to build a nanoapp with a different build system, just as it is for the CHRE framework, it’s recommended to use the common build system included in this project, as it makes it easy to support a variety of target platforms. The rest of this section assumes you are using the CHRE build system to create a non-static nanoapp.

Update the Makefile in your nanoapp’s directory to:

  • Define nanoapp metadata, including:
    • NANOAPP_NAME: sets the output filename of the binary
    • NANOAPP_ID: 64-bit identifier, in hexadecimal format
    • NANOAPP_VERSION: 32-bit version, in hexadecimal format (see versioning section below)
    • NANOAPP_NAME_STRING, NANOAPP_VENDOR_STRING: human-readable strings for the name of the nanoapp and vendor, respectively
    • NANOAPP_IS_SYSTEM_NANOAPP: 0 or 1 (see Nanoapp Overview)
  • Populate COMMON_SRCS with the C or C++ source files to compile
  • Populate COMMON_CFLAGS with compiler flags, like additional include paths
  • Include any additional .mk files for vendor extensions, etc. before app.mk

Refer to build/nanoapp/app.mk for full details.

The nanoapp can then be built using a command like OPT_LEVEL=s make <build_target> -j`nproc` (see the CHRE Framework Build System section for details on build targets), which will produce build artifacts at out/<build_target>/<nanoapp_name>.*.

Loading onto a Device

Exact steps to load a nanoapp binary can vary by device, but for developing a preloaded nanoapp, this typically involves the following steps:

  • Perform any needed post-processing of the nanoapp binary to permit it to be loaded (such as signing with a development or production key)
  • Write the binary to the device’s storage (for example, using adb push)
  • Update preloaded_nanoapps.json or other configuration as needed, so that CHRE knows to load the new nanoapp
  • Restart CHRE to reload all nanoapps, including the new one

Nanoapp Versioning

While not strictly enforced, nanoapps are recommended to follow the convention of the CHRE framework and use Semantic Versioning. In the case of a nanoapp, the key versioned “API” is considered the interface between the client and nanoapp. Nanoapp versions are represented as a 32-bit integer, where the most significant byte represents the major version, followed by one byte for the minor version, and two bytes for the patch version.

Using the CHRE API

The CHRE API is the key interface between each nanoapp and the underlying system. Refer to the extensive API documentation in the header files at chre_api/include, as well as usage of the APIs by sample nanoapps. The CHRE API is normally included via #include "chre_api/chre.h".

Utility Libraries

Some source and header files under util are specifically designed to aid in nanoapp development, and others were initially created for use in the framework but can be leveraged by nanoapps as well. In general, any source and header file there that does not include a header from chre/platform (part of the internal CHRE framework implementation details) may be used by a nanoapp, and files within a subdirectory called nanoapp are specifically targeted for use by nanoapps.

This includes util/include/chre/util/nanoapp/log.h (meant to be included via #include “chre/util/nanoapp/log.h”), which provides macros like LOGD which can be conditionally compiled, include a configurable prefix to help identify the sender, and suppress double promotion warnings.

The utilities library also includes a number of container classes, which are meant to mimic the C++ standard library, but with a lightweight, CHRE-compatible implementation. This includes:

  • chre::DynamicVector: an std::vector analogue
  • chre::FixedSizeVector: accessed like std::vector, but only uses statically allocated memory
  • chre::ArrayQueue: can be used as a circular buffer
  • chre::UniquePtr: an std::unique_ptr analogue
  • chre::Optional: an analogue to std::optional from C++17
  • chre::Singleton: a container for a statically allocated object with explicit initialization and deinitialization (e.g. enables construction of a global object to be deferred until nanoappStart())

Interacting with the Host

Nanoapps can interact with one or more clients on the host (applications processor) through a flexible binary message-passing interface. For simple interactions in cases where the lowest memory footprint is desired, using only the built-in message type field with no additional payload, or passing hand-rolled packed C-style structures (e.g. using Java’s ByteBuffer on the client side) can work, though this approach can be error-prone. Using a well-defined serialization format, such as Protocol Buffers (see the Using NanoPB section below) or FlatBuffers, is usually a better choice.

There are a few common tips to keep in mind when interacting with the host:

  1. Nanoapp binaries are usually updated independently from client code - watch out for compatibility issues arising from changes to the messaging protocol, and use a serialization format like Protocol Buffers if possible.

  2. Nanoapp messages to the host always wake it up if it’s asleep. If this is not required, nanoapps are encouraged to batch their messages and opportunistically send when the host wakes up for another reason (see chreConfigureHostSleepStateEvents()).

  3. After calling chreSendMessageToHostEndpoint(), ownership of the memory associated with the message payload is assigned to the framework. Do not modify it until the free callback is invoked.

  4. Nanoapp messaging should be unicast, unless broadcast messaging is strictly necessary. Design the messaging protocol such that the client initiates communication, and save the host endpoint ID in the nanoapp to use when sending replies.

Interacting with Other Nanoapps

While most nanoapps are only concerned with providing functionality for a single client on the host, it is possible for a nanoapp to provide services to other nanoapps within CHRE. Similar to how nanoapps communicate with the host by passing messages, nanoapps can communicate with one another by passing events with arbitrary binary payload. Event IDs starting in the range CHRE_EVENT_FIRST_USER_VALUE are reserved for this purpose.

Typically a nanoapp creates a nanoapp client library which other nanoapps can include, which presents a simple, expressive API, and handles the implementation details of passing events to the target nanoapp, and interpreting incoming messages.

Refer to the functions defined in chre/event.h for more details.

Using TensorFlow Lite for Microcontrollers

Many nanoapps use machine learning techniques to accomplish their functionality. The CHRE build system has built-in support for integrating TensorFlow Lite for Microcontrollers (TFLM) into a nanoapp. Sync the TFLM sources, set TFLM_PATH, and define USE_TFLM=true in your Makefile - see apps/tflm_demo/README for details and an example nanoapp.

Using Nanopb

The CHRE build system has integrated support for using Nanopb to provide support for Protocol Buffers in a nanoapp. To integrate this into your nanoapp’s Makefile, first install and configure dependencies:

  • Sync the Nanopb source tree (e.g. from a release on GitHub), and define the NANOPB_PREFIX environment variable to its path
  • Download and install the protobuf compiler protoc and make it available in your $PATH, or set the PROTOC environment variable

Then in your nanoapp’s Makefile, populate NANOPB_SRCS with the desired .proto file(s). That’s it! Though some additional options/parameters are available - see build/nanopb.mk for details.

Nanoapp Development Best Practices

Even though CHRE aims to provide an environment for low-power and low-latency contextual signal processing, these two are often conflicting goals. In addition, CHRE is usually implemented in a resource-constrained environment with limited memory available.

As it requires collaboration from all nanoapps to optimize their resource usage for CHRE to live up to its promises, some best practices are provided here as guidance for nanoapp development.

Memory Efficiency

Avoid dynamic heap allocations where possible

As CHRE is designed in a resource-constrained environment, there is no guarantee runtime memory allocation will succeed. In addition, dynamic heap allocations make it difficult to estimate the memory usage in advance. Developers are therefore encouraged to use static allocations where possible.

Be careful of stack usage

Unlike Linux’s default stack of 8MB that scales dynamically, CHRE only has a fixed stack of limited size (8KB is typical). Ensure you keep any allocations to an absolute minimum and any large allocations should go out of scope prior to navigating deeper into a stack.

Prefer in-place algorithms

Prefer in-place algorithms over out-of-place ones where efficiency allows to minimize additional memory requirements.

Power Efficiency

Be opportunistic when possible

Examples include:

  • If the host is asleep and doesn’t need to act on a nanoapp message immediately, buffer until it wakes up for another reason.
  • Make a WiFi on-demand scan request only if the WiFi scan monitor doesn’t provide a scan result in time.

Batch data at the source where possible

By batching data at the source, it reduces the data delivery frequency and helps keep CHRE asleep and improve power efficiency. Clients should make data requests with the longest batch interval that still meets the latency requirement. Examples include:

  • Make a sensor data request with the longest latency possible.
  • Make an audio data request with the longest deliveryInterval possible.

Standard Library Usage

CHRE implementations are only required to support a subset of the standard C and C++ libraries, as well as language features requiring run-time support. This list is carefully considered to ensure memory usage and implementation complexity are minimized. Following these principles, some features are explicitly excluded due to their memory and/or extensive OS-level dependencies, and others because they are supplanted by more suitable CHRE-specific APIs. While not meant to be an exhaustive list and some platforms may differ, the following standard library features are not meant to be used by nanoapps:

  • C++ exceptions and run-time type information (RTTI)
  • Standard library multi-threading support, including C++ library headers <thread>, <mutex>, <atomic>, <future>, etc.
  • C and C++ Standard Input/Output libraries
  • C++ Standard Template Library (STL)
  • C++ Standard Regular Expressions library
  • Dynamic memory allocation (malloc, calloc, realloc, free), and libraries that inherently use dynamic allocation, such as std::unique_ptr
  • Localization and Unicode character support
  • Date and time libraries
  • Functions that modify normal program flow, including <setjmp.h>, <signal.h>, abort, std::terminate, etc.
  • Accessing the host environment, including system, getenv, etc.
  • POSIX or other libraries not included in the C11 or C++17 language standards

In many cases, equivalent functionality is available from CHRE API functions and/or utility libraries. For example, chreLog may be used for debug logging, where a more traditional program might use printf.

Debugging

Similar to the framework debugging methods, each has its nanoapp counterpart to support nanoapp debugging through the framework. Please see the Framework Debugging section for reference/context.

Logging

CHRE API chreLog() logs information into the system as part of the CHRE logs. Normally this appears in logcat, but some platforms may route it to a different logging system (a future version of the CHRE API is expected to make logcat logging mandatory).

Nanoapps are encouraged to #include "chre/util/nanoapp/log.h" and use the LOGx() macros defined therein, which requires these additional steps:

  • Define LOG_TAG to a short, human-readable identifier for your nanoapp, as this gets prepended to logs
  • Define NANOAPP_MINIMUM_LOG_LEVEL to a CHRE_LOG_LEVEL_\* value in your Makefile for compile time log level filtering - it’s recommended to use CHRE_LOG_LEVEL_DEBUG for development, and CHRE_LOG_LEVEL_INFO for release

See also the Framework Debugging section for more general guidance on logging in CHRE.

Debug Dump

When running on CHRE v1.4+, nanoapps can also append information to the CHRE framework debug dump. Nanoapps interested in using this capability should call chreConfigureDebugDumpEvent(true) in nanoappStart(), then when CHRE_EVENT_DEBUG_DUMP is received in nanoappHandleEvent(), use chreDebugDumpLog() to write human-readable output to the debug dump, which appears in bug reports under the Context Hub HAL debug section. In the reference CHRE framework implementation, nanoapp debug dumps have the nanoapp name and ID automatically prepended, for example:

Nanoapp debug dumps:

 DebugDumpWorld 0x0123456789000011:
  Debug event count: 2
  Total dwell time: 92 us

Refer to the associated CHRE API documentation and Framework Debugging section for more information.

CHRE_ASSERT

To help catch programming errors or other unexpected conditions, nanoapps can use the CHRE_ASSERT macro provided by #include "chre/util/nanoapp/assert.h". Keep in mind that if one nanoapp encounters an assertion failure, it most likely will cause a reset of the processor where CHRE is running, impacting other functionality (though this can vary by platform). Therefore, assertions are only recommended to be used during development. Define the CHRE_ASSERTIONS_ENABLED variable in your Makefile to false to disable assertions at compile time.