The majority of embedded developers are working with at least two operating systems. We’ll generally be using a target OS, possibly an off the shelf RTOS, but often an in house kernel or similar, while our development machine will likely be running a different OS – be that Windows, Linux or some other. Even when I’ve used Linux for both development and target OS I’ve still had to cross-compile for the target due to it having a different processor or kernel version.
We have always been more productive and less frustrated when we’re able to execute, test and debug the majority of our code directly on our development machines. Yes, on-target development tools with automatic image download and full debugger support do make the task of working on the target platform much easier, but working on the host is generally quicker still. Being able to debug the same code on different OS is sometimes invaluable in tracking down hard to catch defects, as the debuggers on each are likely to have different strengths and weaknesses.
I’ve also never felt comfortable locking in to a particular OS vendor. Commercial concerns aside, it’s always possible to imagine a scenario where execution on an alternate OS is needed. For example, if we have a graphical user interface then perhaps a demonstration or training app is needed on a PC; if we have intellectual property in our algorithms, the business may decide to license these to a partner with a different OS; or it may be that we simply want to be able to choose the best evolution path for the codebase without having our hands tied technically with respect to the operating system.
To address these concerns we’ve always developed a OS abstraction layer (OSAL); an in-house code library that presents a common interface for all operating systems and can be compiled and built against any of the target OS. All other production code is prohibited from accessing the OS directly; rather it must use the OSAL wrapper instead.
Why do I think this is important in an agile context? In agile we give particular value to working software, and so it is important to be able to show that our software is working at all stages of the development process. We’d like to be able to execute tests prior to the target hardware being available; we can sometimes run the target OS in a VM or an emulator but this can execute much slower and we can’t always rely on this. We’d like all developers to be able to confirm easily that they haven’t broken any tests before they check in, which means we’d like a fully automated regression test that runs quickly on the fastest processors we have, which (I hope!) is our development machines. And we’d like to be able to run tests within a continuous integration server that executes all of our tests whenever code changes, using a tool like Jenkins whose server won’t necessarily be running either our target or development OS.
For Unit testing an OSAL becomes very valuable, because circumstances that can be very hard to test in full testing (expiration of a timer after a long period, error conditions on a hardware interface, etc.) can easily be created by providing mock versions of the OSAL services that can be controlled from within the tests. For instance a mock timer service would support the normal OSAL API but also allow the time to be reset to arbitrary times and the clock to be incremented as desired, easily allowing for time based scenarios to be Unit tested in accelerated time.
I’d also advocate developing an OSAL wrapper in an agile “as needed” manner. It’s rare that the full services of an operating system are required, and the OSAL need only support those that the application needs. We would be very cautious against standardising on POSIX or some other portable OS standard; we’ll likely spend lots of time writing OS services or modes we’ll never need. Start simple and implement the basics:- we’ll typically find that we need very common services like Threads, Locks, Timers and File System services early on, and may then add Networking and hardware interface support (Serial, USB, etc.) later as they become needed. And in a similar vein the OSAL API need not expose the full functionality of the underlying OS, only that which is required. Where some specific functionality is only available on one OS, avoid using that feature at all if you possibly can, as that’s another flavour of OS lock-in. Where the usage of these services varies significantly between your target OSs the OSAL wrapper will need to adapt to these from a common interface.
An OSAL wrapper that supports multiple OS will clearly need different implementations for each target OS. This can be done by use of #ifdef statements, or conditionally compiling OS specific files based on the target. Common header files defining the API to the wider application are essential, being careful to make sure that they contain no OS specific code or #ifdef statements. Separate header files for each category of OS services are recommended, as we don’t want application code to be dependent on all OS services, and we might like to mock or remove a particular service while keeping others. And of course, if you find that you have OS-specific compilation in code outside the OSAL, then something has gone wrong: that logic needs to be moved into the OSAL wrapper.
So should abstraction wrappers be developed for all third party tools? Not necessarily, for instance I’d suggest that nothing is gained from abstracting standard C/C++ header files supported on all target systems. We prefer to use MinGW and gcc on Windows so that the standard header files are common to our target environments. There may be value in abstracting libraries, for example an XML handling library, but only if there is a possibility of a swap to an alternate implementation. Graphical frameworks, such as Qt, would be very challenging to abstract with arguable gain. In these instances a preferred approach would be to limit exposure by restricting usage of the framework to a particular tier, such as the presentation or persistence layer.
To summarise, managing an operating system dependency behind an OS abstraction layer allows for easier code re-use in other target environments and hence target flexibility. In our experience an OSAL wrapper that supports your development systems as well as the target systems significantly improves developer productivity.