I love C. I started using it on DEC PDPs and Intel 8080s ages ago. It has long since replaced assembler and, along with C++, remains the programming language of choice in the embedded tool space according to most surveys.
My recent article, “Parallel Processing Zooms While Debugging Zags” (www.electronicdesign.com, ED Online 20428), generated a few e-mails on parallel programming. I also received a couple of responses to a short comment I made that C was unfortunately the primary programming language for the embedded space.
“I don’t understand why people today keep insisting C is an inferior programming environment. Certainly, C is ‘closer to the machine’ than many others, and that means you have to pay more attention to the details. But contrary to popular belief, easier isn’t the same as better. It is C’s closeness to the machine that recommends it in the first place,” wrote Bruce Koerner.
“A well crafted C program will be faster and smaller and no less reliable than one written in a modern language. The advantage of the modern languages is that it is easier to craft a program that won’t crash the system, but they do it at a premium. It is not a matter of one being better than the other. It is a tradeoff,” Bruce added.
“For applications that must be fast or compact, there is nothing like a highly skilled programmer writing in C (unless you want to go to assembly code, which is machine-specific and far harder to use). For bulk programming, where size and speed don’t matter and logic errors are immediately obvious, you can use the modern stuff,” he concluded.
While I do stand by my comment, I also understand Bruce’s view. C has many advantages. In some instances, “closer to the machine” is one of those advantages, especially in embedded environments. Yet it can be a disadvantage when it comes to portability. Differences in target architectures tend to cause problems in porting applications that are too close to the machine.
Likewise, C pointers are another advantage because they tend to map to the hardware well, though their use is also prone to errors. Errant and null pointers are common problems, as are buffer overflows. Both are related to C’s open addressing model, where bounds aren’t checked. This allows C to be used to write operating systems as well as applications.
Well-crafted programs can take advantage of C’s efficiencies while avoiding these and other problems that can arise. Unfortunately, creating a well-crafted program takes an experienced programmer. Even then, errors can creep into applications. These can be hard to find and, as any virus writer will tell you, easier to exploit than you might think.
C can be used to write robust, safe, and reliable applications, and there are plenty of examples of wellcrafted programs. The problem is that C isn’t inherently robust, safe, or reliable, nor does it incorporate features that make this possible.
C and C++ programmers can do some things to create applications that are robust, safe, and reliable, starting with static analysis tools. Most C programmers are familiar with Lint. Lint and Lint-like tools flag suspicious code. This type of support is often available within the compiler. The MISRA (Motor Industry Software Reliability) C guidelines are found in many embedded C/C++ compilers.
Static analysis is often overlooked because of the overhead. But the advent of fast, multicore machines with lots of hard-disk space as well as network clusters for distributed make support means these tools should be in every C/C++ developer’s toolkit.
A change is in the air as well for runtime support. For example, standard runtime libraries that used functions with pointer parameters to a buffer are now being delivered with an additional buffer size parameter. These retrofits perform the same type of range checking that other languages such as Ada and Java perform inherently.
One problem with this approach is that programmers need to take advantage of the retrofits and use them properly, including correct usage of the buffer size parameter. Also, it prevents the compiler from optimizing range checking. For example, if function A performs a range check and calls function B with the same parameter, then function B doesn’t necessarily need to perform the same check again.
There was even talk of including garbage collection in the C0X and C++0X standards. Various garbage collection implementations are available, but nothing is in general use. Unfortunately, integration is often the issue when using this support.
I’ll have to leave the discussion of C’s speed and compactness versus the competition to another time. C is still the benchmark to meet but one that safer competitors like Java can meet. In the meantime, I’ll continue to use the C/C++ compilers because they are often the only alternative available for many development platforms.