https://ajxs.me/blog/Giving_Ada_a_chance.html
~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:
* Programming
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 standards^3 -- 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"^1. 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 to
^2. 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 DoD^4.
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 fields^6.
#include
#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 naivete 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 lifted^7, 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 subset^8 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 Agency^9,
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
safety^10, 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 releases^11.
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 way^12.
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.
(c) 2021 AJXS