blob: bb64ef41b8f737cd11c9c6f4d34848ef05ec04a0 [file] [log] [blame] [view]
# CHIP Device Layer Design Notes
This document contains overviews, notes and other informative material related
to the internal design of the CHIP Device Layer (`src/platform`). It is intended
as a place to host documentation on topics that are of value to implementers,
but for reasons of size or scope don't naturally fit within comments in the
code.
This is intended to be a living document, with an informal structure, that
evolves along with the code. Developers are encouraged to add things that they
think would be useful to their fellow engineers.
This document contains the following sections:
- [Device Layer Adaptation Patterns](#Device-Layer-Adaptation-Patterns)
---
### Device Layer Adaptation Patterns
The Device Layer uses various design patterns to make it easier to adapt the
code to different platforms and operating contexts.
The CHIP Device Layer is intended to work across a variety of platforms and
operating contexts. These environments may differ by system type, OS, network
stack and/or threading model. One of the goals of the Device Layer is to make it
easy to adapt the CHIP application stack to new environments. This is especially
desirable in cases where the new platform is substantially similar to an
existing adaptation.
As part of its design, the CHIP Device Layer enables a pattern of code reuse
that strives to reduce the need for preprocessor conditionals (e.g. #ifdef).
While not eliminating #ifdefs entirely, the design allows major variances in
behavior to be expressed as distinct code bases (typically separate C++ classes)
which are then brought together via composition to enable a particular
adaptation.
To promote application portability, the CHIP Device Layer employs a pattern of
static polymorphism to insulate its application-visible API from the underlying
platform-specific implementation. A similar interface pattern is used within the
Device Layer itself to provide compartmentalization between components.
As much as possible, the above goals are achieved via the use of zero-cost
abstraction patterns (zero-cost in terms of code size and execution overhead).
Effort has been made to make the patterns easy to work with, without a lot of
conceptual burden or fiddly syntax.
This following sections describe some of the patterns used to achieve these
goals.
1. [Interface and Implementation Classes](#Interface-and-Implementation-Classes)<br>
2. [Method Forwarding](#Method-Forwarding)<br>
3. [Target Platform Selection](#Target-Platform-Selection)<br>
4. [Generic Implementation Classes](#Generic-Implementation-Classes)<br>
5. [Overriding Generic Behaviors](#Overriding-Generic-Behaviors)<br>
6. [Multiple Inheritance and Subclassing of Generic Implementations](#Multiple-Inheritance-and-Subclassing-of-Generic-Implementations)<br>
7. [Static Virtualization of Generic Implementation Behavior](#Static-Virtualization-of-Generic-Implementation-Behavior)<br>
8. [.cpp Files and Explicit Template Instantiation](#-ipp-files-and-explicit-template-instantiation)<br>
---
### Interface and Implementation Classes
The CHIP Device Layer uses a dual-class pattern to separate the abstract
features of a component object (generally its externally visible methods) from
the concrete implementation of those features on a particular platform.
Following this pattern, each major component in the Device Layer is embodied in
(at least) two C++ classes: an abstract interface class and an implementation
class.
The outwardly visible _**abstract interface class**_ defines a set of common
methods (and potentially other members) that are universally available to the
component user, but independent of the underlying implementation. Interface
classes contains no functionality themselves, but instead forward all method
calls to an associated implementation class using a zero-cost abstraction
technique. Interface classes serve to formalize the functional interface of a
component, as well as provide a place on which to host implementation-neutral
API documentation.
An _**implementation class**_ provides a concrete, platform-specific
implementation of the logical functionality exposed by an interface class. This
functionality may be provided directly by the class itself (i.e. within its
methods), or via delegation to one or more helper classes.
Pairs of abstract interface classes and implementation classes exists for each
major application-visible component of the Device Layer. Additionally, similar
pairs of classes are defined within the Device Layer to aid in isolation between
components.
Abstract interface classes are named after the functionality they provide–e.g.
ConfigurationManager, ConnectivityManager, etc. Implementation classes take the
name of their interface class with the suffix `Impl` attached. In all cases,
implementation class are required to inherit publicly from their interface
classes.
```cpp
class ConfigurationManagerImpl;
/** Interface class for ConfigurationManager component
*/
class ConfigurationManager
{
using ImplClass = ConfigurationManagerImpl;
public:
CHIP_ERROR GetDeviceId(uint64_t & deviceId);
static CHIP_ERROR Init();
...
};
/** Concrete implementation of ConfigurationManager component for a specific platform
*/
class ConfigurationManagerImpl final
: public ConfigurationManager
{
...
};
```
### Method Forwarding
Interface classes forward method calls to their implementation classes by means
of short inline functions called **_forwarding methods_**. These methods forward
calls from the application by downcasting the object’s `this` pointer and
invoking similarly named methods on the implementation class. This pattern is
similar to the C++
[Curiously Recurring Template Pattern](https://en.wikipedia.org/wiki/Curiously_recurring_template_pattern)
with the exception that the relationship between the base class and the subclass
is fixed, rather than being expressed as a template parameter. A type alias
named `ImplClass` is used within the interface to make the forwarding method
definitions more concise.
```cpp
inline CHIP_ERROR ConfigurationManager::GetDeviceId(uint64_t & deviceId)
{
/* forward method call... */
return static_cast<ImplClass*>(this)->_GetDeviceId(deviceId);
}
```
One convenient feature of this pattern is it allows for the forwarding of static
methods as well as instance methods. E.g.:
```cpp
inline CHIP_ERROR ConfigurationManager::Init()
{
return ImplClass::_Init();
}
```
The methods on the implementation class that are the target of forwarding
methods are known as **_implementation methods_**. For every forwarding method
there must be a corresponding implementation method.
A leading underscore (\_) is used to distinguish implementation methods from
their forwarding methods. This arrangement helps to emphasize the distinction
between the two, and ensures the generation of a compilation error should an
implementer neglect to provide an implementation method.
Implementation methods are not meant to be called directly. To block this type
of use, implementation classes declare their implementation methods as private,
and then use a friend declaration to give the interface class the (sole) right
to invoke these methods as part of forwarding.
```cpp
class ConfigurationManagerImpl;
/** Interface class for ConfigurationManager component
*/
class ConfigurationManager
{
using ImplClass = ConfigurationManagerImpl;
public:
CHIP_ERROR GetDeviceId(uint64_t & deviceId);
static CHIP_ERROR Init();
...
};
/** Concrete implementation of ConfigurationManager component for specific platform
*/
class ConfigurationManagerImpl final : public ConfigurationManager
{
/* Let the forwarding methods on ConfigurationManager call implementation
methods on this class. */
friend ConfigurationManager;
private:
CHIP_ERROR _GetDeviceId(uint64_t & deviceId);
static CHIP_ERROR _Init();
...
};
inline CHIP_ERROR ConfigurationManager::GetDeviceId(uint64_t & deviceId)
{
/* Forward calls to corresponding implementation method... */
return static_cast<ImplClass*>(this)->_GetDeviceId(deviceId);
}
inline CHIP_ERROR ConfigurationManager::Init()
{
/* Forward calls to corresponding static implementation method... */
return ImplClass::_Init();
}
```
### Target Platform Selection
An implementation class provides a concrete implementation of a Device Layer
component for use on a particular platform. Multiple implementation classes may
exist within the Device Layer source tree for the same component. Each of these
classes has the same name, but their code is unique to the associated platform.
The choice of which implementation class is included at compile time is done by
means of a computed #include directive with the following form:
```cpp
/* contents of ConfigurationManager.h */
...
#define CONFIGURATIONMANAGERIMPL_HEADER \
<platform/CHIP_DEVICE_LAYER_TARGET/ConfigurationManagerImpl.h>
#include CONFIGURATIONMANAGERIMPL_HEADER
...
```
This directive appears within the header file that defines the component’s
interface class. The C++ pre-processor automatically expands the #include line
to select the appropriate implementation header based on the chosen platform. In
this way, source files which include the component’s interface header file
naturally get the correct implementation header as well.
Implementation header files for each supported platform are arranged within
subdirectories that are named after their target platform (e.g. `ESP32`). All
such files have the same file name (e.g. `ConfigurationManagerImpl.h`), and each
file contains a definition for like-named class (`ConfigurationManagerImpl`).
Source files specific to a platform are placed in subdirectories immediately
beneath the root Device Layer source directory (e.g.
`src/adaptations/device-layer/ESP32`). Like platform-specific header
directories, these are subdirectories are named after the target platform.
The choice of target platform for the Device Layer is specified at project
configuration time, using the configure script option
`--device-layer=<target-platform>`. Passing the --device-layer option results in
the definition of a pair of pre-processor symbols where the name of the target
platform is incorporated into the definitions. E.g.:
```cpp
#define CHIP_DEVICE_LAYER_TARGET ESP32
#define CHIP_DEVICE_LAYER_TARGET_ESP32 1
```
The --device-layer configuration option also selects the appropriate set of
platform-specific source files to be included in the generated library file.
This is accomplished via logic in the Device Layer’s Makefile.am.
### Generic Implementation Classes
Often it is possible to share implementation code across a range of platforms.
In some cases the code in question is substantially the same for all targets,
with only minor customizations required in each case. In other cases, the
commonality of the implementation extends across a subset of platforms that
share a particular architectural feature, such as a common OS (Linux, FreeRTOS)
or network stack (sockets, LwIP).
To accommodate this, the CHIP Device Layer encourages a pattern of factoring
common functionality into **_generic implementation base classes_**. These base
classes are then used to compose (via inheritance) the concrete implementation
classes that underlie a component.
Generic implementation base classes are realized as C++ class templates which
follow the C++
[Curiously Recurring Template Pattern](https://en.wikipedia.org/wiki/Curiously_recurring_template_pattern).
Implementation classes wishing to incorporate common behavior inherit from an
instantiation of the template, passing the implementation class itself as the
template’s parameter.
```cpp
/** Generic base class for use in implementing ConfigurationManager components
*/
template<class ImplClass>
class GenericConfigurationManagerImpl
{
...
};
/** Concrete implementation of ConfigurationManager component for specific platform
*/
class ConfigurationManagerImpl final
: public ConfigurationManager,
public GenericConfigurationManagerImpl<ConfigurationManagerImpl> /* <-- Implementation provided by
generic base class. */
{
...
};
```
In many cases, the generic implementation base class itself will directly
provide some or all of the implementation methods needed to satisfy the
component’s interface. The rules of C++ method resolution are such that calls to
a forwarding method on the interface class are mapped directly to the base class
method. In this situation, the derived implementation class needn't declare a
version of the target method at all, and method calls are forwarded statically,
at compile time, without overhead.
```cpp
/** Interface class for ConfigurationManager component
*/
class ConfigurationManager
{
using ImplClass = ConfigurationManagerImpl;
public:
CHIP_ERROR GetDeviceId(uint64_t & deviceId);
static CHIP_ERROR Init();
...
};
/** Generic base class for use in implementing ConfigurationManager components
*/
template<class ImplClass>
class GenericConfigurationManagerImpl
{
protected:
CHIP_ERROR _GetDeviceId(uint64_t & deviceId); /* <-- Invoked when GetDeviceId() called. */
...
};
/** Concrete implementation of ConfigurationManager component for specific platform
*/
class ConfigurationManagerImpl final
: public ConfigurationManager,
public GenericConfigurationManagerImpl<ConfigurationManagerImpl>
{
...
};
```
### Overriding Generic Behaviors
Where desirable, concrete implementation classes are free to override the
implementation methods provided by a generic base class. This is accomplished by
defining a platform-specific version of the method on the implementation class.
The rules of C++ result in the method on the implementation class being called
in preference to the generic method.
The new method may entirely replace the behavior of the generic method, or it
may augment its behavior by calling the generic method in the midst of its own
implementation.
```cpp
CHIP_ERROR ConfigurationManagerImpl::_GetDeviceId(uint64_t & deviceId)
{
using GenericImpl = GenericConfigurationManagerImpl<ConfigurationManagerImpl>;
/* Call the generic implementation to get the device id. */
uint64_t deviceId = GenericImpl::_GetDeviceId(deviceId);
/* Special case the situation where the device id is not known. */
if (deviceId == kNodeIdNotSpecified) {
deviceId = PLATFORM_DEFAULT_DEVICE_ID;
}
return deviceId;
}
```
### Multiple Inheritance and Subclassing of Generic Implementations
Concrete implementation classes are free to inherit from multiple generic base
classes. This pattern is particularly useful when the overall functionality of a
component can be naturally split into independent slices (e.g. methods that
support WiFi and methods that support Thread). Each such slice can then be
implemented by a distinct base class which ends up being composed together with
other base classes in the final implementation.
```cpp
/** Concrete implementation of ConfigurationManager component for specific platform
*/
class ConfigurationManagerImpl final
: public ConfigurationManager,
public GenericWiFiConfigurationManagerImpl<ConfigurationManagerImpl>, /* <-- WiFi features */
public GenericThreadConfigurationManagerImpl<ConfigurationManagerImpl> /* <-- Thread features */
{
...
};
```
A generic implementation base class may also inherit from other generic base
classes. This is useful for “specializing” a generic implementation for a
certain sub-range of use cases (e.g. for a particular OS type).
```cpp
/** Generic base class for use in implementing PlatformManager features
* on all platforms.
*/
template<class ImplClass>
class GenericPlatformManagerImpl
{
...
};
/** Generic base class for use in implementing PlatformManager features
* on FreeRTOS platforms.
*/
template<class ImplClass>
class GenericPlatformManagerImpl_FreeRTOS
: public GenericPlatformManagerImpl<ImplClass>
{
...
};
```
### Static Virtualization of Generic Implementation Behavior
When creating generic implementation base classes, developers are encouraged to
use a pattern of static virtualization to delegate operations to the concrete
implementation class in cases where the operation may or must be implemented in
a platform-specific way.
For example, consider a generic implementation of the ConfigurationManager
component where value accessor methods such as `GetDeviceId()` operate by
retrieving values from an underlying key-value store. The particulars of how the
key-value store is implemented will likely vary by platform. To allow for this,
the generic implementation class is structured to delegate the operation of
retrieving the value for a key to a method on the concrete implementation class.
Following the Curiously Recurring Template Pattern, delegation is accomplished
by casting the `this` pointer to the implementation class and calling a method
with the appropriate signature. An inline helper function named `Impl()` helps
to make the code concise.
```cpp
template<class ImplClass>
class GenericConfigurationManagerImpl
{
protected:
CHIP_ERROR _GetDeviceId(uint64_t & deviceId);
...
private:
ImplClass * Impl() { return static_cast<ImplClass*>(this); }
};
class ConfigurationManagerImpl final
: public ConfigurationManager,
public GenericConfigurationManagerImpl<ConfigurationManagerImpl>
{
friend GenericConfigurationManagerImpl<ConfigurationManagerImpl>;
private:
CHIP_ERROR ReadConfigValue(const char * key, uint64_t & value);
};
template<class ImplClass>
CHIP_ERROR GenericConfigurationManagerImpl<ImplClass>::_GetDeviceId(uint64_t & deviceId)
{
/* delegate to the implementation class to read the 'device-id' config value */
return Impl()->ReadConfigValue(“device-id”, deviceId);
}
CHIP_ERROR ConfigurationManagerImpl::ReadConfigValue(const char * key, uint64_t & value)
{
/* read value from platform-specific key-value store */
...
}
```
In the above example, the delegated method is conceptually ‘pure virtual’ in
that the concrete implementation class must supply a version of the method,
otherwise compilation will fail. In other situations, a similar pattern can be
used to allow an implementation to override a default behavior supplied by the
base class on an as-needed basis.
Again, delegation happens by casting the `this` pointer and calling an
appropriate method. In this case, however, the generic base class provides a
default implementation of the target method which will be used unless the
subclass overrides it.
```cpp
template<class ImplClass>
class GenericPlatformManagerImpl
{
protected:
void _DispatchEvent(const CHIPDeviceEvent * event);
void DispatchEventToApplication(const CHIPDeviceEvent * event);
...
private:
ImplClass * Impl() { return static_cast<ImplClass*>(this); }
};
template<class ImplClass>
void GenericPlatformManagerImpl<ImplClass>::_DispatchEvent(const CHIPDeviceEvent * event)
{
...
/* Delegate work to method that can be overridden by implementation class */
Impl()->DispatchEventToApplication(event);
...
}
template<class ImplClass>
void GenericPlatformManagerImpl<ImplClass>::DispatchEventToApplication(const CHIPDeviceEvent * event)
{
/* provide default implementation of DispatchEventToApplication() */
...
}
```
### .cpp Files and Explicit Template Instantiation
The rules for C++ templates require that the compiler 'see' the full definition
of a class template at the moment of its instantiation. (Instantiation in this
context means the moment at which the compiler is forced to generate an actual
class from the recipe provided by the template). Typically this requires placing
the entire definition of the class template, including all its methods, into a
header file, which must then be included before the moment of instantiation.
To provide a separation between the definition of a class template and the
definitions of its members, the CHIP Device Layer places all non-inlined
template member definitions into a separate file. This file has the same base
name as the template header file, but with a `.cpp` suffix. This pattern reduces
clutter in the header file and makes it possible to include the non-inlined
member definitions only when they are needed (more on this below).
```cpp
/* contents of GenericConfigurationManagerImpl.h */
template<class ImplClass>
class GenericConfigurationManagerImpl
{
protected:
CHIP_ERROR _GetDeviceId(uint64_t & deviceId);
...
};
```
```cpp
/* contents of GenericConfigurationManagerImpl.cpp */
template<class ImplClass>
CHIP_ERROR GenericConfigurationManagerImpl<ImplClass>::_GetDeviceId(uint64_t & deviceId)
{
...
}
```
It is often the case that the C++ compiler is forced to instantiate a class
template multiple times, once for each .cpp file it compiles. This can add
significant overhead to the compilation process. To avoid this, the Device Layer
uses the C++11 technique of
[explicit template instantiation](https://en.cppreference.com/w/cpp/language/class_template#Explicit_instantiation)
to direct the compiler to instantiate the template only once. This is
accomplished in two steps: first, all header files that makes use of a class
template includes an `extern template class` declaration before the point at
which the template class is used. This tells the compiler to _not_ instantiate
the template in that context.
```cpp
/* contents of ConfigurationManagerImpl.h */
#include <CHIP/DeviceLayer/internal/GenericConfigurationManagerImpl.h>
// Instruct the compiler to instantiate the GenericConfigurationManagerImpl<ConfigurationManagerImpl>
// class only when explicitly asked to do so.
extern template class GenericConfigurationManagerImpl<ConfigurationManagerImpl>;
...
```
Then, within a corresponding .cpp file, the template's .cpp file is included and
a `template class` definition is used to force an explicit instantiation of the
template.
```cpp
/* contents of ConfigurationManagerImpl.cpp */
#include <CHIP/DeviceLayer/internal/GenericConfigurationManagerImpl.cpp>
// Fully instantiate the GenericConfigurationManagerImpl<ConfigurationManagerImpl> class.
template class GenericConfigurationManagerImpl<ConfigurationManagerImpl>;
...
```
The result is that the template's non-inlined members are only parsed and
instantiated once, during compilation of the referencing .cpp file, avoiding
redundant processing in other contexts.