Everything ends with code quality
During 2020, we have run a series of articles on the code quality theme. The series has shown that quality, and specifically code quality, is a broad topic that can (and maybe should) be attacked from different angles. In this last installment of the series, we will try to summarize and highlight some of the most important topics.
What is code quality and why all the fuzz?
If you have read “Zen and the Art of Motorcycle Maintenance” by Robert M. Pirsig, you know that spending a lot of effort on trying to define quality can have some quite dramatic effects on your life. So we will tread carefully from here.
There is not one single definition of code quality that can be applied in all situations, but rather a spectrum of different metrics and suggestions as to what constitutes good code quality. Some of these are:
- A low number of defects. Well, that one is sort of obvious on the surface, but what does it really mean and how do you measure it? There are of course ways to statistically measure this by for example counting discovered defects over time, but it is a rather subjective measure in that it really says nothing about the circumstances and contexts for defects.
- The code is maintainable, which implies readable and possible to understand with reasonable effort. This is also a very subjective matter, and the chance that two or more software developers would have the same definition is very close to zero.
- The code is reusable in whole or in parts by other software projects. Once again, a difficult thing to measure, as there is no objective way of measuring how easy something is to reuse.
- The code is safe and secure. You guessed it, a rather difficult concept to pin down. We can use various statistical measures like mean time between failures, or number of security defects discovered over time. However, that will only tell us if we get better or worse as time passes and give us a rough idea of where we are right now.
- Code complexity – there are different ways of measuring the perceived complexity of a code base, and as complexity probably affects all the other measures, it can be argued that code complexity is important to keep as low as possible. But some of the metrics that are used for complexity will sometimes award rewriting code in ways that might decrease the metric but at the same time also decrease readability and maintainability, which was probably not what we intended.
One common theme for the above examples are that you can only measure these qualities after the fact. Such measuring can be important to measure progress, but it does not really help you with how to make actual progress on any of those scores. Another takeaway is that while it might be hard to define good quality, it is often quite easy to recognize not-so-good quality when you are exposed to it.
Software development for embedded devices shares many of the same challenges as other software development. But on top of that we are often dealing with devices that carry out a mission critical function, are deployed in hostile environments or in very remote locations, or are supposed to run forever on a coin cell battery, or some other challenging deployment scenario that rarely happens in the desktop or server software market.
As Shawn Prestridge noted in his Move fast and break things? Not so fast in embedded article, embedded software development does not necessarily go very well together with some of the recent trends in software engineering. Some decisions you will have to live with for a very long time, so nailing down some basic rules early on in your project will probably help you deliver software with better quality, faster and with fewer uncertainties.
Throughout this series, there is one thing though that we constantly get back to: up-front preparation can go a long way in increasing code quality, safety and security (no matter how we define these terms) and decrease the number of sleepless nights.
Start with Security
If you are starting out today with a completely new product idea in mind, or are building incrementally on top of an already existing design, there is a very high probability that you will add some kind of connectivity to the device. It does not really matter if it is WiFi, Bluetooth, ZigBee or even a wired connection, just think about the diverse set of CAN bus hacks in automotive over the last few years. As soon as you have made that decision, you have made your future self-vulnerable to hacks, cracks, and compromised device integrity. To top it off, you are also exposed to regional legislation that is coming into effect around the globe concerning connected IoT devices. The focus of such legislation is right now on the protection and integrity of things like sensitive personal data, but also by way of ensuring software integrity, on stopping cascading attacks that can hop from one vulnerable device to another.
Security-related decisions you make at the beginning of your project will have impact all the way through production and deployment and the whole life cycle of the product. Moreover, changing a security related decision late in the project could be very expensive or close to impossible. Therefore, even if you make a conscious decision to ignore security for now, you should make sure that you do not paint yourself into a corner – just in case you have to go back and revisit that decision at a later stage. Clive Watts did a thorough walk-through of things to consider in his article How to cover the best security practices by design.
In this series, we have been talking extensively about static analysis and runtime analysis in different contexts. In the embedded world we are mainly writing our software in C and C++, but it can be argued that these languages as is are not very well suited for writing reliable systems. They have a huge amount of undefined behaviors and implementation defined behaviors, which means that code that works for one arbitrary tools/hardware combination might fail for another one. The sheer amount of such murky corners in the languages are almost impossible to remember even for seasoned programmers. Further, a simple typo can transform your code into something that still compiles, but behaves very differently from your intentions.
If you work with functional safety requirements, you already know that it is highly recommended to eliminate the problematic language constructs by following a dedicated coding standard like MISRA. But we would like to argue that any software development for embedded systems will benefit from using clearly define subsets of the language, to remove defects that are just a consequence of strange and arcane language rules. The MISRA set of guidelines is an excellent starting point, but for connected systems, you should also consider CERT C. The CWE is also a good resource. Quite a few of the embedded-related CWEs are covered by rules in MISRA or CERT, but it will not hurt to read up on the CWE effort in general.
And given that we have so far in this text talked about code quality as having a quite elusive, well, quality, automated code analysis is something that will help you from day one to improve code quality by highlighting troublesome code constructs immediately. By acting on the analysis results, you will improve the quality in a very concrete manner by removing problems today, instead of being hit by them in the future.
Applying language subset standards like MISRA in combination with your own coding standard for things not covered by the language subset will also help with consistency. This include things like naming conventions, how to organize functionality into modules, usage rules for globally accessible variables and data structures, how and when to use the volatile keyword etc. Having a clear set of rules and preferably some tool support to uphold the rules will help tremendously in improving code quality
Use it or lose it
If you are looking at increasing code quality and at the same time increase your overall productivity, using the right tools are key. The downside is that for some functionality or process improvement, you need new tools, but the upside is that you might already have the tools available. It might even be so that a manager with foresight has already provided your organization with analysis tool etc.
Reading up on the tools available in your development environment might a very good investment if you find features and functions you were not aware of previously. A great way to start that journey is with the debug environment – judging from user feedback, it is a fairly high probability that you will find features in your debugger that you were not aware of. There is this saying that goes like “when all else fails, read the manual” that is actually rather accurate. For most of the time, you can stay comfortably within the tool functions and features that you know and use often. But when that thorny problem hits you, it will pay off to have read the manual in advance.
Tying up the loose ends
If you have read previous articles in this series, we hope that you have picked up something new when it comes to attacking code quality. As we have found, defining in no uncertain terms exactly what constitutes code quality is very difficult. But we have on the other hand given you a number of tools and ideas on how to minimize writing code of bad quality.
Another key point is that by focusing on code quality, you will more or less automatically also make your code safer, more secure, more reliable and easier to maintain. That are some really nice side effects, if you ask me! :) So starting with code quality also ends with code quality.
Written by Anders Holmberg, General Manager Embedded Development Tools, IAR Systems