Note: The original version of this article appeared JOOP (Journal of Object-Oriented Programming), vol. 11, no. 1, January 1998, pages 69-71, 76. ©101 Communications, included with permission.
Is object technology ready for the embedded world?
Christopher Creel, TRC
Bertrand Meyer, ISE
Until recently the area of embedded and real-time systems had remained largely untouched by the object wave. Things are quickly changing, although there is still some reluctance to object-oriented ideas in the embedded community. This article will examine the reasons for such reluctance and how to overcome them. It is based in part on a recent Hewlett-Packard project for software embedded in printers, using Embedded Eiffel from Interactive Software Engineering.
Embedded systems are everywhere. Ted Lewis (IEEE Computer, September 1997) cites interesting figures: 2 billion chips and microcontrollers were sold in 1994, versus a miserable 10 million personal computers; the average General Motors car these days has $675 of steel and $2,500 of electronics; electric shavers contain 2 Kbytes of software. Many devices that appear to belong the "hardware" category are in fact largely software products; it is through software, for example, that current laser printers can impress us with amazing performance at reasonable prices. Software is just as essential to space missions, to modern weapon systems, to electronic appliances.
The software in such systems usually started as a small effort, almost an afterthought; but in many cases it has grown to hundreds of thousands of lines. Object technology and Eiffel can help in many ways:
- The productivity and quality benefits of object technology are just as important in the embedded world as elsewhere. The ability to turn out new products faster is particularly important in consumer electronics; the need to improve the "engineering cycles" (the turnaround time in response to market demands), complementing the traditional focus on CPU cycles, was particularly important in HP's decision to look at object technology and Eiffel.
- As in all large developments, embedded or not, the approach provides key techniques of abstraction and modularization. Eiffel goes further than most other approaches by not compromising on the abstraction and information hiding; for example, alone among major O-O languages, Eiffel does not allow global variables, a well-known obstacle to modular development.
- Inheritance, single and (as in Eiffel) multiple, is a key tool in organizing the potential complexity of the abstractions with which embedded developments must deal.
- Reliability is particularly critical in embedded developments, and it must be there at release time. If you find a bug in a payroll program, it's unpleasant but you can fix it and rerun the payroll. If the bug exists in a million shavers or printers, tough luck. Techniques such as static typing, the removal of pointer arithmetic are essential; others, discussed below, include Design by Contract, as introduced by Eiffel, and garbage collection.
The role of C
Part of the reason for the resistance to O-O ideas in the embedded world may be found in its culture, which remains largely focused on machine-level concerns. It is not surprising that the dominant language is C, really a portable assembly language, with its free type conversions (e.g. between pointers and integers) and pointer arithmetic. C++, a more complicated version of C, does not fundamentally affect the situation.
It would be foolish to ignore the presence of C. Eiffel not only recognizes C but takes advantage of it. In fact the wide availability of such a portable low-level language is a godsend to Eiffel compilers by ISE and several other vendors, which use it as their internal target code. This approach to portability avoids some of the problems of Java: no need here to include at run time a virtual machine which kills performance, uses up precious space, and raises tricky licensing issues; just rely on the resident C compiler, adding its optimizations to the Eiffel-specific optimizations of the Eiffel compiler.
The reign of C also requires newer technologies to pay the requisite homage. The previous column (The Component Combinator for Enterprise Applications, JOOP, January 1998) described the many ways in which Eiffel interacts with the rest of the world. The presence of an "external" clause enables Eiffel classes to call out C functions, macros etc.; in the other direction, the CECIL library allows C and C++ to call Eiffel. CECIL is particularly important in an environment where a huge base of installed C software exists and the O-O component is the new kid on the block. In that case it is the C system that must call the new O-O features. This assumes, of course, that it can call the appropriate primitives to initialize the Eiffel run-time system. In such a development the Eiffel part of the software, run-time included, is a static library that the legacy code includes at link time. This is how the HP project introduced its Eiffel components.
Design by Contract
Reliability, as noted, is crucial for an embedded system. A payroll system that crashes produces a GPF; printer software that crashes can jam the printer or damage the engine. Eiffel's Design by Contract can go a long way towards ensuring reliability:
- Writing the contracts in the first place forces the designers to state what they are trying to do. This definitely helps doing it right!
- Contracts serve as a basis for documentation. Thanks to environment tools (the "short" and "flat-short" forms) you can document a class automatically, removing the implementation details but leaving the interface as captured by the contracts.
- Contracts enable developers -- especially the top developers, who typically will soon be called to other projects -- to express the intent behind their designs and hence to leave behind a clear statement of original intent, reducing the risk that further contributors or maintainers will destroy the software's consistency and quality.
- Contracts are a precious tool for quality assurance. With contracts the QA team knows a good part of what it is looking for: any situation, in inter-module communication, where a client may fail to meet its supplier's expectations, or conversely. This is applicable not just to testing (see next) but, before testing, to code reviews and static inspection.
- Contracts are a remarkable testing and debugging tool. Once module authors have taken the trouble to express what they expect and guarantee, the ability to check these conditions at testing time uncovers many bugs. A bug is a discrepancy between what is and what should be; only with Design by Contract do developers actually include the description of What Should Be (the contracts) along with What Is (the implementation).
These are not just theoretical possibilities. In the HP project, a number of bugs were found in the previously existing C code -- simply because, as it was calling the Eiffel mechanisms, it violated some of its contracts! This was an entirely unintended consequence of the use of object technology, and it certainly caught management attention.
Such benefits follow from a peculiar property of Design by Contract: a violation of the "precondition" part of a contract for some software element indicates a bug in the caller, not in the element itself: the caller was told to satisfy a certain condition, and failed to meet it, usually because of some mistake in the caller's own computation. This explains why the presence of contracts on the (contract-full) Eiffel side was able to uncover errors on the (contract-free) C side.
An even more striking case was that of a chip error. A contract of the form
some_feature (x: INTEGER): INTEGER is
x >= 0 and x <= ((2^8)-1)
Result := ... Something ...
Result > 0 and x <= ((2^8)-1)
worked fine on the simulator, but the precondition started to fail on the actual hardware. The problem was a defect in the power computation of the floating-point unit in the processor used for the power computation! The specter of the Pentium bug could be seen for a while.
Systematic application of Design by Contract techniques is one of the best things that could happen to the embedded world.
Embedded software engineers are, for obvious reasons, constantly preoccupied with performance, both for space (RAM as well as ROM) and for time.
Programs usually get burned into ROM. Here there are two feared sources of space penalty:
- Code bloat, especially as a result of relying on reusable libraries. Any good Eiffel compiler will, however, perform automatic dead code removal, eliminating this potential problem.
- The size of run-time libraries. For any language there is some space overhead for run-time libraries, but support for object-oriented mechanisms requires more, in particular garbage collection and exception handling. Embedded Eiffel provides an "A la carte" approach to run-time construction, which enables each customer to pick the appropriate mix of run-time components (I/O or not, signal handling or not...) to fit a global ROM budget. In the HP case the budget was 120 Kbytes, although it is possible to decrease the occupation further.
RAM space obviously depends on how many objects will be created, but the presence of automatic garbage collection dramatically helps reduce space occupation to the strict necessary. HP's budget here was 70K, for sophisticated graphical manipulations.
For time, embedded engineers' concerns typically focus on three areas:
- The impact of garbage collection -- see next.
- Dynamic binding. But again a good compiler will remove overhead by automatically identifying calls that do not need to be dynamically bound. The Eiffel programmer is, however paradoxical this may seem at first, in a better position than the C++ programmer, who must specify explicitly which calls are static and which are dynamic (virtual). Dynamic binding being the correct semantics when it has a different effect, it is natural for a cautious C++ programmer to specify "virtual" even when not needed, preventing optimization. Eiffel programmers do not worry about such things: they expect the correct semantics of dynamic binding in all cases, relying on the compiler to perform the appropriate optimization when it determines the effect to be the same as that of static binding.
- Excessive modularization, causing overhead due to unnecessary calls. Indeed, well-applied object technology tends to introduce many small routines. But there is no reason to take routine calls literally! Here again the Eiffel programmer has an advantage over programmers using other approaches, since the Embedded Eiffel compiler performs inlining automatically, when it is safe and justified.
On all these matters, techniques of Eiffel compilation and run-time, perfected over a twelve-year history, ensure a level of performance that address all or most of the concerns of embedded developments. But remember that object technology, at least in the Eiffel style, is an open approach: if a tight inner loop or other time-critical component requires every cycle and every byte that the hardware can offer, it can always be coded in C or assembly. In today's embedded systems, only a fraction of the code -- the reactive part -- is meant to provide immediate response to some external signal; the rest -- the information processing part is similar to the kind of processing done in non-embedded systems, and should use the best software engineering techniques available.
Good O-O environments come with automatic garbage collection, to reclaim the space of any objects that have outlived their usefulness to the software. (Java may be viewed as a version of C++ that makes garbage collection possible, but at the expense of several other crucial facilities, and without completely removing the rest of the C heritage which is so adverse to object technology.)
Although embedded engineers often think first of garbage collection as a potential performance burden, the first effect to be noted is actually a beneficial effect on space performance. Manual storage management a` [PREVIOUS LETTER IS LOWER-CASE "A" WITH GRAVE ACCENT] la C/C++ is so difficult and error-prone that one typically ends up not reclaiming much of the unused space. In an embedded context with tight RAM constraints this is catastrophic. A garbage collector will do much better, guaranteeing that all such space will be returned for new objects. This opens new horizons to embedded systems, enabling them to use sophisticated algorithms (say, for graphical output) that need to create so many objects that developers would never even contemplate them under traditional approaches.
The time effect of automatic garbage collection also needs to be considered. Overall, GC is the clear winner: manually freeing space does not come for free, and as in two of the other areas considered above, inlining and static binding, computers do a much better job than humans. The remaining concern is the potential unpredictability of garbage collection cycles, which in a real-time environment can conflict with the need to react in a guaranteed time to external events, say the detection of an incoming missile. But this is not necessarily as untractable an issue as is sometimes thought. Several factors alleviate it:
- The Eiffel garbage collector can be turned on or off dynamically through library calls (collection_on, collection_off). So a critical section that creates some with hard real-time response requirements -- part of what was called above the reactive part -- can turn off GC.
- Good modern GCs are incremental: they don't stop everything until done collecting, but instead they wake up periodically, collect some objects, and go dormant again. So the potential interruptions are short, and in fact most of the time (in ISE Eiffel) invisible to humans, although they may still be too long for hard real-time constraints, say at the millisecond level.
- The normal criterion for awakening the GC is object allocation. As long as you don't allocate any object, you won't trigger the GC. Note that object creation, like any other non-trivial operating system call (say I/O), is unpredictable by nature, so a critical section should probably not do any if these concerns are essential.
- There still remains, of course, the case of an event requiring immediate attention but happening when the software is engaged in some (seemingly) non-critical activity -- the information processing part -- which has no reason to disable the GC and so may engage in a GC cycle at the wrong time. This is where multithreading comes to the rescue, as discussed next.
All this indicates that embedded software engineers should consider garbage collection as an ally, not an enemy. Embedded Eiffel provides detailed tracing mechanisms (the FOOTPRINT utility) to study the behavior of garbage collection, and a number of library facilities to tune its properties -- size of chunks allowed both initially and later, time-space tradeoffs -- to obtain the ideal mode of operation.
Many embedded and real-time systems can benefit from multithreading to perform several actions at once. The EiffelThreads library provides an extensive set of portable mechanisms to take advantage of the full threading facilities of the underlying operating systems; it currently supports in a source-compatible form the threading models of Solaris, POSIX, Windows and VxWorks. Although multithreading is also available in Java, the EiffelThreads model is more abstract, freeing the user from having to worry about low level details.
There is another unique property to EiffelThreads. How does multithreading combine with garbage collection? Most of the environments we have seen have a single garbage collector for all threads. This means that whenever a thread needs memory, all threads must stop for the GC to go into action. This can be extremely detrimental to performance; in as open an environment as Eiffel, the consequences could in fact be catastrophic: assume a thread has called out some external (C or C++) routine: until it has returned from that external routine, over which we have no control, we cannot do anything, so all threads have to wait!
For flexibility and performance, Embedded Eiffel assigns a separate garbage collector to each thread. (The GC's code is of course reentrant and shared; only the data structures are thread-specific.) So each thread can have its own pattern of object creation and reclamation, without bothering the others. Each thread can also take advantage separately, according to its needs, of the GC-tuning facilities mentioned above.
This property also has a crucial consequence on the problem of real-time response predictability discussed above. You can reserve a thread for the critical reactive sessions, possibly disabling GC in that thread, although not in the information-processing threads. Then to guarantee response time properties, it suffices to use an operating system whose multithreading mechanism provides a fair, guaranteed share of the CPU time to every thread.
Embedded systems raise specific debugging problems, since they must run on a host environment different from the target environment, and that target environment usually has limited resources. Today's sophisticated host-target development systems, such as VxWorks, provide cross-debugging facilities, but they are usually targeted to C. Since Embedded Eiffel generates C code, and includes references to the original Eiffel line numbers in that C code, it is possible to use such debuggers in connection with the Eiffel source. This of course comes in addition with the sophisticated visual Eiffel debugger of EiffelBench (which in a recent project prompted one user, only half-jokingly, to write that he had removed what he thought was the last bug, and felt depressed about it, because the debugger was so much fun to play with!).
Having a portable development environment gives much more freedom to developers. It is in particular not uncommon for engineers to enjoy continuing their work on their PC at home or, for that matter, in a ski cabin somewhere in the Sierra Nevada. The full source compatibility of EiffelBench across Unix (e.g. HP-UX), Windows NT/95, VxWorks etc. makes this possible: when you come back on Monday morning you just load the changes and click "Quick Melt" to recompile on your workstation without a glitch. After all, the compiler itself is in Eiffel, benefits from the same portability technology, and it's the same compiler on every platform.
The traditional resistance of embedded developers to O-O ideas has until now had some justifications. This article has shown, we hope, that the time for hesitations was yesterday, and that the technology is ripe for the embedded world to join the rest of the industry in benefitting from the best that object technology has to offer.
Christopher Creel is a software engineer at the Technical Resource Connection (Perot Systems). This article is based on work done while he was senior architect at Hewlett-Packard's Color Laserjet and Consumables Division in Boise, Idaho.
Bertrand Meyer, editor of the Eiffel column, is president of Interactive Software Engineering and author, among other books, of Object-Oriented Software Construction, (second edition, Prentice Hall, 1997). More information about Embedded Eiffel can be found at http://www.eiffel.com.
Additional information on the topics of this article appears in Eiffel for embedded systems at Hewlett-Packard (an interview of Christopher Creel).