The Tigerbeetle Style Guide is interesting reading, but here I’m going to focus on The Power of Ten – Rules for Developing Safety Critical Code by Gerard J. Holzmann at JPL/NASA from 2006 (Wikipedia). That article is almost 20 years old and I worked on software in and around flight systems from 1996-2009, so I’ve some experience of that culture. The target environments are remote and embedded systems where high reliability and recoverability of individual systems is key. It’s very different from the Google data center type world that I have been working in since 2012 where these books are key references: Software Engineering at Google and Google Site Reliability Books. Despite the differences, there are tons of places where the same techniques and tools lead to good results.
Also keep in mind the following from 2006:
C99 and C++03 were new. C++11 was a radical change.
Python 3.0 was not yet released and type annotations didn’t come until 2015.
Go, Rust, TypeScript, and Zig hadn’t been released.
Fortran 2003 was out, but the language doesn’t have a stdlib and https://stdlib.fortran-lang.org/ is a work in progress.
Automated testing / CI were not common or well developed then. Please write code with great tests and make them run automatically.
1. Rule: Restrict all code to very simple control flow constructs – do not use goto
statements, setjmp or longjmp constructs, and direct or indirect recursion.
This is still great advice. HDF5 is still full of gotos and that creates so much trouble. There are so many better ways to handle cleanup if you aren’t stuck in C99. My take for folks wanting to maintain a C ABI is to use C++ inside the code and then just expose C interfaces.
For simple and constrained problems, a quick recursive solution may still be useful if the code is much easier to follow. However, in general, recursion is not great for production or embedded systems.
2. Rule: All loops must have a fixed upper-bound. It must be trivially possible for a
checking tool to prove statically that a preset upper-bound on the number of iterations
of a loop cannot be exceeded. If the loop-bound cannot be proven statically, the rule
is considered violated.
This rule seems to imply that the code hasn’t properly sanitized inputs. Input validation and sanitization is critical for security and performance in general. In my opinion, this rule moves the responsibility to too late in the process.
3. Rule: Do not use dynamic memory allocation after initialization.
This is a good rule for long running embedded systems. However, it’s a total fail for most other types of code. It will introduce complexity and limitations where they aren’t needed. There are certainly places where memory pools / arenas are good, but they should be dictated by the particular data and algorithm. Only allocating at initialization should be the exception, not the rule.
In many cases (e.g. most of google3), the emphasis should be on keeping the stack size constrained. Many modern applications have hundreds of threads at any time. Excessive stack size means large amounts of wasted stack / excessive address space usage as the system will have to assume that all threads may use large amounts of stack.
4. Rule: No function should be longer than what can be printed on a single sheet of
paper in a standard reference format with one line per statement and one line per
declaration. Typically, this means no more than about 60 lines of code per function.
This is in general a good idea, but shouldn’t be a rule. Sometimes decomposing code into lots of functions makes it harder to follow. However, modern programming languages make writing tighter code much easier, so the places where long functions or methods are needed should be far fewer these days.
5. Rule: The assertion density of the code should average to a minimum of two
assertions per function. Assertions are used to check for anomalous conditions that
should never happen in real-life executions. Assertions must always be side-effect
free and should be defined as Boolean tests. When an assertion fails, an explicit
recovery action must be taken, e.g., by returning an error condition to the caller of the
function that executes the failing assertion. Any assertion for which a static checking
tool can prove that it can never fail or never hold violates this rule. (I.e., it is not
possible to satisfy the rule by adding unhelpful “assert(true)” statements.)
This rule is weird. asserts are always booleans in modern languages. If you are using C, please switch to C++ and make as many of the asserts be static_assert. But, I think a lot of what was asserts in the past should be unit tests now. Excessive asserting in code can make it hard to follow the code. If it is an assert, there should never be recovery action. An assert is a crash under test and nothing in production. Full stop. Error handling and asserts are two separate topics.
6. Rule: Data objects must be declared at the smallest possible level of scope.
This is pretty reasonable. It helps with debugging such that you don’t have to look over as much code when things go wrong. It generally makes test writing easier. Keeping around data when not necessary is wasteful and reduces the ability of the overall system. Especially in modern systems with many process each with many threads, waste of RAM greatly reduces how much can be done on each system.
7. Rule: The return value of non-void functions must be checked by each calling
function, and the validity of parameters must be checked inside each function.
First, this should start with a rule that the C standard library should be avoided as much as possible. I’ve seen code that checks every printf, fopen, fclose, fread, and fwrite and has error handling on each. The code size explodes without improving reliability. This is more about API design. Modern APIs are just so much less painful.
After that however, every return should be checked or explicitly ignored. The `_` convention is a great way to explicitly ignore the result.
8. Rule: The use of the preprocessor must be limited to the inclusion of header files and
simple macro definitions. Token pasting, variable argument lists (ellipses), and
recursive macro calls are not allowed. All macros must expand into complete
syntactic units. The use of conditional compilation directives is often also dubious,
but cannot always be avoided. This means that there should rarely be justification for
more than one or two conditional compilation directives even in large software
development efforts, beyond the standard boilerplate that avoids multiple inclusion of
the same header file. Each such use should be flagged by a tool-based checker and
justified in the code.
This is in general good. A big lesson for C folks should be to switch to C++ and convert as many #define values and macros to const_expr as possible. I love that Rust is immutable by default.
For python folks, doing imports in functions makes maintenance difficult. 🙁
9. Rule: The use of pointers should be restricted. Specifically, no more than one level of
dereferencing is allowed. Pointer dereference operations may not be hidden in macro
definitions or inside typedef declarations. Function pointers are not permitted.
Yes, pointers should still be generally restricted. However, how pointers are used should probably not be at all the same as in the past. In C++, unique_ptr is fantastic.
10. Rule: All code must be compiled, from the first day of development, with all
compiler warnings enabled at the compiler’s most pedantic setting. All code must
compile with these setting without any warnings. All code must be checked daily with
at least one, but preferably more than one, state-of-the-art static source code analyzer
and should pass the analyses with zero warnings.
Yes, turn on all checks possible and keep them on as errors. Also, for C / C++ code, please run all tests through the sanitizer modes. Please use fuzzing. Please use LLMs to look for other issues, but be warned that false positives, false negatives, and hallucinations are constant companions. Build for more than one arch (e.g. x86_64 and arm).