This is the second in the series of postings that describe the quality processes at SEGGER. This article picks a single design principle from the many we use when developing our software products, one that is deeply rooted.
Let’s start!
Product configuration and spaghetti defines
SEGGER products are runtime configurable by design which means that you configure the capabilities of the end product when you link your application. The base configuration can be considered a “white label” product, one that has only basic functions but can be configured to produce a final application with exactly the right feature set for your customers—no more than needed, and nothing less than required.
To configure what you need, SEGGER provide a collection of “add” functions that add features to the minimal footprint product. As an example, emSSL has a base framework devoid of ciphers, hashes, MACs, mutual authentication, and public key cryptography support (to name a few). During initialization, features are added to the runtime which plug into parts of the product and customize it for use. Here’s an example:
/********************************************************************* * * Configuration. */ void SSL_MAC_Add (const SSL_MAC_API *pAPI); void SSL_CIPHER_Add (const SSL_CIPHER_API *pAPI); void SSL_CURVE_Add (const SSL_CURVE *pCurve); void SSL_SUITE_Add (const SSL_SUITE *pSuite); void SSL_SIGNATURE_SIGN_Add (const SSL_SIGNATURE_SIGN_API *pAPI); void SSL_SIGNATURE_VERIFY_Add (const SSL_SIGNATURE_VERIFY_API *pAPI); void SSL_SIGNATURE_ALGORITHM_Add (unsigned ID); void SSL_ROOT_CERTIFICATE_Add (SSL_ROOT_CERTIFICATE *pCert); void SSL_PROTOCOL_Add (const SSL_PROTOCOL_API *pAPI); void SSL_CLIENT_ConfigMutualAuth (void); void SSL_SERVER_ConfigMutualAuth (void);
Once added, emSSL (in a debug configuration) checks to ensure that the configuration makes sense, that nothing is missing for features that have dependencies and, crucially, nothing is “added” that has no need to be there. And it displays a clear message for anything it finds. I’m not giving much away by showing some of the SSL code:
// // Error on missing cipher; warn on superfluous cipher. // for (i = SSL_CIPHER_ID_NULL+1; i < SSL_CIPHER_ID__MAX; ++i) { if (aRequiredCiphers[i] && SSL_Globals.aCIPHER[i] == NULL) { Halt = 1; SSL_WARN((SSL_WARN_CONFIG, "SSL: Cipher %s required but not added!", SSL_SUITE_GetCipherName((SSL_CIPHER_ID)i))); } else if (!aRequiredCiphers[i] && SSL_Globals.aCIPHER[i] != NULL) { SSL_WARN((SSL_WARN_CONFIG, "SSL: Cipher %s added but not required!", SSL_SUITE_GetCipherName((SSL_CIPHER_ID)i))); }
We make sure that all parts of software are decoupled so that, for instance, using emSSL in client mode does not drag in any of the server capability when linked. We expect linkers to eliminate code that is never called, which is not an unrealistic expectation these days: the SEGGER linker certainly does.
Why choose runtime configuration?
A good question, why do SEGGER choose to do it this way? The classic way is to set a whole slew of preprocessor definitions at compile time and build the product. We prefer runtime configuration for a number of very good reasons:
- It enables us to deliver a product in object code form that still has a minimal code footprint yet scales to the most capable product simply by adding features.
- Some customers, such as silicon vendors and OEMs, deliver SEGGER object code libraries to their customers and these need to be totally configurable without delivering thousands of libraries compiled in different ways each offering a different set of features. A good example of SEGGER’s capability is the emWin product that is widely licensed and available for most microcontrollers as a download from tier-one silicon manufacturers.
- The preprocessor is a blunt instrument for software configuration.
Preprocessor definitions…for experts only
Let me explain that last point. When you look at source code in an editor, you want to see just something you can read top to bottom without mental gymnasics. Far too many times I have seen what can only be described as “spaghetti definitions” when #ifdef and friends compile in or compile out code in the name of efficiency and take out code in the middle of a statement. One particular RTOS and one particular SSL product come instantly to mind where literally hundreds of defines can be set to compile down a product to its smallest size. I wonder about the genius, or crazed mind, that can deliver more preprocessor statements than lines of C code in a single function.
All these #defines make the code look ugly, especially when the #ifdef is used to add in or take out some condition or part of code, like this:
/* For DTLS if this is the initial handshake, remember the client sequence * number to use it in our next message (RFC 6347 4.2.1) */ #if defined(MBEDTLS_SSL_PROTO_DTLS) if( ssl->conf>transport == MBEDTLS_SSL_TRANSPORT_DATAGRAM #if defined(MBEDTLS_SSL_RENEGOTIATION) && ssl->renego_status == MBEDTLS_SSL_INITIAL_HANDSHAKE #endif ) { /* Epoch should be 0 for initial handshakes */
Sure, many development environments now help you by desaturating code in the editor if it is made inactive by #if…#else…#endif preprocessor statements. This certainly helps when you have a really nasty preprocessor condition, but why should this be necessary? Just because you have the capability doesn’t mean you have a responsibility to use it.
We can spare a few bytes in code size or a few cycles in performance to have clean source code that you can read and debug through, not wonder whether something in plain sight actually made it into your application.
Conditionally-compiled code makes your source look ugly and it makes it hard to maintain. You have to test, in each configuration, whether something is compiled in or not. And you need to test the interactions between each set of definitions to ensure that customers don’t end up with a set of #defines that configure a product which will not then compile or, worse, lead to an undiagnosed misconfguration that fails at runtime and is deployed into a product.
Latent problems
And using #ifdef or #if defined hides other problems: the compiler doesn’t know anything about the thing you are testing, just that it needs “to be defined or not be defined”. As such, the compiler can give you zero assistance when you mis-spell a configuration definintion. Look at that code again, shown below, can you detect the error here? Can your compiler assist you and save you from your single-character mistake?
/* For DTLS if this is the initial handshake, remember the client sequence * number to use it in our next message (RFC 6347 4.2.1) */ #if defined(MBEDTLS_SSL_PROTO_TLS) if( ssl->conf->transport == MBEDTLS_SSL_TRANSPORT_DATAGRAM #if defined(MBEDTLS_SSL_RENEGOTIATION) && ssl->renego_status == MBEDTLS_SSL_INITIAL_HANDSHAKE #endif ) { /* Epoch should be 0 for initial handshakes */
SEGGER does not use #ifdef, #ifndef, #if defined, or #if !defined as compiler assistance for diagnosing errors in these cases is absent. Instead we set controlling preprocessor values to 0, 1, or some other number so that, should we ever mistype something going into the preprocessor, the preprocessor can diagnose it:
#if DEBUG _DoDebugStuff(); #endif #if DEGUG _ReallyNeverMeantThat(); #endif
In the above, any good preprocessor will warn you that “DEGUG” is undefined and you’re protected. The same can’t be said when using #ifdef instead of #if because, well, DEGUG being undefined is not an error, it’s just a valid configuration the compiler is clueless to help with. Just don’t do this, it’s nonsense and you will fall under the bus one day.
Productivity
Let me also touch on one more item that should make you think. Each time you change a definition, say to add or remove some option in your application, that change in a header file or IDE setting means that your entire library needs to be recompiled, and usually your entire application along with it. That’s a pain, it takes time. In SEGGER’s world, you simply comment in or out a line of code in your setup function and relink: no recompile required!
Good engineering delivers savings in ways you don’t appreciate until you endure the pain of a product that is badly engineered.
Don’t get me wrong, defines have their place and SEGGER do use them, but not for such fine-grained configuration in “if” statements. Rather, the high-level defines are used to build a product with additional checks, logging, or to optimise algorithms for size or speed. This is the correct use for the preprocessor, one that makes total sense.
Next time
Fact: code that has not been tested doesn’t work. And there’s more to say about the benefits of runtime configuration.