Giving Ada a Chance
Much of the technical material presented in this article is available as part of my contributions to osdev.org
A Chance Collision #
By no deliberate design of my own, I happen to live close to a university. Not in the kind of 'University town' common to much of Europe or the United States, but in the densely packed suburban sprawl of Sydney's inner-city. Making my way home one serendipitous afternoon, I happened across a sizeable stack of books sitting on the curb outside the aforementioned University's engineering buildings. The university was in the process of liquidating its stockpile of old engineering books, and had left them in a pile for the local council to collect. Sifting through the pile for anything interesting, two books in caught my eye: Building Parallel, Embedded, and Real-Time Applications with Ada, and Concurrent and Real-Time Programming in Ada.
I'd heard of Ada before. I understood that it came from a pedigree of languages developed for the United States military, and that it still occupied a niche in the development of safety-critical applications, but that was about all I knew. Curious, I threw the books in my bag and off I went.
Exceeding My Expectations #
Admittedly, I had pictured Ada's syntax resembling the uncompromising verbosity and rigid construction of COBOL, or perhaps the Lovecraftian hieroglyphics of APL's various eldritch incarnations. Turning the pages, I was pleasantly surprised to see modern constructs associated with modern high-level languages such as ranges, slicing, and exception-handling. The syntax —admittedly verbose by modern standards1— seemed carefully constructed to make the language comprehensible at a glance.
The fact that Ada was designed with embedded software in mind had me interested. I already had some limited experience with bare-metal development on the x86 and ARM platforms using C and assembly, so the prospect of using higher-level constructs on bare-metal had me intrigued.
Not the Camel You Expected #
A common criticism of Ada is that it's a language 'designed by committee', or even worse, a language 'designed by committee for the military'2. The implication being that this would preclude it from having any kind of real-world practicality3. I'll spare readers a detailed retelling of Ada's conception within the Department of Defense's 'High Order Language Working Group' (HOLWG), save to say that Ada was born of the need for single, unified higher-level language suitable for use in the multitude of real-time embedded systems developed by the DoD4. In the wise words of the working-group's chair, Colonel William A. Whitaker: 'It was concluded that no existing language could be adopted as a single common high order language for the DoD, but that a single language meeting essentially all the requirements was both feasible and desirable'. If such a thing was indeed feasible, the DoD's deep pockets would help it bring it into existence. Bell Labs were invited to evaluate the C programming language against the DoD requirements, however (In the words of the HOLWG) '...they said that there was no chance of C meeting the requirements of readability, safety, etc.' (Whitaker, 1993).
In what would prove a controversial decision, the DoD would go so far as to mandate the use of Ada for all in-house software engineering.
So What Makes It Special? #
Ada has many useful features that are of particular interest for low-level programming. I'll go over a few of my favourite features below.
Custom Types
In addition to being a strongly typed language, Ada allows for the definition of new scalar, enumerated, and record types (the equivalent of structures in C). Custom primitive types can also be constrained to a predefined range of values.
The example below demonstrates the definition of a new integer type based upon Ada's native Natural
type,
restricted to a predefined range.
The use of the subtype directive informs the compiler that other variables of the Natural
type are compatible with the newly defined subtype.
VGA_COL_COUNT : constant := 80; VGA_ROW_COUNT : constant := 24; subtype Col is Natural range 0 .. VGA_COL_COUNT - 1; subtype Row is Natural range 0 .. VGA_ROW_COUNT - 1;
The below example illustrates the creation of incompatible custom integer types. While their base type and range constraints are identical, Ada treats both as separate, incompatible types. An assignment of a variable of one type to the value of another is illegal, and will trigger a compile-time error.
type Integer_1 is range 1 .. 10; type Integer_2 is range 1 .. 10; A : Integer_1 := 8; B : Integer_2 := A; -- illegal!
The following example demonstrates the creation of a custom enumerated type. It also demonstrates a subtype of an enumerated type with a constrained range of values.
type Day_Of_Week is (Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday); subtype Work_Day is Day_Of_Week range Monday .. Friday;
A variable with the type of Work_Day
is restricted to its constrained range.
Any attempt to assign a value outside this range to a variable of this type will raise a Constraint_Error
exception at runtime.
Representation Clauses
One feature that particularly impressed me is Ada's representation clauses. They provide a construct for defining the in-memory representation of a type. I could immediately see how I could use this feature in my own long-suffering operating system projects.
The following example demonstrates the definition of a record type, as well as its associated representation in memory.
---------------------------------------------------------------------------- -- The format of the System Table Descriptor pointer used by the processor -- to load descriptor tables like the GDT and IDT. ---------------------------------------------------------------------------- type System_Table_Descriptor is record Size : Unsigned_16; Offset : System.Address; end record with Size => 48; for System_Table_Descriptor use record Size at 0 range 0 .. 15; Offset at 0 range 16 .. 47; end record;
The Size
aspect specifier instructs the compiler that the System_Table_Descriptor
type must be 48 bits in size.
The record representation clause instructs the compiler as to the required layout of this record type in memory.
This example specifies that the Size
member should occupy bits 0 to 15,
and the Offset
member should occupy bits 16 to 47.
This feature is analogous to C's bit-fields.
The following example demonstrates defining the in-memory representation of an enumerated type.
---------------------------------------------------------------------------- -- The privilege level for a particular descriptor. -- These correspond to the 'protection ring' that this descriptor is -- accessible from. ---------------------------------------------------------------------------- type Descriptor_Privilege_Level is ( Ring_0, Ring_1, Ring_2, Ring_3 ) with Size => 2; for Descriptor_Privilege_Level use ( Ring_0 => 0, Ring_1 => 1, Ring_2 => 2, Ring_3 => 3 );
The Size
aspect specifier instructs the compiler that the Descriptor_Privilege_Level
type must be 2 bits in size.
The representation clause instructs the compiler as to required representation of each possible value of the enumerated type in memory.
In this example the value of Ring_0
will be represented by a value of 0x0
in memory,
the value of Ring_1
will be represented by 0x1
, and so on.
A Practical Example #
The following example, and accompanying comparison with C, demonstrates the configuration of a hypothetical UART device by interfacing with an 8-bit memory-mapped configuration register. This example has been adapted from a presentation by AdaCore viewable here.
with System.Storage_Elements; use System.Storage_Elements; ------------------------------------------------------------------------------- -- Main ------------------------------------------------------------------------------- procedure Main is ---------------------------------------------------------------------------- -- Baud rate type. ---------------------------------------------------------------------------- type Baud_Rate_T is (b_9600, b_14400, b_115200); for Baud_Rate_T use ( b_9600 => 0, b_14400 => 1, b_115200 => 7 ); ---------------------------------------------------------------------------- -- Parity Select Type ---------------------------------------------------------------------------- type Parity_T is (None, Even, Odd); for Parity_T use ( None => 0, Even => 1, Odd => 2 ); ---------------------------------------------------------------------------- -- Control Register for a hypothetical UART device. ---------------------------------------------------------------------------- type UART_Control_Register_T is record Baud_Rate : Baud_Rate_T; Parity : Parity_T; Unused : Boolean := False; ISR : Boolean; end record with Volatile_Full_Access, Size => 8; for UART_Control_Register_T use record Baud_Rate at 0 range 0 .. 2; Parity at 0 range 3 .. 4; Unused at 0 range 5 .. 6; ISR at 0 range 7 .. 7; end record; ---------------------------------------------------------------------------- -- The address of the UART control register. ---------------------------------------------------------------------------- UART_CONTROL_REG_ADDR : constant System.Address := To_Address (16#8000_0040#); ---------------------------------------------------------------------------- -- The UART control register itself. ---------------------------------------------------------------------------- UART_Control_Register : UART_Control_Register_T with Import, Convention => Ada, Address => UART_CONTROL_REG_ADDR; begin -- Configure the UART. UART_Control_Register.Baud_Rate := b_115200; UART_Control_Register.Parity := Even; end Main;
Contrast this with the same functionality implemented in C. Despite being shorter, the register cannot be altered without using bitwise operators to manipulate the individual fields. This approach is generally considered more error-prone than using a record in Ada overlaid at the register's memory-mapped address. It's possible to define a struct type in C with bit-fields for the individual elements, however the C standard does not guarantee the layout and order of the individual fields5.
#include <stdint.h> #define UART_CNTL_REG_ADDR 0x80000040 #define UART_CNTL_BAUD_MASK 0x07 #define UART_CNTL_BAUD_SHIFT 0 #define UART_CNTL_PARITY_MASK 0x18 #define UART_CNTL_PARITY_SHIFT 3 #define UART_CNTL_IE_MASK 0x80 #define UART_CNTL_BAUD_9600 0 #define UART_CNTL_BAUD_14400 1 #define UART_CNTL_BAUD_115200 7 #define UART_CNTL_PARITY_NONE 0 #define UART_CNTL_PARITY_EVEN 1 #define UART_CNTL_PARITY_ODD 2 #define UART_CNTL_ISR_ENABLE 2 #define UART_CNTL_ISR_DISABLE 2 int main(int argc, char **argv) { /** The UART control register pointer. */ volatile uint8_t *uart_control_reg = (uint8_t*)UART_CNTL_REG_ADDR; // Configure the UART. *uart_control_reg &= ~UART_CNTL_BAUD_MASK; *uart_control_reg |= UART_CNTL_BAUD_115200; *uart_control_reg &= ~UART_CNTL_PARITY_MASK; *uart_control_reg |= (UART_CNTL_PARITY_EVEN << UART_CNTL_PARITY_SHIFT); }
For anyone interested in learning more about Ada, or tutorials for learning the language, I recommend they visit AdaCore's learning center, at learn.adacore.com, and Olivier Henley's fantastic list of Ada resources at Awesome Ada.
The Bad and the Ugly #
No programming language is perfect, and Ada is no exception. I freely admit that I'm by no means an expert in Ada, so some of these (minor) complaints might be explained by my naiveté of established conventions and inexperience with the language.
Poor pointer semantics - The requirement to declare a pointer-to-type as a new type feels especially onerous.
Declaring a distinct pointer type may be intuitive to the compiler,
but it provides an unnecessary burden to the programmer.
Conversion to and from —and subsequent dereferencing of— pointers is unnecessarily cumbersome.
Needing to instantiate the generic library
Address_To_Access_Conversions
to do something as simple as creating a pointer from an arbitrary address seems like unnecessary overkill.
That being said, I like the semantic intuitiveness of Ada's access types,
they point to a memory address, and allow access to the value located there.
The noted absence of pointer arithmetic is very welcome: Access types in Ada are not a numeric type in any form.
Perhaps these minor inconveniences are simply unavoidable consequences of integrating a low-level concept like pointers into higher-level language constructs. Despite the presence of many higher-level constructs such as ranges, fat-pointers and object-orientation, Ada is principally oriented around low-level concerns, which is a perfect segue into my second issue…
The runtime - In addition to implementing Ada's standard library, the runtime has a wide scope of responsibilities, such as performing range checks on constrained types. Comprehensive knowledge of the Ada runtime and its structure is required to implement even moderately high-level language constructs for a new platform.
My compiler didn't ship with a suitable zero-footprint runtime library for the x86 platform. As a result I had to implement my own 'zero-footprint' runtime library. I found the process to be particularly unintuitive. The equivalent process of putting together the build pipeline and infrastructure for a bare-metal C environment is much more straightforward. The ability to make highly granular modifications to the language's runtime is extremely useful if you're targeting platforms with limited resources or functionality. However the complicated nature of this process serves as a barrier to uptake that other languages may not have. This is by no means an insurmountable problem, just something I found particularly confusing as a newcomer.
I put together a comprehensive guide to setting up an Ada runtime library suitable for operating system development on the x86 platform, which can be viewed here.
Despite some minor misgivings, my experience with Ada remains positive overall. More than once I've heard online detractors accuse Ada of having aged poorly, or excuse it on the basis that it's a product of its time; A historical artefact to be taken as-is, not to be judged by today's —presumably higher— standards. The fact that many of the high-level language constructs found in modern scripting languages have been in Ada since the beginning shows that maybe the rest of the programming ecosystem hasn't actually aged as well as we think.
What Happened? #
Despite having a lot to offer, Ada never gained much ground as a mainstream programming language outside the aerospace and defense industries. Many explanations have been offered as to why, with language historians often citing Ada's early compiler ecosystem as a barrier for the language's wider adoption. Veterans of the DoD's Ada mandate trade war stories about long compilation times on expensive, proprietary development infrastructure. The technical challenge of implementing, and operating an Ada compiler on contemporary hardware was so significant that enterprise software tool developer Rational would even go so far as creating a dedicated workstation for the task, the Rational R10006. Even with the Ada standard being publicly available, It wouldn't be until the release of GNAT in 1995 —based on the FSF's GCC compiler— that Ada would have a freely available compiler.
Despite Department of Defense sponsored studies acknowledging Ada as the most suitable language for the task (National Research Council, 1997), the DoD would ultimately repeal its controversial mandate7 that all in-house mission-critical software be developed in Ada. By this point in time the DoD had largely shifted its focus away from sponsoring expensive in-house software development, and towards the utilisation of COTS (Commercial Off The Shelf) software. This event would mark the beginning of Ada's decline in the general programming community.
For the development of the benighted F-35 Joint Strike Fighter's avionics software, Lockheed Martin would ultimately choose to forgo Ada in lieu a heavily abridged dialect of C++8. John H. Robb, the Senior Manager of the F-35 Joint Strike Fighter Air Vehicle Software team at Lockheed Martin Aeronautics Fort Worth writes on the subject: 'Ada was seen as the technically superior and more robust language, but concern over the ability to successfully staff the software engineers required to develop the massive amounts of safety critical software caused the F-35 team to carefully look and finally to choose C and C++ for the implementation of safety critical software.' For Ada fans this statement is bittersweet indeed.
What's Next, and What Have We Learned? #
For all the water-cooler chatter of obsolescence, Ada still enjoys serious respect in industries where failure isn't an option. Nvidia recently chose Ada and its SPARK subset9 as their language of choice for developing safety-critical firmware in their embedded-systems. Some other notable users of Ada include the European Space Agency10, BAE Systems, Saab, Thales, and Boeing. A quick glance at leading Ada compiler manufacturer AdaCore's customer list demonstrates that the market for Ada is certainly alive and well. The historical record will likely prove kinder to Ada than its critics.
Many developers point to Rust as the systems programming language of the future.
Rust markets itself as a safe11 language,
which invites comparisons against both C and Ada (for vastly different reasons).
Superficial comparisons aside,
Rust seems like a language that has neither learned much from C's failures, nor from Ada's successes.
Despite what the Rust community may say —or the algal bloom of new OSdev projects in Rust—
Rust does not seem well adapted to low-level programming:
It lacks basic features like bitfields, and data structure packing.
Its foreign function interface seems particularly poorly implemented.
The official Rust documentation
suggests the use of the external third-party libc
library (called a 'crate' in Rust parlance)
to provide the type definitions necessary to interface with C programs.
As of the time of writing, this crate has had
95 releases12.
Contrast this with Ada's Interfaces.C
package,
which was added the language in Ada 95 and hasn't needed to change in any fundamental way since.
Despite Rust's present shortcomings,
it could prove to be a capable language that has much to offer if it can manage to standardise itself and evolve in a sensible way13.
I'm nothing if not an idealist, for better or worse. I've been willing to pick for myself the smallest hills to die on in matters of technical dispute. When push comes to shove however, it pays to be pragmatic. It's true of course that any fully-featured systems programming language can be utilised in a safety-critical manner. As John H. Robb himself so eloquently notes in the aforementioned article: '…the basic elements required to make a language work in [the safety-critical] domain are persistence, determination, and good software engineering discipline'. This is of course true, however the qualities he mentions are valuable commodities indeed. If persistence, determination, and discipline are to be requirements, why not choose a language that requires less of them to achieve the same high-quality results? After all, software is certainly easier to get wrong than it is to get right.
References #
- William A. Whitaker (1993). Ada - The Project, The DoD High Order Language Working Group. ACM SIGPLAN Notices Vol. 28, No. 3, March 1993.
- National Research Council (1997). Ada and Beyond: Software Policies for the Department of Defense. Washington, DC: The National Academies Press. https://doi.org/10.17226/5463.
- In comparison to the plethora of modern languages that count C as an ancestor. ↲
- It's worth noting that in the days of Ada's conception the United States military, or at very least the US government, could very well have been the chief developer and consumer of high-assurance embedded systems worldwide. If there was to be any paradigm shift in the developmental methodology of such systems, it would seem likely that it would have come from this sector. ↲
- In my humble opinion it's better to design a language to fit an existing problem domain than to pick your weapon of choice and set out in search of new problem domains to apply it to, à la Javascript. ↲
- The oft-repeated claim that embedded development within the DoD was plagued by a troublesome and mountainous plurality of different languages has been anecdotally disputed online, usually by Ada's detractors. Even if this weren't true, I find it hard to believe that JOVIAL would be a more ideal standard language for the DoD's future projects. ↲
- Refer to section 6.7.2.1 paragraph 11 of the C1X standard. ↲
- The Rational R1000 workstation is an interesting topic in its own right. The Danish Data Museum has detailed their excellent work restoring an R1000/s400 computer donated to them by a Danish aerospace manufacturer. ↲
- While the mandate itself was never popular with developers, the repealing of the mandate was probably as much a cost-saving exercise as anything. Without a specific requirement to use the Ada programming language, DoD projects would likely be open to more competitive bidding from a wider range of software vendors. It's also worth noting that historical accounts attest to the teams commonly receiving 'waivers' for the mandate. Enforcement of the mandate seems to have been fairly soft in its later days. ↲
- The Joint Strike Fighter Air Vehicle (AV) C++ Rules were developed by Lockheed Martin to create a dialect of C++ compatible with safety-critical software engineering. Bjarne Stroustrup hosts a copy of the guidelines on his website: https://www.stroustrup.com/JSF-AV-rules.pdf. ↲
- SPARK is a formally-provable subset of Ada. SPARK reduces the scope of the language to a strict subset for which formal verification of lack of runtime errors is possible. ↲
- Whenever the topic of Ada comes up on Hacker News, someone inevitably brings up the failure of the European Space Agency's Ariane V rocket, as though it were some searing indictment of the Ada programming language. Without delving unnecessarily far into a subject about which a wealth of material has already been written, it's suffice say that Ada can be spared the blame for this accident. Evidence supports the crash having been caused by poor design practices, the end. Despite the wealth of evidence exonerating the language, some people will simply never get it, and conjecture persists in the face of common sense. ↲
- Safety is a different concept than being suitable for safety-critical applications. Rust is designed principally around the elimination of the kind of memory bugs associated with the C programming language, such as use after free, buffer overflow and memory leaking, among others. ↲
-
This begs the question: what about a C interface requires this much work?
Admittedly the
libc
crate ostensibly provides a very broad scope of functionality. Their official RFC states the opposite: 'The primary purpose of this crate is to provide all of the definitions necessary to easily interoperate with C code (or "C-like" code) on each of the platforms that Rust supports.', indicating a narrow scope of requirements. However browsing the Issues section their Github gives the opposite impression. Either way, interoperability (in my experience) remains dysfunctional at best. ↲ - Rust, which as of the time of writing has yet to be formally standardised, seems to have a very wide variety of hands on the wheel. It enjoys an active and enthusiastic community, which ostensibly appears to be a collection of refugees from the Node.js and Golang communities. Despite my misgivings about some of Rust's design decisions, I can see its benefits. For companies such as Mozilla, migrating their products from C++ to Rust seems like a great idea. ↲