There is code running everywhere and on everything. It’s no longer isolated to just our cell phones and laptops. There are smart keys, smart toasters, smart pens, and even smart clothing. And everything is going to continue to get smarter. From a software development perspective, smarter means that there is more code being shoved onto these processors every day. When the software catches up and maxes out the hardware, there is new hardware waiting to take on the new responsibilities. With the ever-growing pile of code that we, as software developers, are tasked to maintain, there are, inevitably, things that will be missed. And as we rely more and more on software-enabled devices for our daily activities, those errors can jeopardize our safety and security. Despite this risk, the software development community has only recently become cognizant of these issues as they manifest as security vulnerabilities in IoT devices, car control systems, and financial software systems. So, what is the solution for handling mountains of code that needs to be safe, secure, and maintainable? The aerospace industry has had an answer to this questions for many years. When tasked with applications with requirements such as code safety and reliability, efficient software development cycles, and the ability to maintain code bases for many years, they turn to a programming language designed for exactly that purpose, Ada.
The Ada programming language isn’t much of a household name in industries outside of aerospace and defense. Most of us who target embedded platforms are more familiar with C. There are two reasons for this. C is the fundamental language of Unix-like operating systems, and it is an easy language to program with. In fact, C’s design is based on two principles: the language should trust the programmer to do what’s right, and it shouldn’t prevent the programmer from doing what needs to be done. These principles promote speedy development for embedded systems but at a cost. C programs tend to enable clever programming, which often involves complex preprocessor logic and bit manipulations with very little commenting to describe what the author was trying to accomplish. When it comes time for debugging, especially during maintenance, tracing issues is a nightmare. So, as a C developer, I was always told that comments shouldn’t be required to understand my code, because if they are, it’s probably bad code and I should go rewrite it before the code review.
Ada’s design principles also promote speedy development, but with the philosophy that debugging and maintenance need to be considered as part of the development process. Code should be easy to write, easy to read, easy to maintain, and most importantly, it should be reliable. The Ada compiler takes much of this burden from the developer by generating runtime checks to guarantee the integrity of the application. In Ada, unlike C, the developer spends more time specifying than implementing. Since the developer gives the compiler more information to work with about the context of the code, the compiler can automatically handle much of the error checking that is typically caught during runtime testing (or after deployment). And from a code-reviewer’s perspective, the extra information in the specification makes it easier to understand the context of the code.
I had never seen, or even heard of, Ada before joining AdaCore. I didn’t know why aerospace companies weren’t just using C and C++ for their rockets and airplanes. But, I figured there must be a good reason. When I first looked at the Ada syntax I saw paradigms familiar to other ALGOL-like programming languages. I recognized control structures like if then statements, loops and case statements, and each line ended with a semicolon, which is somewhat calming when looking at a new language. The major syntactic differences from C are that instead of bracket closures, scope is defined with keywords like begin and end, and variable declarations are backwards from what I was used to with the variable name preceding the type. What was interesting though, was that there were more words in the code than I was used to seeing. Specifically, there was heavy use of user-defined data types with the relevant information about the type. For example:
The above example demonstrates a few very powerful features of Ada. The first is Ada’s strong typing. The user has the ability to define data types in order to guarantee that types cannot be mixed accidentally. Because Ada does not have implicit casting (known as type conversion in Ada), the programmer has to consciously cast (convert) during the assignment, bringing attention to the fact that the data types are different.
Another feature shown above is Ada’s “aspect” system. Using the with keyword, the programmer can specify certain properties of the type. In the case above, I am specifying that the type Byte has a size of 8 bits. This constraint directs the compiler to use 8 bits to represent objects of the type. The range keyword is a contract that tells the compiler to insert runtime checks on assignments to objects of this type. For example, when a value is stored into a variable of type Grade, the compiler will add checks to make sure that that value is within the range 0 through 100 inclusive; if the checks fail, an exception is raised.
Contracts are an extremely powerful feature of Ada that makes it useful for safe applications. They also make the code more readable because the compiler handles inserting runtime checks, removing the burden from the programmer to write these explicitly in the source code. Contracts can be more general than range checks however:
Subtype predicates allow the programmer to create more complex patterns that the compiler will check at run time in contexts such as the targets of assignment statements. The type Even above will be checked to make sure that the value is an even number. Similarly, the type Divisor will be checked to make sure that it is not zero, avoiding a potential divide-by-zero error. The type Sorted_List is using a feature that is a little more complicated; a type Invariant. This type of contract can ensure that some postcondition holds for an encapsulated type (known as a “private type” in Ada), on return from operations that are performed on objects of that type. In the above example, on return from an operation such as Insert or Delete that may be available for the Sorted_List type, the Is_Sorted function will be invoked.
We can apply contracts to subprograms in the form of preconditions and postconditions. As the names suggest, these contracts check the state of the application prior to calling and upon returning from a specific subprogram. Here is an example:
If either the precondition or postcondition fails at run time, an assertion error will be raised. This will guarantee that subprograms cannot execute in an unknown state. Secondarily, contracts also serve as documentation to reviewers and code maintainers. Without looking at the code’s implementation we can get a detailed description of the values that variables will hold, and what subprograms should accomplish during their execution. With contracts alone, Ada makes the coding environment for applications both more reliable, and more readable.
This is just a small set of features that make Ada a powerful language. From its conception in the early 1980’s, it was designed to include facilities for exception handling and parallel computing. And later, with Ada 95, object orientation was added to make it a safer alternative to languages like C++. The latest version of the language, Ada 2012, has added the contract-based programming features already mentioned as well as a host of other features such as advanced concurrency and container support.
For someone new to Ada, it is bit of a departure from the normal programming workflow. Most of the programmer’s effort is spent describing types and subprogram conditions rather than implementing the logic. A byproduct is the availability of many automated tools for Ada that can help detect potential runtime errors before the code is ever executed. The compiler catches many things, which is very convenient when starting out with the language, but there are also static analysis tools, and even formal proof mechanisms that can minimize or even guarantee the absence of runtime errors from our application. If we fit these tools into the software development cycle, it is easy to see how they can maximize efficiency by removing frequent code reviews and long debugging sessions to catch nasty runtime bugs.
Most of us became software engineers because we enjoy the ever-evolving challenges posed by turning application requirements into reliable working code. As technology advances, we are finding innovative ways to make new things. But it is our responsibility, as software engineers, to ensure the software we write is safe and secure. As our software becomes deployed in more everyday devices, this responsibility has real-world consequences. Devices that previously seemed trivial and inert may have the same criticality as the flight management system in the airplanes that fly over our heads. So, let’s look to that industry for ideas in how they accomplish the impressive feat of making the safest devices in the world, let’s use Ada and make safe and secure software that matters.