- Usage
- Settings
- Modules
- Admin
- ???
- Client
- ???
- Admin
- Introspection
- ???
- Receive
- ???
- Route
- ???
- Send
- ???
- Validate
- ???
The following guidelines are based on the practices used in developing code for mission critical and safety systems. While these are guidelines and not absolute law, there must be a very good and valid reason not to follow them. "I don't think its important..." or "That's not how I write code" are not good reasons.
By using and following these guidelines, the up-time of an executable can be measured in years or even decades.
These guidelines have been written to be as language neutral as possible.
Design refers to how the code should be designed, implemented, and/or structured. Reason explains why the Design is important.
Code Flow: No goto
, No exception
Design
Use the language constructs to control code flow, do not use goto
.
Handle errors where they occur, do not use exception
s.
Reason
Contrary to popular dogma, goto
is not evil.
It will, however, lead to unexpected code flow changes.
The use of goto
can also confuse some static-code analyzers.
When there is the temptation to use a goto
, treat that as a sign of opportunity to optimize the structure of the code.
exception
s have many problems.
As error handling, they cause error handling to be away from where the error occurred, so the error handler has lost important context.
Exceptions will also cause the execution stack to be unwound, going up the call stack until an exception handler is found.
All of those functions the exception passes through going up the call stack completely lose their state which will cause problems over time.
As a means of handling "unexpected" and "exceptional" conditions, as soon as those conditions are defined, how are they "unexpected" or even "exceptional"?
For the argument of "error codes" can cause long if..else if..else if
chains, how is that any different from long try..catch..catch
chains?
Most importantly, exception
s create hidden code paths, causing the same side-effects as goto
.
Dynamic Memory Allocation
Design
Only use malloc()
/new
during application initialization, and free()
/delete
during application initialization or termination.
Only stack-based memory allocation will take place during runtime.
Reason
Most application instabilities and insecurities are due to improper use of dynamic memory allocation.
Even with careful use of dynamic memory allocation, frequent memory allocations and releases can lead to memory fragmentation, which can cause system instability.
Limiting memory allocations to program initialization and memory deallocations to program initializations and program terminations will result in a more stable application.
Loops Must Have A Limit
Design
Whenever a loop is used such as for()
and while()
, in addition to the expected loop termination check also have a maximum number of iterations check.
Do not use recursive functions.
Reason
The extra maximum limit check will not only prevent execution hangs but can also be used to locate programming errors.
For example, iterating over a list that has a terminating element can cause unexpected results if that terminating element is missing or corrupted. This list could be a C-String, a linked-list, or any other similar data structure. If the language has an iterator for traversing a collection of elements, how does the iterator know it has reached the end of the collection? If the iterator is maintaining a count/index, good. If the iterator is looking for some data to signal the end of the collection, that is bad and can lead to incorrect looping behavior.
Recursive functions are a form of looping and have the same problems as loops with the potential of smashing the stack. Plus, recursive functions are hard to debug and problematic for code analyzers.
When To Ignore
Intentional infinite loops should not have a maximum limit count, obviously.
Warnings Are Errors
Design
All warnings must be treated as errors.
Reason
While a warning may not mean the executable will be broken, it does mean it can break. This "potential" breakage will happen in the future and probably not on your system. To avoid the "But it works for me" problems, treat all warnings as errors and fix them.
This applies to warnings from code analyzers, compilers, interpreters, and any 3rd Party libraries/tools used.
Crash Early
Design
When invalid data reaches a critical section of code, report an error and crash.
Reason
During development, a crash immediately after an error tells the developer exactly where the problem was found. This makes debugging much easier and helps prevent any potential data corruption that may hide the problem if execution continued. The first part of a function should be parameter validation, any problems found should result in a crash. After the function parameters have been validated, the rest of the function can proceed knowing it has good data.
If a crash happens during testing, again, all the context of the crash will be available to the developer for debugging the problem.
>
A crash during release means: You Failed.
Objects Have Short Lifespans
Design
Objects should only be created when it is needed and destroyed as soon as possible.
Reason
By keeping object creation and destruction as close together as possible, greatly reduces resource leaks. When combined with the other guidelines, the creation and destruction of objects should be visible at the same time and in the same function. For debugging, objects with short lifespans are much easier to trace since they exist for only a few lines of code.
Short object lifespans also means the object is not reused.
For example, using a len
object to store the length of a string and then using the same len
object for the length of a list.
Instead use string_len
and list_len
.
One Level
Design
Pointer de-referencing should only be one level deep.
Class/Interface/Object inheritance should only be one level deep.
Reason
When working with pointers, reading and debugging code like a->b->c = thing
is difficult because c
is not in the immediate context.
A better implementation is B* b = a->b; b->c = thing;
.
Here B* b
is now in the context and it is easier to understand c
.
Even better would be B* b = a->b; C* c = b->c; c = thing;
.
Many layers of inheritance leads to objects using more resources than needed. Debugging is also made more difficult because the interaction between all the layers must be understood. Using flat object hierarchies and object composition can lead to better designs than deep hierarchies.
When inheriting an object from a 3rd party library, that counts as one level of inheritance even if the 3rd party object has multiple levels of inheritance.
Macro Functions (C/C++)
Design
Only use Macro Functions when code must be inlined.
Macro Function parameters should only be used once.
Macro Functions should not be more that one level deep.
Reason
The C/C++ keyword inline
is a "hint" to the compiler to consider inlining that function.
The compiler may ultimately decide not to inline that function.
Conversely, the compiler may decide to inline a function that does not use the inline
keyword.
(Yes, it can be argued that the inline
keyword is useless.)
Using Macro Functions is a way to force a function to always be inlined.
When using Macro Functions, each parameter must only be used once.
The classic example of "Why Macros Are Bad" is: macro_func(a++, ++b);
.
Besides being bad C/C++ code, using the parameters several times may have inconsistent results with macros.
If a Macro Function parameter will be used more than once, assign the parameter to a temporary object and then use that temporary object instead.
This will result in consistent behavior.
Just like with inheritance, a Macro Function should only be one level deep and flat. When nested Macro Functions are expanded, the resulting code can be difficult to read and debug.
When To Ignore
Using nested Macro Functions for data conversions can be useful and deep by necessity. Using C/C++ as an example:
/* Convert to Seconds */
#define SECONDS(x) (x)
#define MINUTES(x) SECONDS(x * 60)
#define HOURS(x) MINUTES(x * 60)
Function Limits
Design
Functions should be short and concise enough to fit within 60 lines from start to end.
Functions should have no more than 4 levels of indenting.
A line of code in a function should be no longer than 80 characters, including the indenting.
Reason
A function must do only one conceptual thing and that conceptual thing may be composed of several other conceptual things. So limiting the length of a function to 60 lines helps enforce this practice. Also by limiting a function to 60 lines, most editors will be able to show the entire function all at once without needing to scroll.
When indenting a section of code, it usually signals that an isolated action will take place. By the time there are more than 4 levels of indention, the function is doing too much and needs to be split into multiple functions. (Using an indent width of 8 monospace spaces helps with this.)
Enforcing a line length of 80 characters, encourages each line of code does only one thing. Combined with indenting, the amount of work each line of code can do for each level of indentation gets less and less.
Also, editing code with only 80 characters makes it easy to see code side-by-side, very useful when viewing diff
's.
Y.A.G.N.I.
Design
You Ain't Gonna Need It: Only write code that is needed when it is needed.
Reason
Writing code that might or will be useful in the future is not a good thing. The code may evolve in such a way that what "might have been useful" is no longer needed and when that happens, there is dead-code. Also the code that "will be useful" must be maintained, debugged, and tested, which is wasted effort until that point in the future becomes the present.
If there is functionality that may be useful, make a note of the purpose of the functionality not the implementation. Then when the future arrives, implement the code.
The only code that should exist is code that is executed.
The Power Of 10
Most of the above guidelines are based on an article by Gerard J. Holzmann called The Power of 10: Rules for Developing Safty-Critical Code.