After six years of writing firmware in C++, I nearly forgot what it is like to develop in plain C.
In fact, I remember I wrote quite convoluted C macros to implement template-like containers, therefore when I was handed a prototype firmware to contribute, I thought I was quite fine. After all, I needed just to dust off my old library to be up and running.
Well in the past six years not only I wrote C++ firmware, but also I have been dangerously exposed to high doses of functional programming. Therefore my last C++ code looked a lot like (at least in my intentions) Scala code, just more verbose.
My first task on the firmware was to move error handling from a low-level function to a higher one. Actually the code just spun forever on the unexpected condition. I had to return either a value or an error indication.
Now, this is a common situation and there are a bunch of techniques to get ahead of it. The classical (and the most dreaded) one is to use pointers and NULL pointers. This is quite bad because if you don’t check for NULL pointer and you use the returned value you are in for a good (or bad according to your perspective) fireworks show. Also, this is rather uninformative – the caller only knows that the callee failed, but has no clue about the problem. It may be transient or permanent, it may be about wrong parameters or physical problems.
The other common way to solve this is to return an error code and pass the result via a pointer. The caller prepares some memory to accept the result and passes the pointer to this area along with the other function parameters. This solution is not that brilliant as well, since the caller may fail to provide space for the result or may use the result even if an error is reported.
Unfortunately, C doesn’t provide a safe and robust mechanism to handle errors (I don’t want to even consider errno or setjmp/longjmp as sane error handling strategies). If you want to do it, you are in for repetitive and boring coding, that’s possibly the reason why in so many C projects errors are scantily checked.
In Functional Programming errors are easily handled by an Either monad – Either is an object that can be … well either one thing or another. Usually, one thing is the error information and the other is the actual result. So a function that can somehow fail may return an Either with an error value if fails, or with an actual value if succeeds.
This is quite convenient because you can’t just take the result, you are forced to check if it is valid, moreover, you can chain together several Either and the monad magic allows you to execute all the path to the composed result only if all the Either contain valid values.
Back to C, it was clear that I had to write a macro-template to implement Either. Now the problem is that C has no constructor/destructor concept. Therefore if you want to handle object-like constructs you need to adopt some conventions that allow you given a type name to forge the constructor and destructor names.
But then you have copy constructors and move constructors. That’s annoying for user-defined types, but it is even more annoying for native types (int, unsigned, char…) since you need to box them somehow in order to make the template code working. And if you want to avoid excessive function calling, you need to make sure that such functions are declared as inline.
In order to simplify these macros, I opted for an error code implemented as an integral type (so no need for copy/move constructors nor destructors) while the result had to be a generic object required to have all the additional handling functions.
My template-macros go like this – I wrote a set of macros that creates the declaration of type and functions parameterized on the type and then another set of macros creates the implementation. So far so good, but then I wanted to write map/flatMap functions.
Map and flatMap are critical for composing monads, therefore they are half the reason to write this code. So, when I turned to map/flatMap I discovered that I can’t just use a macro once for all, in fact using the Scala syntax (for brevity sake) –
class Expect[E,V] { def map[U]( f: V -> U ) : Expect[E.U] = ??? }
map
is a template itself in a template class. Therefore it is not possible to define it once for all in a macro. I mean, while in C++ and Scala you can declare a map function, in C you can’t:
#define Expect_map_DECL( T__, E__, R__ ) ??? T__##_map( ??? (*f)( R__ const* ) )
T__
is the name of the Expect type. This type has an error of type E__
and a result of type R__
. But what is the name of the resulting Expect, i.e. the one that has E__
as error and the type returned by f
as a result? And how do I declare the returned value of f
in advance without knowing what f
is.
Therefore map
has to be defined each time you need to use it. And when you use map
, then you have to re-instantiate all the methods for the new Expect. Not impossible, but quite cumbersome.
Sorted out everything I had my Expect super macro… I started to use it in the source code, but it leads to absolutely verbose and boring code.
It just requires a legendary level of concentration and focus on details to keep everything aligned. And then you’ll have a hard time to debug (all code is inside macros).
So, yes, it can be done, but there’s no point in doing it if you can avoid it. And, guess what, actually you can use C++ for writing firmware.
I thought about it for a long four seconds and decided to dump my Expect C macro for C++. The first problem was to add the C++ support to the C Eclipse project. Eclipse is one of the most frustrating IDE/editor I ever worked with. I try to avoid it as much as possible, but this is the ST official IDE and the project I received was managed by Eclipse. Adding C++ is quite easy if you know how to do it. Thanks to my friend Francesco, I found that you need to add the C++ nature by adding the line:
<nature>org.eclipse.cdt.core.ccnature</nature>
in the <natures>
section.
Then you have to replicate includes and defines. And this is the easy part. Now you have to compile and link.
In order to compile some files in C++, you have to change their extensions and fix quite a lot of warnings that stem from the differences between the two languages. Then you need to wrap C++ functions that need to be called from C in
#if defined( __cplusplus ) extern "C" { #endif void functionCallableFromC( void ); #if defined( __cplusplus ) } #endif
This is needed to switch off the C++ name mangling.
This is quite some work, but the problems are just beginning since the STM32CubeIDE C++ toolchain doesn’t support exceptions. Well, I somewhat cheated – I had a comprehensive C++ library that includes an Either template (and a pretty solid one). This library has some components that rely on exceptions. So it was time to wrap all the exceptions in capability macro checking. Something like:
#if defined( __cpp_exceptions ) if( n >= Limit ) { throw std::exception( "despair!" ); } #else assert( n < Limit ); #endif
Most of the exceptions were like this, but some were a bit more troublesome, so I opted to disable components in such cases.
Eventually, also the more functional components of the library where compiled and there I discovered that the compiler failed on specific noexcept
constructs such as:
auto map( F f ) const noexcept( noexcept( f( std::declval<T>() ))) -> Option<std::invoke_result_t<F,T>> {...}
The odd noexcept
structure means that the map function doesn’t throw if the function f invoked on type T is noexcept
.
The problem was that the toolchain provided by the IDE is old (or, well, not up to date as it could be), dating back to 2017.
It had been quite a ride, but I wasn’t ready to give up yet, so I asked the Internet how to update STM32CubeIde toolchain.
After a bit of grokking, I ended up in an ST forum where they wrote that updating the toolchain was something they had to do for the next version… possibly. And that there is no easy way to add your toolchain of choice. The suggested method included replacing the binaries of the official toolchain.
This was too much. I decided it was time to stop fighting the tool and add that damn NULL pointer and proceed with the project, hoping that better times would come and I could write code without piling up technical debt.