I’ve talked about Ada a couple of times here before, and how I think it might be a very sensible replacement for the C family of programming languages in the specific area of OS development, where compatibility with existing code libraries is less of a critical criteria, and code correctness and portability becomes a lot more important. Today, I’d like to discuss how this language meets the criteria which I’ve set before on a good programming language for OSdeving.
Because of its focus on embedded devices, critical systems, real-time operation, and not copying the worst ideas of C for compatibility, Ada belongs to the niche of domain-specific languages. In this niche, it is reasonably popular, though nothing like C or C++. The consequence of this is that Ada implementations are quite solid overall, but perceivably less hardened than your typical C/C++ compilers. When I try to do some weird things in my code, I do trigger the occasional GNAT bug, which results in a legal program not being compiled.
An Ada compiler will take a few years to correctly support latest standard versions, but the situation here is still much better than the C or Fortran situations, where standards from a decade years ago are still not properly supported in the wild. As of 2014, decent implementations of the Ada 2012 standard are already available. User documentation, in the form of tutorials and developer communities, is also reasonably available, though you’ll have to hunt it a little bit.
Specifications and implementation licensing
This is an area where the Ada community really shines. The documentation of both Ada as a programming language, and GNAT, its main FOSS implementation, is top-notch. Language specifications are designed in a community process by the Ada Rapporteur Group, and published in a well organized HTML document, along with a rationale for each new version that describes the highlights of the new language and why the new features are useful.
GNAT, on its side, is not just a bare bones compiler. It comes with a very extensive development toolchain, including such things as a unit test toolkit, a cousin of Make with better support for the language’s specific features, code statistics computations, an IDE, and coverage testing. You can also get, along with it, a range of useful libraries which won’t be that much interesting to OS developers, but may be of interest for client-side devs, such as a GTK port and an XML parsing library.
And the great thing ? Everything is documented. Well documented. Down to that information about how to build a runtime for a new architecture that all OSdevers in any language have always been looking for, but unable to find, except for a range of user-written tutorials that will often make more than a couple dangerous assumption about how a specific toolchain is going to continue behaving in the future.
Nothing to worry about on this front, either. Since its 95 release, Ada has gone through two major revisions while remaining remarkably compatible with itself, as it introduced such syntaxic novelties as containers, iterator-based for loops, advanced multitasking features, and support for contract-based programming.
If we push the requirement of compatibility as far as to require compatibility with existing code (which, for OSdeving, I probably wouldn’t), the standard offers provisions for interfacing Ada code with other languages such as C or Fortran.
Since it comes from the embedded world, Ada has very good support for hardware oddities like bitfields, arbitrary RAM access, and precisely specified data structuring. Things like integer size, data alignment, packing, and padding, may be precisely specified by a developer. There is no “helpful” hidden feature like automatic garbage collection waiting for you in the corner if you forget to turn it off in your critical code regions.
In fact, as Ada was designed with real-time systems in mind, it can be very predictable if you avoid “dangerous” features which are explicitly marked as such in the spec. Some parts of the standard libraries, including standard containers, are even available in several versions, each making differing compromises about memory usage and run-time predictability.
Like C++, Ada follows a runtime philosophy of “if you don’t use it, it shouldn’t cost you anything, and you don’t need to implement it”. The basic “zero footprint” Ada runtime is very lean, while still allowing for much more high-level and expressive coding than C at equivalent performance.
To control which runtime features you want to enable, instead of having you fiddle with nonstandard compiler switches, Ada provides restrictions as a standard way to disable language features that you can’t support in a specific code section. Many restrictions are described in the standard, allowing one to disable such problematic features as multitasking and memory allocation when they are unwanted.
Ada also has good support for hardware interfacing primitives like CPU timestamp counters and interrupt handlers, provided that you implement support for it in your OS’ runtime first.
Abstraction and modularity
For abstraction and codebase organization, Ada provides everything one would expect from a modern programming language, and then some more.
- A strong type system (and by that, I mean that you can even create incompatible numerical types for such purposes as dimensional analysis in physics)
- Powerful package organization mechanisms, with interface/implementation separation, public and private parts, and “child” packages which have access to their parent’s data.
- Generics. One of the first languages to provide them back in the day, actually.
- Contract-based programming with function preconditions, postconditions, and invariants.
- Containers & iterators, with transparent for loop syntaxes.
- High-level array manipulation, including slicing (extracting part of an array and doing something with it) and first-class array litterals.
- Exceptions. Because although clunky, these remain the best standard error handling primitive designed to date.
- Object-oriented programming with interfaces and operator overloading.
- Standard tasking support, with protected objects, various scheduling policies, and asynchronous transfers of control.
- And, needless to say, strings with support for non-ASCII characters. Duh.
Safe even after 2am
It’s sad to spend hours debugging a piece of code, only to find out that the problem would come from a trailing semicolon after a conditional instruction, or filling a variable of an enum type with values of another enum type. Ada has been very carefully designed to avoid this kind of common, “silly” programmer mistakes, which actually originate in poor programming language designs.
Examples of things which the language provides are well-specified scalar types, such as integers with a known maximum and minimum value, and bound checking triggering exceptions when one tries to fill them with incorrect data. Or floating-point numbers with a precisely specified amount of significant digits. All kinds of bound checks are enabled by default during debugging, and may be disabled in production code where performance is a critical concern. And the aforementioned high-level syntax helps clearly specifying a program’s intent in code, so that its correctness may be checked automatically rather than solely by a “hopefully large enough” set of unit tests.
For those who want even more guarantees, there is a dialect (near-subset, in fact) of Ada called SPARK, which can be easily mixed with mainline Ada (as long as your implementation supports both). Though it comes with important restrictions as compared to Ada, such as forbidding dynamic memory allocation, what it offers in return is an even more predictable behavior, that lends itself well to formal analysis. SPARK tooling can prove such desirable code properties as the absence of run-time errors or memory side effects in a specific codebase section. The compromise may be worth it in code sections that must offer critical security or reliability guarantees.
If I start getting my hands dirty at implementation again, I’ll probably end up trying to switch from C/++ to Ada while the codebase is still small enough to allow for such a rewrite. To evaluate the feasibility of this, in the current TOSP codebase, there is a “bootstrap” component, currently written in Assembly and C, which is in charge of setting up the machine’s MMU and turning the CPU in 64-bit (“long”) mode. I think rewriting it in Ada and SPARK would be a perfect candidate for evaluating the viability of these in OSdeving contexts, as it does some serious bit fiddling, runs under minimal runtime conditions, has to be started by a bootloader (with all that this implies in terms of precise executable layout), and needs to communicate precisely structured data to the kernel at the end.