blob: d6436523d657a6b35361158d2258b94118cb903c [file] [log] [blame]
[.text-center]
= Project Connected Home over IP Software
:listing-caption: *Listing*
:toc: macro
:toclevels: 7
:sectnumlevels: 7
:sectanchors:
:sectlinks:
:plusplus: ++
:sectnums!:
== Best Practices, Coding Conventions, and Style
[.text-center]
_Revision 5_ +
_2020-09-22_
[.text-center]
*Status:* [red]*Approved* / [red]*Active*
toc::[]
== Typographic and Syntactic Conventions
The following syntactic conventions are used throughout this document:
_shall_::
is used to indicate a mandatory rule or guideline that must be adhered
to without exception to claim compliance with this specification.
_should_::
is used to indicate a rule or guideline that serves as a strong
preference to suggested practice and is to be followed in the absence of
a compelling reason to do otherwise.
_may_::
is used to indicate a rule or guideline that serves as a reference to
suggested practice.
== Introduction
There are likely as many unique combinations of software engineering and
development standards, conventions, and practices as there organizations
that do such work. This document pulls together those that Project
Connected Home over IP believes best for our organization, its efforts,
and products that consume those efforts, with a particular emphasis on
embedded systems with C or C{plusplus} language development and runtime
environments.
This document and requirements should be considered canonical for all
Project Connected Home over IP shared infrastructure software, including
both RTOS-based and non-RTOS-based projects on both tightly- and
loosely-constrained system platforms.
The document is broadly categorized at the highest level into:
* Best Practices and Conventions
* Format and Style
And, within conventions, further sub-categorized into those that apply
to:
* Tightly-constrained
* Loosely-constrained
system platforms. Applicability to tightly-constrained systems also
generally applies to shared infrastructure software that is used on both
tightly- and loosely-constrained systems.
link:#id.jzphr1iiku89[Figure 1 belowattempts to illustrate both
qualitative and quantitative applicability of these guidelines to
Project Connected Home over IP software.
Generally, product-specific applications have the greatest flexibility
and latitude in applying these guidelines to their software. Whereas,
shared infrastructure bears the least flexibility and bears the greatest
adherence to these guidelines.
image:CODING_STYLE_GUIDE-figure1.png[Figure 1. Graphical summary of the
qualitative and quantitative applicability to Project CHIP software.]
[[id.jzphr1iiku89]]
[.text-center]
*Figure 1.Graphical summary of the qualitative and quantitative
applicability to Project CHIP software.
:sectnums:
== Standards
Project CHIP embedded software development adopts the minimum C and C{plusplus}
standards listed in Table 2.1 below.
[[t.4d8bfeef046f29261fc72f1a903d6d10a909957a]][[t.2]]
[cols=3,options="header"]
|===
|Language |Minimum Standard |Aliases
|C|ISO9899:1999|ISO C99, C99
|C{plusplus}|ISO14882:2014|ISO C{plusplus}14, C{plusplus}14
|===
[.text-center]
*Table 2.1.* C and C{plusplus} language minimum standards adopted by Project CHIP
software.
Product-specific software may elect to use later standards to the extent
their software is not broadly shared inside or outside Project CHIP.
=== C
Project CHIP embedded software development uses and enforces the
ISO9899:1999 (aka ISO C99, C99) C language standard as the minimum.
Wherever possible, particularly in non-product-specific,
shared-infrastructure software, toolchain-specific (e.g GCC/GNU)
extensions or the use of later standards shall be avoided or shall be
leveraged through toolchain-compatibility preprocessor macros.
==== Motivation and Rationale
At the time of this writing, the C99 standard has been out for over 20
years. Project CHIP and both the new and contributed source code that
comprise it have only existed for the last seven to eight of those
20-plus years.
This is beyond more than adequate time for this standard to be pervasive
throughout any toolchain vendors C compiler and saves team members from
worrying about ISO9899:1990 (aka ISO C90, C90) portability issues that
have long-since been solved by C99.
=== C{plusplus}
Project CHIP embedded software development uses the ISO14882:2014 (aka
ISO C{plusplus}14) language standard as a baseline for source code
compatibility. Conformance with other standards, for example, ISO14882:1998
(aka ISO C{plusplus}98), may be additionally required in cases where wider
portability is necessary, but in all cases, ISO C{plusplus}14 is the baseline
requirement.
Wherever possible, particularly in non-product-specific,
shared-infrastructure software, toolchain-specific (e.g GCC/GNU)
extensions or the use of later standards shall be avoided or shall be
leveraged through toolchain-compatibility preprocessor macros.
==== Motivation and Rationale
CHIP strives to use the latest C++ functionality as long as existing compilers
support such standards.
C{plusplus}14 is considered pervasive enough to be used. As compilers start
supporting standards such as C{plusplus}17, C{plusplus}20 and beyond,
CHIP may follow suit.
== Conventions and Best Practices
=== Common
The following sections summarize those best practices that are
independent of particular nuances of either the C or C{plusplus} languages.
==== When in Rome
The most important convention and practice in the Project CHIP embedded
software is "_When in Rome..._", per the quote below.
[quote, St. Ambrose]
____
If you should be in Rome, live in the Roman manner; if you should be
elsewhere, live as they do there.
____
===== Motivation and Rationale
At this stage in the work groups and the teams life cycle, it is rare
the project or subsystem that is entirely new and built from scratch.
More often than not, development will involve extending, enhancing, and
fixing existing code in existing projects.
When in this situation, it is mandatory you observe how things are done
in this context and do the best that you can to follow the prevailing
conventions present. Not doing so can lead to readability and
maintenance problems down the line and will likely earn you the
disapprobation of the codes _owner_ or other team members.
Your extensions or fixes to existing code should be *indistinguishable*,
stylistically, from the original code such that the only way to
ascertain ownership and responsibility is to use the source code control
systems change attribution (aka _blame_) feature.
If you find the conventions so foreign or otherwise confusing, it may be
best to let whoever owns the file make the necessary changes or seek the
counsel of others in the group to find out what the right thing to do
is. Never just start changing code wholesale for personal reasons
without consulting others first.
==== Language-independent
===== Commenting Out or Disabling Code
Unused code shall not be disabled by commenting it out with C- or
C{plusplus}-style comments or with preprocessor `#if 0 ... #endif` semantics.
====== Motivation and Rationale
Code should either be actively maintained and "in" the source base for a
purpose or removed entirely. Code that is disabled in this way is
generally sloppy and does not convey a sense of certainty and direction
in the code.
Anyone who is interested in the history of a particular source code file
should use the source code control system to browse it.
Code that is debug- or test-only should be moved to a conditionally
compiled test source file or conditionalized with an appropriate
`WITH_DEBUG`, `WANT_DEBUG`, `WITH_TESTS`, `WANT_TESTS`, or some similar such
preprocessor mnemonic that can be asserted from the build system.
===== Use C _stdint.h_ or C{plusplus} _cstdint_ for Plain Old Data Types
Standard, scalar data types defined in _stdint.h_ \(C) or _cstdint_ (C{plusplus})
should be used for basic signed and unsigned integer types, especially
when size and serialization to non-volatile storage or across a network
is concerned.
Examples of these are: `uint8_t`, `int8_t`, etc.
====== Motivation and Rationale
These types have been effectively standardized since C99 and should be
available on every platform and provide more neutral portability than
OS-specific types such as `u8`, `UInt8`, etc. Moreover, because these are
pervasive, you do not need to spend any time and energy as a developer
and engineer creating more such types on your ownthe compiler vendors
have already done the hard work for you.
Additionally, using traditional scalar types such as `char`, `int`, `short`, or
`long` have portability issues where data width is concerned because these
types are either signed- or sized-differently on different processor
architectures and and ABIs for those architectures. For example, a char is signed
on some architectures and unsigned on others and a long is 32-bits on some
architectures and 64-bits on others.
==== Language-dependent
===== C{plusplus}
====== Avoid `using namespace` Statements in Headers
By doing this, you are effectively forcing every other module that
includes the header to also be using the namespace. This causes
namespace pollution and generally defeats the purposes of namespaces.
Fully-qualified symbols should be used instead.
=== Tightly-constrained Systems and Shared Infrastructure
Applicability to tightly-constrained systems also generally applies to
shared infrastructure software that is used on both tightly- and
loosely-constrained systems.
==== Avoid Heap-based Resource Allocation
Heap-based resource allocation should be avoided.
===== Motivation and Rationale
As emphasized throughout this document, the software produced by Project
CHIP is consumed both inside and outside Project CHIP, across a variety
of platforms. The capabilities of these platforms are broad, spanning
soft real-time, deeply-embedded systems based on RTOSes that
may cover life safety and/or physical security applications to richer,
softly-embedded systems based on non-RTOS platforms such as Darwin or
Linux. While the latter are apt to have fully-functional heaps, the
former explicitly may not.
Consequently, when planning new or extending existing Project CHIP code,
consider the platforms to which the code is targeted. If the platforms
include those deeply-embedded platforms absent functioning heaps, then
heap-based resource allocation is absolutely forbidden. If not,
consideration should be made to the cost / benefit trade-offs of
heap-based allocation and, if possible, it should be avoided using one
of the recommended techniques below.
===== Alternatives
In either case, recommended resource allocation alternatives are:
* In Place Allocation and Initialization
* Pool-based Allocators
* Platform-defined and -assigned Allocators
The interfaces in https://github.com/project-chip/connectedhomeip/blob/master/src/lib/support/CHIPMem.h[_src/lib/support/CHIPMem.h_] provide support for
the latter two alternatives.
====== Use In Place Allocation and Initialization
Regardless of whether the source code and runtime are C or C{plusplus}, the
first step is creating storage for the object being allocated and
initialized. For simple
https://en.wikipedia.org/wiki/Passive_data_structure[plain-old-data
(POD)] data structures, this can be done by just allocating the
structure at an appropriate scope. Alternatively, _raw_ storage can be
allocated and then cast. However, great care must be taken with the
latter approach to ensure that natural machine alignments and language
strict-aliasing rules are observed. With the simple data structure
declaration, the compiler does this on your behalf. With the raw
approach, you must do this.
Once the storage has been allocated, then use symmetric initializers and
deinitializers such as those, for example, for `pthread_attr_t`. An
example is shown in the listing below.
[source,C,caption='',title='{listing-caption} *{counter:refnum}*. Using in place allocation and initialization in C or C{plusplus}.']
----
#include <pthread.h>
#include <stdint.h>
...
// Preprocessor Definitions
// Allocate the structure using "raw" storage.
#if defined(__cplusplus) && (__cplusplus >= 201103L)
#include <type_traits>
#define chipDEFINE_ALIGNED_VAR(name, size, align_type) \
typename std::aligned_storage<size, alignof(align_type)>::type name;
#else
#define chipDEFINE_ALIGNED_VAR(name, size, align_type) \
align_type name[(((size) + (sizeof (align_type) - 1)) / sizeof (align_type))]
#endif // defined(__cplusplus) && (__cplusplus >= 201103L)
// Forward Declarations
extern void * foobar_entry(void *aArgument);
// Global Variables
#if USE_STRUCT_STORAGE
// Allocate the structure directly.
static pthread_attr_t sThreadAttributes;
#elif USE_RAW_STORAGE
static chipDEFINE_ALIGNED_VAR(sThreadAttributes, sizeof (pthread_attr_t), uint64_t);
#endif // USE_STRUCT_STORAGE
int foobar()
{
int retval;
int status;
pthread_t thread;
pthread_attr_t * attrs = (pthread_attr_t *)&sThreadAttributes;
// Now "construct" or initialize the storage.
retval = pthread_attr_init(attrs);
if (retval == 0)
{
retval = pthread_create(&thread, attrs, foobar_entry, NULL);
if (retval == 0)
{
status = pthread_join(thread, NULL);
if (status != 0)
{
retval = status;
}
status = pthread_attr_destroy(attrs);
if (status != 0)
{
retval = status;
}
}
}
return (retval);
}
----
For non-scalar types and objects such as C{plusplus} classes, this gets slightly
trickier since C{plusplus} constructors and destructors must be accounted for
and invoked. Fortunately, C{plusplus} has placement new which handles this.
The listing below modifies the listing above using C{plusplus} placement new
to ensure the class is properly constructed before initialization and
destructed after deinitialization.
[source,C++,caption='',title='{listing-caption} *{counter:refnum}*. Using C{plusplus} placement new for in place allocation and initialization.']
----
#include <new>
#include <pthread.h>
#include <stdint.h>
...
// Preprocessor Definitions
// Allocate the structure using "raw" storage.
#if defined(__cplusplus) && (__cplusplus >= 201103L)
#include <type_traits>
#define chipDEFINE_ALIGNED_VAR(name, size, align_type) \
typename std::aligned_storage<size, alignof(align_type)>::type name;
#else
#define chipDEFINE_ALIGNED_VAR(name, size, align_type) \
align_type name[(((size) + (sizeof (align_type) - 1)) / sizeof (align_type))]
#endif // defined(__cplusplus) && (__cplusplus >= 201103L)
// Type Declarations
class ThreadAttributes
{
public:
ThreadAttributes() {};
~ThreadAttributes() {};
operator pthread_attr_t *() { return &mAttributes; }
private:
pthread_attr_t mAttributes;
};
// Forward Declarations
extern void * foobar_entry(void *aArgument);
// Global Variables
static chipDEFINE_ALIGNED_VAR(sThreadAttributes, sizeof (ThreadAttributes), uint64_t);
int foobar()
{
int retval = -1;
int status;
pthread_t thread;
ThreadAttributes * ta;
pthread_attr_t * attrs;
ta = new (&sThreadAttributes) ThreadAttributes;
if (ta != NULL)
{
attrs = static_cast<pthread_attr_t *>(*ta);
// Now "construct" or initialize the storage.
retval = pthread_attr_init(attrs);
if (retval == 0)
{
retval = pthread_create(&thread, attrs, foobar_entry, NULL);
if (retval == 0)
{
status = pthread_join(thread, NULL);
if (status != 0)
{
retval = status;
}
status = pthread_attr_destroy(attrs);
if (status != 0)
{
retval = status;
}
}
}
ta->~ThreadAttributes();
}
return retval;
}
----
====== Use Pool-based Allocators
In place allocation allows the successful allocation, initialization,
deinitialization, and deallocation of a single object allocated from
preallocated storage. However, if the desire exists for a fixed,
configurable pool of objects where 0 to `n` of such objects can be
allocated at any one time, a pool allocator for that specific object
type must be created.
As shown in the listing below, a pool allocator for a `Foo` class of
`CHIP_FOO_COUNT` objects is effected, assuming the existence of another
helper class, StaticAllocatorBitmap, which uses a bitmap to track the
storage of objects from a static array of storage.
[source,C++,caption='',title='{listing-caption} *{counter:refnum}*. Using pool-based allocators.']
----
#include <stdint.h>
// Preprocessor Definitions
// Allocate the structure using "raw" storage.
#if defined(__cplusplus) && (__cplusplus >= 201103L)
#include <type_traits>
#define chipDEFINE_ALIGNED_VAR(name, size, align_type) \
typename std::aligned_storage<size, alignof(align_type)>::type name;
#else
#define chipDEFINE_ALIGNED_VAR(name, size, align_type) \
align_type name[(((size) + (sizeof (align_type) - 1)) / sizeof (align_type))]
#endif // defined(__cplusplus) && (__cplusplus >= 201103L)
// Type Definitions
class Foo
{
public:
Foo();
Foo(const Foo &inFoo);
~Foo();
};
// Global Variables
static chipDEFINE_ALIGNED_VAR(sFooAllocatorBuffer, sizeof (StaticAllocatorBitmap), uint32_t);
static StaticAllocatorBitmap *sFooAllocator;
static void CreateFooAllocator(void *inStorage,
const StaticAllocatorBitmap::size_type &inStorageSize,
const StaticAllocatorBitmap::size_type &inElementCount,
StaticAllocatorBitmap::InitializeFunction inInitialize,
StaticAllocatorBitmap::DestroyFunction inDestroy)
{
sFooAllocator = new (sFooAllocatorBuffer)
StaticAllocatorBitmap(inStorage,
inStorageSize,
inElementCount,
inInitialize,
inDestroy);
}
static StaticAllocatorBitmap &GetFooAllocator()
{
return *sFooAllocator;
}
static void *FooInitialize(AllocatorBase &inAllocator, void *inObject)
{
memset(inObject, 0, sizeof(Foo));
return inObject;
}
static void FooDestroy(AllocatorBase &inAllocator, void *inObject)
{
return;
}
int Init()
{
static const size_t sFooCount = CHIP_FOO_COUNT;
static chipAllocatorStaticBitmapStorageDefine(sFooStorage, Foo, sFooCount, uint32_t, sizeof (void *));
int retval = 0;
CreateFooAllocator(sFooStorage,
sizeof (sFooStorage),
sFooCount,
FooInitialize,
FooDestroy);
return retval;
}
Foo * FooAllocate()
{
Foo *foo;
foo = static_cast<Foo *>(GetFooAllocator().allocate());
return foo;
}
void FooDeallocate(Foo *inFoo)
{
GetFooAllocator().deallocate(inFoo);
}
----
====== Use Platform-defined and -assigned Allocators
This is a variation on both in place allocation and pool-based
allocation in that it completely delegates resource allocation to the
system integrator and the platform on which the particular software
subsystem is running.
The advantage of this approach is that it allows the platform to decide
how resource allocation will be handled and allows the package to scale
independently of platform resource allocation.
The package may define default implementations for a few types of
platform allocation strategies, such as heap-based allocators and
pool-based allocators.
There are a range of granularities for achieving this type of
delegation, depending on the desired size of the API surface, as shown
in the listings below.
[source,C++,caption='',title='{listing-caption} *{counter:refnum}*. Using a common allocator method pattern with unique allocators per object, accessed from a unique singleton access per allocator.']
----
chipPlatformInitFooAllocator();
chipPlatformInitBarAllocator();
foo = chipPlatformGetFooAllocator().allocate();
chipPlatformGetFooAllocator().deallocate(foo);
bar = chipPlatformGetBarAllocator().allocate();
chipPlatformGetBarAllocator().deallocate(bar);
----
[source,C++,caption='',title='{listing-caption} *{counter:refnum}*. Using a common allocator method pattern with unique allocators per object, accessed from a common singleton access with type per allocator.']
----
chipPlatformInitAllocator(CHIP_FOO_T);
chipPlatformInitAllocator(CHIP_BAR_T);
foo = chipPlatformGetAllocator(CHIP_FOO_T).allocate();
chipPlatformGetAllocator(CHIP_FOO_T).deallocate(foo);
bar = chipPlatformGetAllocator(CHIP_BAR_T).allocate();
chipPlatformGetAllocator(CHIP_BAR_T).deallocate(bar);
----
[source,C,caption='',title='{listing-caption} *{counter:refnum}*. Using unique allocators per object.']
----
chipPlatformInitFooAllocator();
chipPlatformInitBarAllocator();
foo = chipPlatformFooAllocate();
chipPlatformFooDeallocate(foo);
bar = chipPlatformBarAllocate();
chipPlatformBarDeallocate(bar);
----
[source,C,caption='',title='{listing-caption} *{counter:refnum}*. Using a common allocator pattern with unique allocators per object, accessed from a common interface with type per allocator.']
----
chipPlatformInitAllocator(CHIP_FOO_T);
chipPlatformInitAllocator(CHIP_BAR_T);
foo = chipPlatformAllocate(CHIP_FOO_T);
chipPlatformDeallocate(CHIP_FOO_T, foo);
bar = chipPlatformAllocate(CHIP_BAR_T);
chipPlatformBarDeallocate(CHIP_BAR_T, bar);
----
:sectnums!:
== Recommended Reading
While the following references and reading are not part of the formal
best practices, coding conventions, and style cannon, they are
informative and useful guides for improving the style and quality of the
code you write:
. Jet Propulsion Laboratory.
http://lars-lab.jpl.nasa.gov/JPL_Coding_Standard_C.pdf[JPL
Institutional Coding Standard for the C Programming Language.] Version
1.0. March 3, 2009.
. Jet Propulsion Laboratory.
http://pixelscommander.com/wp-content/uploads/2014/12/P10.pdf[The
Power of Ten Rules for Developing Safety Critical Code]. December
2014.
. Meyers, Scott. Effective C{plusplus}: 55 Specific Ways to Improve Your
Programs and DesignsThird Edition. 2005.
. Meyers, Scott. More Effective C{plusplus}: 35 New Ways to Improve Your
Programs and Designs1996.
. Meyers. Scott. https://www.artima.com/shop/effective_cpp_in_an_embedded_environment[Effective C{plusplus} in an Embedded Environment]. 2015.
. Motor Industry Software Reliability Association. Guidelines for the
Use of the C Language in Critical SystemsMarch 2013.
. Motor Industry Software Reliability Association. Guidelines for the
Use of the C{plusplus} Language in Critical Systems. June 2008.
== Revision History
[cols="^1,^1,<2,<3",options="header"]
|===
|Revision |Date |Modified By |Description
|5 |2020-09-22 |Grant Erickson |Added Tightly-constrained Systems and Shared Infrastructure > Avoid Heap-based Resource Allocation
|4 |2020-09-15 |Grant Erickson |Added Common > Language-dependent > Avoid `using namespace` Statements in Headers
|3 |2020-09-01 |Grant Erickson |Added Common > Language-independent > Use C _stdint.h_ or C{plusplus} _cstdint_ for Plain Old Data Types
|2 |2020-07-09 |Grant Erickson |Added Common > Language-independent > Commenting Out or Disabling Code
|1 |2020-07-08 |Grant Erickson |Initial revision.
|===
[.text-center]
_Project Connect Home over IP Public Information_