Skip to content

Component Design

David Antliff edited this page Nov 22, 2017 · 6 revisions

Introduction

All ESP32 components are currently written in C.

Components follow an explicit creation/initialisation pattern, with an explicit destructor.

Composition is a common pattern in most programming designs. Components are created by including other components, and these sub-components are often encapsulated. In fact, in many cases, the composition of sub-components is hidden from the user of the component and is not apparent via the API. The C programming language typically models this composition by the use of a structure. Sub-components are also structures, so the main component may contain these as inline structs or referenced by pointers, depending on the use case.

Consider a component that operates with a complex data structure - one that is composed of several other components. Some of these components are The "classical" way of doing this in C is to require the user of a component to construct the entire data structure. In all cases, this requires the user of the API to understand how to build the data structure correctly. Fields must all be set correctly, and there is no mechanism to automatically apply defaults.

It becomes a similarly manual process when the time comes to deallocate the component and its sub-components.

#include <stdlib.h>
#include <string.h>
#include <stdio.h>

typedef struct
{
    int x;
} A;

typedef struct
{
    A a0;
    A * a1;
} B;

int main(int argc, char ** argv)
{
    // static allocation of B
    A a1 = { 42 };
    B b0 = { { 17 }, &a1 };

    // dynamic allocation of B
    B * b1 = malloc(sizeof(*b1));
    memset(b1, 0, sizeof(*b1));
    b1->a0.x = 18;
    b1->a1 = malloc(sizeof(A));
    b1->a1->x = 42;

    // mixed allocation of B
    B b2 = { { 19 }, malloc(sizeof(A)) };
    memset(b2.a1, 0, sizeof(A));
    b2.a1->x = 42;

    printf("b0: a0.x %d, a1->x %d\n", b0.a0.x, b0.a1->x);
    printf("b1: a0.x %d, a1->x %d\n", b1->a0.x, b1->a1->x);
    printf("b1: a0.x %d, a1->x %d\n", b2.a0.x, b2.a1->x);


    // no deallocation of statically allocated b0

    // deallocation of dynamically allocated b1
    free(b1->a1);
    free(b1);

    // dellocation of mixed allocation of B
    free(b2.a1);
}

Naturally for deeper trees of components this becomes impractical and often helper functions need to be used to construct the data structure.

Consider the situation where a new sub-component is added to the main component. In some cases this will require all construction/destruction code to be modified to accommodate the new sub-component (unless a zero default is acceptable). And in fact the zero default is only applied if the user remembers to memset each component!

Consider also the situation where a structure is refactored or modified in a non-backwards compatible manner. In this case, all users will have their code break.

There is another way - to consider each component's structure as a "grey box". The user's client code can see the internals (to permit static allocation) but the policy is for access to only be made via API functions. Three steps are used to instantiate a component:

  1. Allocation
  2. Initialisation
  3. Configuration

Allocation

It may be useful for a user to instantiate a component statically, in order to better control memory usage, or ensure controlled scope of the component. Conversely, it may be useful for a component to be dynamically allocated, such as part of another dynamically allocated component (or an array) or for temporary use.

Therefore the requirements are for components to support:

  1. static allocation (allocation in global scope or on the current stack frame) or,
  2. dynamic allocation (on the heap) according to the user's needs.

To achieve 1, a component's structure definition is provided in the public header for that component. This allows for the the component to be directly instantiated on the current stack frame, or in global scope. Any inline sub-component structures will be automatically allocated (as per the rules of C). Pointers to sub-component structures are left uninitialised.

To achieve 2, a constructor function (e.g. foo_malloc()) is provided that will allocate the structure and set all memory to zero. A pointer to the allocated structure is returned. No initialisation is performed by this function.

Dependencies

A component's dependencies may be self-managed or externally managed (injected):

  1. Self-managed dependencies are allocated by the component itself, when it is allocated. To support pure static allocation, this implies that self-managed dependencies cannot be dynamically allocated. Therefore they must be inline structures, fully instantiated as a field in the main component structure.

  2. Externally managed dependencies are allocated by client code, and supplied to the component at instantiation time. This could be via an initialisation function, or by a configuration function.

There are at least two kinds of scopes on sub-components. Let's call them:

  1. Private: a component is composed of one or more private sub-components that are hidden from the main component's user. I.e. the main component's API does not directly reveal the existence of the sub-component, nor does it provide direct access. A private sub-component may be self-managed (allocated inline) or externally managed and injected.

  2. Public: a component is composed of one or more public sub-components that are made available to the main component's user. I.e. the main component's API may provide direct access to the sub-component, typically full retrieval. This type of dependency works with both self-managed sub-components and externally-managed sub-components.

Dependency Injection

The use of an initialisation function (or an alternate initialisation function) provides the opportunity for a user to inject a different public dependency. For example, a sub-component can be instantiated by the client code, initialised, configured, then passed to the initialisation (or configuration) of a parent component. Care must be taken when the parent component is freed - in most cases the injected subcomponent will be freed at the same time, unless the API requires a client-supplied dependency in which case the client must handle the deallocation of the sub-component, if necessary.

Initialisation

This is provided as a second step, in order to ensure consistency between static and dynamic allocated component structures. Regardless of how the component has been allocated, it should be initialised before further use.

The intialisation function sets all fields to suitable default values. These may be non-zero. A flag is set to record that the structure has been initialised. Any sub-component structures are also initialised in this step.

The initialisation function (ideally) should not have any user-selectable parameters. For example, it may be acceptable for the user to supply a non-changing value such as a bus address, however selection of, say, a mode should not be parameterised. This is to minimise the likelihood that the initialisation function prototype will change in the future.

Configuration

Configuration functions require the structure to be initialised first. These functions are provided, typically one for each "aspect" of configuration, in order to decouple configuration from initialisation. This is to ensure that new configuration functions can be added in future, and existing functions can be deprecated if necessary.

Backwards Compatibility

If structure fields are modified or removed, or new fields are added, the initialisation function can be modified to suit. No change to the API is required, and existing code will continue to compile. Note that if static allocation is chosen by the user, then a recompilation of client code will be required. Dynamic allocation does not require a recompilation of the client code provided the structure is treated as a black box (no direct access to fields).

The user of configuration functions enables future modification without breaking the existing API.

Deinitialisation

It may be useful to allow for deinitialisation (or reinitialisation) to return a component structure to a particular state, or release resources. Such functions can be provided as necessary.

Destruction

The use of an explicit destruction function simplifies the effort required to deallocate a dynamically allocated component's structure, and any sub-component structures that have been automatically allocated by the component's instantiation.

The _free() function takes a pointer to a pointer so that it can set the structure pointer to NULL following a successful deallocation.

Example - still a work in progress.