You hit the nail exactly, westfw.
This is not about "normal" C or C++ or their relative merits. This is about a funky environment that works best with a specific subset of both, both in the language spec, and in the approach. Plus quite a few utterly nonstandard GNU and ELF tricks, because they're the most widely supported toolchain (with those tricks supported by many non-GNU toolchains also).
As an analog:
Many people know that when dealing with aluminium, especially cast aluminium, you can actually use woodworking tools to work with it. They don't work perfectly, and dedicated aluminium tools will perform better and leave a nicer finish; but you can do it in a pinch without damaging your woodworking tools if you do it carefully (not forcing the tool, letting it do its job at its pace; not letting the tools bind or jam, heat up, etc).
My approach here is to AVOID starting with that, and instead examine the properties of aluminium and see how it can be machined and worked with, and what kind of tools work best for it; what to avoid, what to do in a pinch, and what some of the hard-won practical tips and tricks are.
I believe, and claim, that starting by comparing to woodworking or working with steel and hard metals, is problematic, because it does not "build" new knowledge; it relies on existing knowledge being correct, and teaching by comparison. Instead, I want learners to start at the very basics, so they can
expand their understanding, tie it to whatever they already know – and optionally fix/expand their knowledge elsewhere, if they discover they learned/believe something incorrectly – without having to start with their existing knowledge and habits and
change those to apply to this situation.
It makes a lot of sense, even if it offends those who insist on comparing C and C++ merits, and using a singular tool for everything ignoring the task at hand.
Example: ELF-assisted automatically-collected arrays
Since most embedded toolchains use the ELF file format for object file representation, we can use the ELF file format properties, and the linker, to do useful work at compile time. This is most commonly used to collect variables and objects declared at random places into a single, contiguous array of memory; either RAM or ROM/Flash.
The variables or objects only need to be declared in the file scope, but can be static (their name/symbol not exported outside the compilation unit or scope), with a custom
section attribute, using syntax
__attribute__((section ("name"))).
For full control of how sections are mapped to the final binary or target address space, a linker file is used. However, most/all default to a linker file that has catch-all rules based on prefixes; for example, that
".rodata.foo" is merged with read-only data,
".rodata", and so on; the linker even provides symbols whose address corresponds to the beginning and end of these sections. So, for simple cases, like collecting structures or objects that define a supported command the embedded device provides into a single consecutive array with known size, one only needs to add the section attributes, declare the section start and end "variables" as externs, and that's it; the linker will do the work for you, even if the structures and objects are defined in a number of different compilation units (separate object files).
The only "trick" here is that each of the structures/objects/variables thus collected needs to have a specific size; and this is affected by packing and padding rules. Either the size must be the same for all objects and match that of an array element of that object type, in which case it can be treated as a normal array; or the exact size must be at the beginning of the object, so that the "array" can be traversed like a list.
For base type objects (data pointers, for example), or objects of the same type with a suitable size (end padding is often a tricky problem), you don't need to bother, and just keeping the structures as C++ will work absolutely fine. (This is exactly how GCC implements static initializer and finalizer functions: their addresses are collected in .init_array and .fini_array sections, which the library start and exit code uses to call those functions without parameters.)
AIUI, C rules differ, so you may need to use
extern "C" { ... } and declare them as C structures, with members in specific order and explicit padding members (making each
N-bit member aligned to
N-bit boundary, with largest members first), to get this to be portable across different hardware architectures, even across 8-bit/32-bit ones. I would also use the C rules for objects of varying size, with the size as the first member in each object.
Usually, a bit of preprocessor macro magic is used, so that all the source code shows is something like
EXPORT_COMMAND("foo", command_processing_function);in the file scope of a module or source file implementing a specific command or command set. The
command_processing_function does not even need to be exported; it is sufficient for the symbol to be visible at that point in the file scope.
I hope you see what this can mean for typical command-processing firmware implementation; how much cleaner and simpler it can make both the source organization and the build machinery. Yet, it is rarely used, because it is not something you use or teach others about in standard hosted environments, because of non-standardness and limited portability there. Here, the situation is different. It is perfectly suitable for the approach/paradigm in this environment.
(This is also exactly how the Linux kernel implements kernel module information including licenses and module options/parameters: it uses the linker to do the work. That's where I initially learned about it. I have used it in systems programming on ELF-based architectures, too; it works fine in userspace in Linux, Mac OS, BSDs, etc.)