The Ada programming language was born in the mid-1970s, when the US Department of Defense (DoD) and the UK’s Ministry Of Defence sought to replace the hundreds of specialized programming languages used for the embedded computer systems that increasingly made up essential parts of military projects. Instead, Ada was designed to be be a single language, capable of running on all of those embedded systems, that offered the same or better level of performance and reliability.
With the 1995 revision, the language also targeted general purpose systems and added support for object-oriented programming (OOP) while not losing sight of the core values of reliability, maintainability and efficiency. Today, software written in Ada forms the backbone of not only military hardware, but also commercial projects like avionics and air-traffic control systems. Ada code controls rockets like the Ariane 4 and 5, many satellites, and countless other systems where small glitches can have major consequences.
Ada might also be the right choice for your next embedded project.
To pick the new programming language, the DoD chartered the High Order Language Working Group (HOLWG), a group of military and academic experts, to draw up a list of requirements and pick candidate languages. The result was the so-called ‘Steelman requirements‘.
Crucial in the Steelman requirements were:
- A general, flexible design that adapts to satisfy the needs of embedded computer applications.
- Reliability. The language should aid the design and development of reliable programs.
- Ease of maintainability. Code should be readable and programming decisions explicit.
- Easy to produce efficient code with. Inefficient constructs should be easily identifiable.
- No unnecessary complexity. Semantic structure should be consistent and minimize the number of concepts.
- Easy to implement the language specification. All features should be easy to understand.
- Machine independence. The language shall not be bound to any hardware or OS details.
- Complete definition. All parts of the language shall be fully and unambiguously defined.
The requirements concluded that preventing programmers from making mistakes is the first and most essential line of defense. By removing opportunities to make subtle mistakes such as those made through implicit type casting and other dangerous constructs, the code becomes automatically safer and easier to maintain.
The outcome of that selection process was that while no existing programming languages was suited to be the sole language for DoD projects, it was definitely feasible to create a new language which could fit all of those requirements. Thus four constructors were paid to do exactly this. An intermediate selection process picked the two most promising approaches, with ultimately one language emerging as the victor and given the name ‘Ada‘.
In-Depth Defense as Default
Ada’s type system is not merely strongly typed, but often referred to as ‘super-strongly typed’, because it does not allow for any level of implicit conversions. Take for example this bit of C code:
typedef uint32_t myInt; myInt foo = 42; uint32_t bar = foo;
This is valid code that will compile, run and produce the expected result with
bar printing the answer to life, the universe and everything. Not so for Ada:
type MyInt is Integer; foo: MyInt; bar: Integer; foo := 42; bar := foo;
This would make the Ada compiler throw a nasty error, because ‘Integer’ and ‘MyInt’ are obviously not the same. The major benefit of this is that if one were to change the type definition later on, it would not suddenly make a thousand implicit conversions explode throughout the codebase. Instead, one has to explicitly convert between types, which promotes good design by preventing the mixing of types because ‘they are the same anyway’.
Anyone who has wrestled through the morass of mixing standard C, Linux, and Win32 type definitions can probably appreciate not having to dig through countless pages of documentation and poorly formatted source code to figure out which typedef or macro contains the actual type definition of something that just exploded half-way through a compile or during a debug session.
Ada adds further layers of defense through compile-time checks and run-time checks. In Ada, the programmer is required to explicitly name closing statements for blocks and state the range that a variable can take on. Ada doesn’t define standard types like
float, but instead requires that one creates types with a specific range from the beginning. This is also the case for strings, where aside from unbounded strings all strings have a fixed length.
At run-time, errors such as illegal memory accesses, buffer overflows, range violations, off-by-one errors, and array access can be tested. These errors can then be handled safely instead of leading to an application crash or worse.
Ada implements an access-types model rather than providing low-level generic pointers. Each access type is handled by a storage pool, either the default one or a custom one to allow more exotic system memory implementations like NUMA. An Ada programmer never accesses heap memory directly, but has to use this storage pool manager.
Finally, the compiler or runtime decides how data is passed in or out of a function or procedure call. While one does have to specify the direction of each parameter (with ‘in‘, ‘out‘, or ‘in out‘), the ultimate decision of whether the data being passed via a register, via the heap, or as a reference will be taken by the compiler or runtime, never by the programmer. This prevents overflow issues where stack space is not sufficient.
The Ravenscar profile and the SPARK dialect are subsets of Ada, the latter of which strongly focuses on contracts. Over time features from these subsets have been absorbed into the main language specification.
Programming with Ada today
The ANSI certified the Ada 83 specification in 1983; Intel’s 80286 had just been released and Motorola’s 68000 was still only four years old. It was the dawn of home computers, but it was also the awkward transition of the 1970s into the 1980s, when microcontrollers were becoming more popular. Think of the Intel 8051 and its amazing 4 kB EPROM and 128 bytes of RAM.
Today’s popular microcontrollers are many times more powerful than what was available in 1983. You can grab any ARM, AVR, RISC-V, etc. MCU (or Lego Mindstorms NXT kit) and start developing for it just like you would any other C-based toolchain. Not surprisingly, the popular GNAT Ada compiler is built on GCC. An LLVM-based toolchain is also in the works, currently using the DragonEgg project.
There are two versions of the GCC-based Ada toolchain. The AdaCore version enjoys commercial support, but comes with some strings attached. The Free Software Foundation’s version is free, naturally, and has more or less feature parity with AdaCore.
To get started quickly, you can either use the GNAT Programming Studio IDE (GPS) that comes with the AdaCore version of the GNAT toolchain (and on Github), or rough it with a text editor of your choice and compile by hand, or cheat by writing Makefiles. The toolchain here is slightly more complicated than with C or C++, but made very easy by using the
gnatmake utility that wraps the individual tools and basically works like GCC, so that it can be easily integrated into a build system.
An example of a small, yet non-trivial, Ada project written by yours truly in the form of a command line argument parser can be found at its project page. You can find a Makefile that builds the project in the
ada/ folder, which sets the folders where the Ada package specifications (
.ads) and package bodies (
.adb) files can be found.
These files roughly correspond to header and source files in C and C++, but with some important differences. Unlike C, Ada does not have a preprocessor and does not merge source and header files to create compile units. Instead, the name of the package specified in the specification is referenced, along with its interface. Here the name of the
.ads file does not need to match the name of the package either. This provides a lot of flexibility and prevents the all too common issues in C where one can get circular dependencies or need to include header files in a particular order.
Where to go from here
After grabbing the GNAT toolchain, firing up GPS or Vim/Emacs, and staring at the blinking cursor on an empty page for a while, you might wonder how to get started. Fortunately we recently covered this project which had an Ada-based project running on a PicoRV32 RISC-V core. This uses the common ICE40LP8K CPLD that is supported by open-source FPGA toolchains, including Yosys.
Introductions and references for the language itself are found as a simple introduction for Java and C++ developers (PDF), the AdaCore basic reference, a reference over at WikiBooks, and of course ‘Programming in Ada 2012‘ in dead-tree format for those who like that little bit of extra heft. It is probably the most complete reference beyond diving into the 2012 Ada Language Reference Manual (LRM) and its glorious 945 pages.
While still quite a bit of a rarity in hobbyist circles, Ada is a fully open language with solid, commercially supported toolchains that are used to write software for anything from ICBM and F-15 avionics to firmware for medical devices. Although a fairly sprawling language beyond the core features, it should definitely be on the list of languages you have used in a project, if only because of how cool it looks on your resume.