Giving Ada a Chance
Update October 2022: After receiving some fantastic correspondence from experienced Ada developers, I've made some amendments to this article to correct some factual errors, remove some unsubstantiated conjecture, and improve the general quality of the writing.
Much of the technical material presented in this article is available as part of my contributions to osdev.org
I consider myself a rational man. While I may believe in an entirely deterministic model of the universe, I do not believe it to be guided by any conscious process. I don't believe in destiny. This absence of guidance makes such fortuitous occurrences as the one I will discuss all the more extraordinary, and for this I am all the more grateful.
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. My regular walk to and from the local shopping centre takes me past several of the buildings belonging to the highly regarded engineering faculty of the aforementioned University.
Making my way home one serendipitous afternoon, I happened across a sizeable stack of books sitting on the curb outside one of the University's engineering buildings. The university was ostensibly in the process of liquidating its stockpile of old engineering books, and had left them in a pile for the local council to collect. Amongst material covering a wide variety of academic disciplines, two books in particular caught my eye: Building Parallel, Embedded, and Real-Time Applications with Ada, and Concurrent and Real-Time Programming in Ada.
I had 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, nothing more. 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 by modern constructs associated with modern high-level languages such as ranges, slicing, and exception-handling. The syntax —Admittedly verbose by modern standards1— seemed deliberately and purposefully constructed to make the language comprehensible at a glance.
The fact that Ada was designed with embedded software in mind was of particular interest to me. 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 seemed promising to me.
Not the Camel You Expected #
A common pejorative refrain directed at Ada by its many detractors is that it is a language "designed by committee", or even worse, a language "designed by committee for the military"2. The implication of which being that this would preclude it from having any kind of real-world practicality. I contend that it is 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 to3. I will spare readers a detailed retelling of Ada's conception within the Department of Defense's 'High Order Language Working Group', save to say that the Ada programming language 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. Ironically, given its status as the de-facto standard language of modern embedded-system development, the C language was considered unsuitable for this purpose: "When Bell Labs were invited to evaluate C against the DoD requirements, 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 and operating-system development. One feature in particular that impressed me greatly was Ada's representation clauses (see below). They provide a method for defining the in-memory representation of low-level data structures. I was very quickly able to adapt my own long suffering operating-system development projects to Ada, improving the quality of my codebase greatly in the process. The following section details some of Ada's features:
Custom Types
In addition to being a strongly typed language,
Ada allows for the definition of new scalar, enumerated, and record types.
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 of this range to a variable of this type will raise a Constraint_Error
exception at runtime.
Representation Clauses
Ada allows for explicitly defining the in-memory representation of scalar and compound types. The following example demonstrates the definition of a record type (equivalent to structures in C), 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 in length, 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 is 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 preface this section by freely admitting that I am by no means an expert in the Ada programming language. Some of these (minor) complaints could be perfectly 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.
However contextually intuitive the act of declaring a distinct pointer type may be to the compiler,
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.
All this being said, I like the semantic intuitiveness of Ada's access types, in contrast to C's pointers.
The noted absence of pointer arithmetic is very welcome: Access types in Ada are not a numeric type in any form.
They point to a memory address, and allow access to the value located there. Simple, sensible.
Perhaps these minor inconveniences are simply unavoidable consequences of integrating a low-level concept like pointers into higher-level language constructs.
For reasons like this, Ada is hard to place on this continuum.
Despite the presence of many higher-level constructs such as ranges, fat-pointers and object-orientation,
Ada is a language 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, it's responsible for a variety of runtime functions, such as performing range checks on constained types. Comprehensive knowledge of the Ada runtime and its structure is required for the implementation of even moderately high-level language constructs on a particular platform. I will admit being unaccustomed to being concerned with a language's runtime library when targeting bare-metal. I found the process of implementing my own runtime library for a bare-metal target to be particularly unintuitive. When beginning the CXOS project, my compiler did not ship with a suitable zero-footprint runtime library for the x86 platform. As a result I was forced to learn the process of implementing a bare-metal x86 runtime for myself. 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 for deployments on platforms with limited resources or functionality, however the complicated nature of this process serves as a barrier to uptake that other language's may not have. This is by no means an insurmountable problem, albeit one that I found particularly confusing for 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 my minor misgivings, my experiences with Ada remain positive overall. More than once I've heard online detractors accuse Ada of having aged poorly, or excuse it on the basis that it was a product of its age; A historical artefact to be taken as-is, not to be judged by today's —impliedly higher— standards. I contend the fact that a modern developer can recognise in Ada many of the high-level constructs considered unique to modern scripting languages demonstrates it is in fact the programming ecosystem on the whole that's aged poorly. I also wonder if complicated low-level programming concepts would be more intuitive to beginners if taught through Ada instead of C, as is common in computer science and engineering faculties worldwide.
What Happened? #
Despite the many great things it has to offer, Ada has never gained much ground as a mainstream programming language outside of the aerospace, and the 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 is not an option. Nvidia recently chose Ada and its SPARK subset9 as their language of choice for development of 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 is a language that markets itself on its focus on safety11,
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 necessary for the task, 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 am nothing if not an idealist, for better or worse. I have 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 is 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, then why not choose a language that requires less of such rigor in order 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 is worth noting that in the days of Ada's conception the United States military, or at very least their government, could have very well 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. ↲
- à 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 were not true, I find it hard to believe that JOVIAL would be a more ideal standardised language for 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 arises on Hacker News, there's inevitably someone smugly bringing up the failure of the first test flight 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 is 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 an 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. ↲