~ajxs

Giving Ada a chance

2021.01.13
An in-depth look at the Ada programming language, its history, and what it has to offer developers today. As well as a fistful of my humble opinions.
tagged as:
TL;DR: Ada is an extremely interesting and robust programming language that has a lot to offer modern developers of system and bare-metal software. At very least, Ada presents many interesting ideas that designers of modern programming languages could stand to learn much from. If you want a 30 second version of this article, check out the practical example that I provide for a comparison of Ada with C.

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 certainly do not believe it to be guided by any conscious process. I do not 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 the 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 Fortran’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 standards3 — 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 military1. The implication of which being that (so-called) design by committee precludes it from any 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 to2. I will spare readers a detailed retelling of Ada’s conception within the Department of Defence’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). After its successful implementation, 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 highly granular way to define the in-memory representation of low-level data structures. I was very quickly able to adapt my own long suffering operating-system development project 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 fields6.

#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);
}

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 the consequence 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, the Ada’s runtime fulfils important functions at runtime, such as range checks on constained types. Comprehensive knowledge of the Ada runtime and its structure is required for the use of even moderately high-level language constructs on a 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 particular time consuming. 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. In rebuttal I contend that the fact that a modern developer can recognise in Ada many of the high level constructs thought to be revolutionary in modern scripting languages indicates that it is in fact the programming ecosystem on the whole that’s aged poorly. I can’t help but think that complicated programming paradigms would seem more intuitive to beginners if taught through Ada instead of C and its derivative languages, as is common in computer science and engineering faculties worldwide.

What’s Next, and What Have We Learned?

Rumors of Ada’s demise remain exaggerated, decades later. Mere years after the DoD Ada mandate had been lifted7, Lockheed Martin’s choice to forgo Ada as the language of choice for the development of the benighted F-35 Joint Strike Fighter in lieu of a heavily abridged dialect of C++5 was for many the definitive sign that the bell tolled for Ada. 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. The proof of the pudding however remains in the tasting. Reports from 2016 indicated that the Block 3i avionics software loaded onto the F-35 was so problematic that it required rebooting on average every 4 hours (flightglobal.com, 2016). The historical record will likely prove kinder to Ada than its critics.

For all the water-cooler chatter of obsolescence, Ada enjoys serious respect in industries where failure is not an option. Nvidia recently chose Ada and its SPARK subset8 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 Agency9, 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 writing is on the wall: Ada is here to stay.

Many developers point to Rust as the systems programming language of the future. Rust is a language that markets itself on its focus on safety10, 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 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 releases11. 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 way12.

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

Flightglobal (2016) F-35 locked and loaded with improved Block 3i software. Retrieved 12 Jan, 2021, from https://www.flightglobal.com/f-35-locked-and-loaded-with-improved-block-3i-software/120527.article

William A. Whitaker (1993). Ada - The Project, The DoD High Order Language Working Group. ACM SIGPLAN Notices Vol. 28, No. 3, March 1993.


  1. 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 come from this sector.
  2. A la Javascript.
  3. In comparison to the plethora of modern languages that count C as an ancestor.
  4. 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.
  5. 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.
  6. Refer to section 6.7.2.1 paragraph 11 of the C1X standard.
  7. It is worth mentioning that a common criticism of the DoD’s Ada mandate was the technical difficulty of writing and compiling Ada in its early days. An oft-cited cause of such difficulties was the technical challenge of implementing a compiler for the language on contemporary hardware. Ada's current de-facto standard compiler GNAT — based on the FSF's GCC compiler — was officially launched in 1995. Historical accounts of Ada's early development tell of long compilation times on expensive proprietary compiler infrastructure. The high cost of said compilers were ostensibly another barrier to Ada's mainstream adoption. It's also worth noting that by the time the DoD’s mandate on Ada was lifted the DoD had largely shifted its focus towards the use of COTS (Commercial Off The Shelf) software. As a result, the lifting of the mandate probably had a much lesser impact than people suppose.
  8. 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.
  9. 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.
  10. 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.
  11. 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.
  12. 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. Rust’s core community ostensibly appears to be a collection of refugees from the Node.js and Golang communities. It enjoys an active and enthusiastic community. 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.