Writing bug-free code is something we all strive for, but its almost impossible to meet this lofty goal with nontrivial programs. There are many different tools and techniques that can help to get us a lot closer to this goal [and more quickly] – one should use a combination of these because they are complementary, each one strengthening our code from a different angle. The technique I am going to cover is Design by Contract (DbC).
The philosophy of DbC is that a unit of code (a function) needs to do what it was specified to do correctly so long as it was called in a way that was agreed on – i.e. a contract. In DbC, these contracts need to be rigidly enforced.
The most common way of enforcing these contracts is to invoke macros from strategic points in your code. These macros most commonly include:
I will not be covering the low-level implementation of how DbC, this topic is already covered well in other articles – please refer to the links at the end of this article.
Although there have been many articles on this subject, many of which touch on the subject of best practices, few focused on this in the context of embedded systems. I will be covering this subject based on my experience, where C is the predominant programming language. Many of the ideas I mention can also be applied to other programming languages and bigger systems.
- Write modular code. Each module should have public functions (published in the module’s header file), and private (static) functions. This is a commonly followed general programming best practice.
- To ensure that the caller is using your code correctly it is best to place most of your DbC precondition assertions inside your public functions (interface code). This is because the task of ensuring private functions behave as designed should already be very effectively covered by Unit Tests. I am a firm subscriber of the DRY (don’t repeat yourself) principle. Placing most DbC checks in public functions ensures that code that may not have been effectively unit tested (you may not have control over that code) behaves correctly.
- DbC improves code readability and is most intuitive to use “ASSERT” for precondition tests, and “ENSURE” for postcondition tests [even though they both do the same thing].
- Use “ALLEGE” when calling a function and you also want to make sure it has performed its expected goal (i.e returning a positive result). The code within the parentheses of an ALLEGE statement will never be compiled out by selecting to compile DbC code out [by using NASSERT].
- One should be confident enough to compile DbC code in for production code (not using NASSERT). If a contract is not met and your DbC code has been compiled out, the code is almost guaranteed to misbehave in ways that are often a lot more difficult to debug, and sometimes a lot more severe.
- A DbC violation should result in the program terminating (the system resetting if it’s a small standalone embedded system).
- Always log a DbC violation with the location in the code it occurred. It is also of huge value to log enough detail for you to understand what occurred and the context in which it occurred.
- For small embedded systems DbC violations can result in a rapid start-reset cycle, and if you don’t want to risk this, you may want to try some sort of a softer approach to these conditions. The following is an example:
- Always log the fact that a contract has been missed.
- The first time a contract is missed, reset the system (perhaps it has got itself into an unusual state and resetting will cure it),
- Treat a missed contract in a decreasingly severe ways if repeated. Consider some of the following options (note the above can add complexity and can be tricky to implement):
- Disable the part of the code where the issue occurred. This could be done with special program logic, or possibly by disabling the thread or service.
- Allow the system to continue (note this is very likely to have other undesired effects, but for a noncritical bug it may not always be noticed or have a destabilizing effect).
- After the first reset, start a timer (e.g. 1 hour) and after that has elapsed, allow a further reset.
- For non-critical portions of code, consider only logging that a contract has been missed (and not resetting). The user may hardly be aware of an issue, and you can address it more painlessly after viewing the logs.
- For more graceful rebooting of a system, a strategy of handling a DbC violation where a memory leak has occurred would be to have some memory pre-allocated. Then before logging the violation and resetting, free that memory, and signal that a reboot should occur. Allow the other modules and threads to shut down in an orderly fashion once they receive that signal.
- If running the code in a debugger, use instructions (often the assembler “BRK” instruction) to stop the debugger. This allows one to more clearly observe the context in which the violation has occurred.
- Using DbC often restricts the range of parameters a user can provide in a module. Nave a way for the user to inspect what that range is. An example of this could be a maximum queue size. An exposed const or define in the header file (or generally even better – a function call to query this) would be helpful to avoid a DbC violation.
Shouldn’t doEmbed from Getty Images
- Asserts can add a lot of clutter to code if overdone and if done in redundant places.
- If a piece of code is wrapped by another piece of code, testing the contracts at both places is wasteful and redundant and can make your code less maintainable.
- If a parameter is guaranteed to have been tested by the caller, do not retest it.
- Never use DbC as a convenient way of stopping your debugger when expected program conditions are met. Rather use breakpoints, or create other macros that are guaranteed to be compiled out when not attached to a debugger.
- Never use Asserts for catching unexpected behaviour from hardware, external software modules, or user inputs.
BenefitsEmbed from Getty Images
- It has been proven that the earlier a bug is caught and addressed, the cheaper it is (by a massive factor) to remove. DbC will facilitate this.
- DbC can do what unit tests cannot do in a bench testing or field trial environment.
- DbC will often make unit tests simpler to write by restricting the range of inputs a module needs to cater for.
- Restricting the range of inputs will often simplify your code. You do not need to cater as much for people doing things out of the ordinary and don’t need to add many lines of defensive code. Simpler code is more reliable code.
- If used intelligently, DbC also helps to make your code more self-documenting.
- DbC works well with unit tests. One problem with bench and field trials is that it is difficult to measure code coverage. However, when using unit tests, you can more easily measure your code coverage and thereby know which DbC tests have been exercised.
ConclusionEmbed from Getty Images
This is by no means a conclusive list of best practices, and much of it is based on what I have experienced working with it. If you have any further ideas or observations on the subject, I would be interested in hearing from you.