Ada for the Embedded C Developer
Ada for the Embedded C Developer
a
f
ort
heEmbeddedCDe
vel
oper
Quenti
nOchem
Rober
tTi
ce
Gus
tavoA.Hoffmann
Pa
tri
ckRoger
s
Ada for the Embedded C Developer
Release 2021-06
1 Introduction 3
1.1 So, what is this Ada thing anyway? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3
1.2 Ada — The Technical Details . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5
i
4.2.1 Representation Clauses . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 82
4.2.2 Embedded Assembly Code . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 83
4.3 Interrupt Handling . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 84
4.4 Dealing with Absence of FPU with Fixed Point . . . . . . . . . . . . . . . . . . . . . . . . 86
4.5 Volatile and Atomic data . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 90
4.5.1 Volatile . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 91
4.5.2 Atomic . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 93
4.6 Interfacing with Devices . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 95
4.6.1 Size aspect and attribute . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 95
4.6.2 Register overlays . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 96
4.6.3 Data streams . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 99
4.7 ARM and svd2ada . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 105
ii
7.3.4 Pointer to subprograms . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 194
7.4 Design by components using dynamic libraries . . . . . . . . . . . . . . . . . . . . . . . 199
10 Conclusion 221
Bibliography 253
iii
iv
Ada for the Embedded C Developer, Release 2021-06
This course introduces you to the Ada language by comparing it to C. It assumes that you have
good knowledge of the C language. It also assumes that the choice of learning Ada is guided by
considerations linked to reliability, safety or security. In that sense, it teaches you Ada paradigms
that should be applied in replacement of those usually applied in C.
This course also introduces you to the SPARK subset of the Ada programming language, which
removes a few features of the language with undefined behavior, so that the code is fit for sound
static analysis techniques.
This course was written by Quentin Ochem, Robert Tice, Gustavo A. Hoffmann, and Patrick Rogers
and reviewed by Patrick Rogers, Filip Gajowniczek, and Tucker Taft.
1 http://creativecommons.org/licenses/by-sa/4.0
CONTENTS 1
Ada for the Embedded C Developer, Release 2021-06
2 CONTENTS
CHAPTER
ONE
INTRODUCTION
To answer this question let's introduce Ada as it compares to C for an embedded application. C
developers are used to a certain coding semantic and style of programming. Especially in the em-
bedded domain, developers are used to working at a very low level near the hardware to directly
manipulate memory and registers. Normal operations involve mathematical operations on point-
ers, complex bit shifts, and logical bitwise operations. C is well designed for such operations as it
is a low level language that was designed to replace assembly language for faster, more efficient
programming. Because of this minimal abstraction, the programmer has to model the data that
represents the problem they are trying to solve using the language of the physical hardware.
Let's look at an example of this problem in action by comparing the same program in Ada and C:
[C]
Listing 1: main.c
1 #include <stdio.h>
2 #include <stdlib.h>
3
16 return sum;
17 }
18
29 return 0;
30 }
3
Ada for the Embedded C Developer, Release 2021-06
Runtime output
Sum: 0
[Ada]
Listing 2: sum_angles.adb
1 with Ada.Command_Line; use Ada.Command_Line;
2 with Ada.Text_IO; use Ada.Text_IO;
3
4 procedure Sum_Angles is
5
19 return Sum;
20 end Add_Angles;
21
Build output
Compile
[Ada] sum_angles.adb
Bind
[gprbind] sum_angles.bexch
[Ada] sum_angles.ali
Link
[link] sum_angles.adb
Runtime output
Sum: 0
Here we have a piece of code in C and in Ada that takes some numbers from the command line
and stores them in an array. We then sum all of the values in the array and print the result. The
tricky part here is that we are working with values that model an angle in degrees. We know that
angles are modular types, meaning that angles greater than 360° can also be represented as Angle
mod 360. So if we have an angle of 400°, this is equivalent to 40°. In order to model this behavior
in C we had to create the MOD_DEGREES macro, which performs the modulus operation. As we
read values from the command line, we convert them to integers and perform the modulus before
storing them into the array. We then call add_angles which returns the sum of the values in the
array. Can you spot the problem with the C code?
Try running the Ada and C examples using the input sequence 340 2 50 70. What does the C
4 Chapter 1. Introduction
Ada for the Embedded C Developer, Release 2021-06
program output? What does the Ada program output? Why are they different?
The problem with the C code is that we forgot to call MOD_DEGREES in the for loop of add_angles.
This means that it is possible for add_angles to return values greater than DEGREES_MAX. Let's look
at the equivalent Ada code now to see how Ada handles the situation. The first thing we do in the
Ada code is to create the type Degrees which is a modular type. This means that the compiler
is going to handle performing the modulus operation for us. If we use the same for loop in the
Add_Angles function, we can see that we aren't doing anything special to make sure that our
resulting value is within the 360° range we need it to be in.
The takeaway from this example is that Ada tries to abstract some concepts from the developer
so that the developer can focus on solving the problem at hand using a data model that models
the real world rather than using data types prescribed by the hardware. The main benefit of this
is that the compiler takes some responsibility from the developer for generating correct code. In
this example we forgot to put in a check in the C code. The compiler inserted the check for us in
the Ada code because we told the compiler what we were trying to accomplish by defining strong
types.
Ideally, we want all the power that the C programming language can give us to manipulate the
hardware we are working on while also allowing us the ability to more accurately model data in a
safe way. So, we have a dilemma; what can give us the power of operations like the C language,
but also provide us with features that can minimize the potential for developer error? Since this
course is about Ada, it's a good bet we're about to introduce the Ada language as the answer to
this question…
Unlike C, the Ada language was designed as a higher level language from its conception; giving more
responsibility to the compiler to generate correct code. As mentioned above, with C, developers
are constantly shifting, masking, and accessing bits directly on memory pointers. In Ada, all of
these operations are possible, but in most cases, there is a better way to perform these operations
using higher level constructs that are less prone to mistakes, like off-by-one or unintentional buffer
overflows. If we were to compare the same application written using C and with Ada using high
level constructs, we would see similar performance in terms of speed and memory efficiency. If we
compare the object code generated by both compilers, it's possible that they even look identical!
Like C, Ada is a compiled language. This means that the compiler will parse the source code and
emit machine code native to the target hardware. The Ada compiler we will be discussing in this
course is the GNAT compiler. This compiler is based on the GCC technology like many C and C++
compilers available. When the GNAT compiler is invoked on Ada code, the GNAT front-end expands
and translates the Ada code into an intermediate language which is passed to GCC where the code
is optimized and translated to machine code. A C compiler based on GCC performs the same
steps and uses the same intermediate GCC representation. This means that the optimizations
we are used to seeing with a GCC based C compiler can also be applied to Ada code. The main
difference between the two compilers is that the Ada compiler is expanding high level constructs
into intermediate code. After expansion, the Ada code will be very similar to the equivalent C code.
It is possible to do a line-by-line translation of C code to Ada. This feels like a natural step for a
developer used to C paradigms. However, there may be very little benefit to doing so. For the pur-
pose of this course, we're going to assume that the choice of Ada over C is guided by considerations
linked to reliability, safety or security. In order to improve upon the reliability, safety and security
of our application, Ada paradigms should be applied in replacement of those usually applied in C.
Constructs such as pointers, preprocessor macros, bitwise operations and defensive code typically
get expressed in Ada in very different ways, improving the overall reliability and readability of the
applications. Learning these new ways of coding, often, requires effort by the developer at first,
but proves more efficient once the paradigms are understood.
In this course we will also introduce the SPARK subset of the Ada programming language. The
SPARK subset removes a few features of the language, i.e., those that make proof difficult, such as
pointer aliasing. By removing these features we can write code that is fit for sound static analysis
techniques. This means that we can run mathematical provers on the SPARK code to prove certain
safety or security properties about the code.
6 Chapter 1. Introduction
CHAPTER
TWO
The Ada programming language is a general programming language, which means it can be used
for many different types of applications. One type of application where it particularly shines is
reliable and safety-critical embedded software; meaning, a platform with a microprocessor such
as ARM, PowerPC, x86, or RISC-V. The application may be running on top of an embedded operating
system, such as an embedded Linux, or directly on bare metal. And the application domain can
range from small entities such as firmware or device controllers to flight management systems,
communication based train control systems, or advanced driver assistance systems.
The toolchain used throughout this course is called GNAT, which is a suite of tools with a compiler
based on the GCC environment. It can be obtained from AdaCore, either as part of a commercial
contract with GNAT Pro2 or at no charge with the GNAT Community edition3 . The information in
this course will be relevant no matter which edition you're using. Most examples will be runnable
on the native Linux or Windows version for convenience. Some will only be relevant in the context
of a cross toolchain, in which case we'll be using the embedded ARM bare metal toolchain.
As for any Ada compiler, GNAT takes advantage of implementation permissions and offers a project
management system. Because we're talking about embedded platforms, there are a lot of topics
that we'll go over which will be specific to GNAT, and sometimes to specific platforms supported
by GNAT. We'll try to make the distinction between what is GNAT-specific and Ada generic as much
as possible throughout this course.
For an introduction to the GNAT Toolchain for the GNAT Community edition, you may refer to the
Introduction to GNAT Toolchain4 course.
When we're discussing embedded programming, our target device is often different from the host,
which is the device we're using to actually write and build an application. In this case, we're talking
about cross compilation platforms (concisely referred to as cross platforms).
The GNAT toolchain supports cross platform compilation for various target devices. This section
provides a short introduction to the topic. For more details, please refer to the GNAT User’s Guide
Supplement for Cross Platforms5
2 https://www.adacore.com/gnatpro
3 https://www.adacore.com/community
4 https://learn.adacore.com/courses/GNAT_Toolchain_Intro/index.html
5 https://docs.adacore.com/gnat_ugx-docs/html/gnat_ugx/gnat_ugx.html
7
Ada for the Embedded C Developer, Release 2021-06
The first piece of code to translate from C to Ada is the usual Hello World program:
[C]
Listing 1: main.c
1 #include <stdio.h>
2
Runtime output
Hello World
[Ada]
Listing 2: hello_world.adb
1 with Ada.Text_IO;
2
3 procedure Hello_World
4 is
5 begin
6 Ada.Text_IO.Put_Line ("Hello World");
7 end Hello_World;
Build output
Compile
[Ada] hello_world.adb
Bind
[gprbind] hello_world.bexch
[Ada] hello_world.ali
Link
[link] hello_world.adb
Runtime output
Hello World
The resulting program will print Hello World on the screen. Let's now dissect the Ada version to
describe what is going on:
The first line of the Ada code is giving us access to the Ada.Text_IO library which contains the
Put_Line function we will use to print the text to the console. This is similar to C's #include
<stdio.h>. We then create a procedure which executes Put_Line which prints to the console.
This is similar to C's printf function. For now, we can assume these Ada and C features have
similar functionality. In reality, they are very different. We will explore that more as we delve
further into the Ada language.
You may have noticed that the Ada syntax is more verbose than C. Instead of using braces {} to
declare scope, Ada uses keywords. is opens a declarative scope — which is empty here as there's
no variable to declare. begin opens a sequence of statements. Within this sequence, we're calling
the function Put_Line, prefixing explicitly with the name of the library unit where it's declared,
Ada.Text_IO. The absence of the end of line \n can also be noted, as Put_Line always terminates
by an end of line.
Ada syntax might seem peculiar at first glance. Unlike many other languages, it's not derived from
the popular C style of notation with its ample use of brackets; rather, it uses a more expository syn-
tax coming from Pascal. In many ways, Ada is a more explicit language — its syntax was designed
to increase readability and maintainability, rather than making it faster to write in a condensed
manner. For example:
• full words like begin and end are used in place of curly braces.
• Conditions are written using if, then, elsif, else, and end if.
• Ada's assignment operator does not double as an expression, eliminating potential mistakes
that could be caused by = being used where == should be.
All languages provide one or more ways to express comments. In Ada, two consecutive hyphens
-- mark the start of a comment that continues to the end of the line. This is exactly the same as
using // for comments in C. Multi line comments like C's /* */ do not exist in Ada.
Ada compilers are stricter with type and range checking than most C programmers are used to.
Most beginning Ada programmers encounter a variety of warnings and error messages when cod-
ing, but this helps detect problems and vulnerabilities at compile time — early on in the develop-
ment cycle. In addition, checks (such as array bounds checks) provide verification that could not
be done at compile time but can be performed either at run-time, or through formal proof (with
the SPARK tooling).
Ada identifiers and reserved words are case insensitive. The identifiers VAR, var and VaR are
treated as the same identifier; likewise begin, BEGIN, Begin, etc. Identifiers may include letters,
digits, and underscores, but must always start with a letter. There are 73 reserved keywords in Ada
that may not be used as identifiers, and these are:
Both C and Ada were designed with the idea that the code specification and code implementation
could be separated into two files. In C, the specification typically lives in the .h, or header file, and
the implementation lives in the .c file. Ada is superficially similar to C. With the GNAT toolchain,
compilation units are stored in files with an .ads extension for specifications and with an .adb ex-
tension for implementations.
One main difference between the C and Ada compilation structure is that Ada compilation units
are structured into something called packages.
2.7 Packages
The package is the basic modularization unit of the Ada language, as is the class for Java and the
header and implementation pair for C. A specification defines a package and the implementation
implements the package. We saw this in an earlier example when we included the Ada.Text_IO
package into our application. The package specification has the structure:
[Ada]
-- my_package.ads
package My_Package is
-- public declarations
private
-- private declarations
end My_Package;
-- my_package.adb
package body My_Package is
-- implementation
end My_Package;
An Ada package contains three parts that, for GNAT, are separated into two files: .ads files contain
public and private Ada specifications, and .adb files contain the implementation, or Ada bodies.
[Ada]
package Package_Name is
-- public specifications
private
-- private specifications
end Package_Name;
Private types are useful for preventing the users of a package's types from depending on the types'
implementation details. Another use-case is the prevention of package users from accessing pack-
age state/data arbitrarily. The private reserved word splits the package spec into public and private
parts. For example:
[Ada]
Listing 3: types.ads
1 package Types is
2 type Type_1 is private;
3 type Type_2 is private;
4 type Type_3 is private;
5 procedure P (X : Type_1);
6 -- ...
7 private
8 procedure Q (Y : Type_1);
9 type Type_1 is new Integer range 1 .. 1000;
10 type Type_2 is array (Integer range 1 .. 1000) of Integer;
11 type Type_3 is record
12 A, B : Integer;
13 end record;
14 end Types;
Subprograms declared above the private separator (such as P) will be visible to the package user,
and the ones below (such as Q) will not. The body of the package, the implementation, has access
to both parts. A package specification does not require a private section.
Ada packages can be organized into hierarchies. A child unit can be declared in the following way:
[Ada]
-- root-child.ads
package Root.Child is
-- package spec goes here
end Root.Child;
-- root-child.adb
Here, Root.Child is a child package of Root. The public part of Root.Child has access to the
public part of Root. The private part of Child has access to the private part of Root, which is one
of the main advantages of child packages. However, there is no visibility relationship between the
two bodies. One common way to use this capability is to define subsystems around a hierarchical
naming scheme.
Entities declared in the visible part of a package specification can be made accessible using a with
clause that references the package, which is similar to the C #include directive. After a with
clause makes a package available, references to the package contents require the name of the
package as a prefix, with a dot after the package name. This prefix can be omitted if a use clause
is employed.
[Ada]
Listing 4: pck.ads
1 -- pck.ads
2
3 package Pck is
4 My_Glob : Integer;
5 end Pck;
Listing 5: main.adb
1 -- main.adb
2
3 with Pck;
4
5 procedure Main is
6 begin
7 Pck.My_Glob := 0;
8 end Main;
Build output
Compile
[Ada] main.adb
[Ada] pck.ads
Bind
[gprbind] main.bexch
[Ada] main.ali
Link
[link] main.adb
In contrast to C, the Ada with clause is a semantic inclusion mechanism rather than a text inclusion
mechanism; for more information on this difference please refer to Packages9 .
The following code samples are all equivalent, and illustrate the use of comments and working with
integer variables:
[C]
Listing 6: main.c
1 #include <stdio.h>
2
11 // regular addition
12 d = a + b + c;
13
17 return 0;
18 }
Runtime output
d = 101
[Ada]
Listing 7: main.adb
1 with Ada.Text_IO;
2
3 procedure Main
4 is
5 -- variable declaration
6 A, B : Integer := 0;
7 C : Integer := 100;
8 D : Integer;
9 begin
10 -- Ada does not have a shortcut format for increment like in C
11 A := A + 1;
12
13 -- regular addition
14 D := A + B + C;
15
Build output
Compile
[Ada] main.adb
Bind
[gprbind] main.bexch
[Ada] main.ali
Link
[link] main.adb
Runtime output
D = 101
You'll notice that, in both languages, statements are terminated with a semicolon. This means that
you can have multi-line statements.
In the Ada example above, there are two distinct sections to the procedure Main. This first section
is delimited by the is keyword and the begin keyword. This section is called the declarative block
of the subprogram. The declarative block is where you will define all the local variables which will be
used in the subprogram. C89 had something similar, where developers were required to declare
their variables at the top of the scope block. Most C developers may have run into this before when
trying to write a for loop:
[C]
Listing 8: main.c
1 /* The C89 version */
2
3 #include <stdio.h>
4
22 return 0;
23 }
Runtime output
Average: 3
[C]
Listing 9: main.c
1 // The modern C way
2
3 #include <stdio.h>
4
22 return 0;
23 }
Runtime output
Average: 3
For the fun of it, let's also see the Ada way to do this:
[Ada]
3 procedure Main is
4 type Int_Array is array (Natural range <>) of Integer;
5
Build output
Compile
[Ada] main.adb
Bind
[gprbind] main.bexch
[Ada] main.ali
Link
[link] main.adb
Runtime output
Average: 3
We will explore more about the syntax of loops in Ada in a future section of this course; but for
now, notice that the I variable used as the loop index is not declared in the declarative section!
The next block in the Ada example is between the begin and end keywords. This is where your
statements will live. You can create new scopes by using the declare keyword:
[Ada]
3 procedure Main
4 is
5 -- variable declaration
6 A, B : Integer := 0;
7 C : Integer := 100;
8 D : Integer;
9 begin
10 -- Ada does not have a shortcut format for increment like in C
11 A := A + 1;
12
13 -- regular addition
14 D := A + B + C;
15
19 declare
20 E : constant Integer := D * 100;
21 begin
22 -- printing the result
23 Ada.Text_IO.Put_Line ("E =" & E'Img);
24 end;
25
26 end Main;
Build output
Compile
[Ada] main.adb
Bind
[gprbind] main.bexch
[Ada] main.ali
Link
[link] main.adb
Runtime output
D = 101
E = 10100
Notice that we declared a new variable E whose scope only exists in our newly defined block. The
equivalent C code is:
[C]
11 // regular addition
12 d = a + b + c;
13
17 {
18 const int e = d * 100;
19 printf("e = %d\n", e);
20 }
21
22 return 0;
23 }
Runtime output
d = 101
e = 10100
Fun Fact about the C language assignment operator =: Did you know that an assignment in C can
be used in an expression? Let's look at an example:
[C]
7 if (a = 10)
8 printf("True\n");
9 else
10 printf("False\n");
11
12 return 0;
13 }
Runtime output
True
Run the above code example. What does it output? Is that what you were expecting?
The author of the above code example probably meant to test if a == 10 in the if statement but
accidentally typed = instead of ==. Because C treats assignment as an expression, it was able to
evaluate a = 10.
Let's look at the equivalent Ada code:
[Ada]
3 procedure Main
(continues on next page)
8 if A := 10 then
9 Put_Line ("True");
10 else
11 Put_Line ("False");
12 end if;
13 end Main;
The above code will not compile. This is because Ada does no allow assignment as an expression.
2.9 Conditions
9 if (v > 0) {
10 printf("Positive\n");
11 }
12 else if (v < 0) {
13 printf("Negative\n");
14 }
15 else {
16 printf("Zero\n");
17 }
18
19 return 0;
20 }
Runtime output
Zero
[Ada]
2.9. Conditions 19
Ada for the Embedded C Developer, Release 2021-06
3 procedure Main
4 is
5 -- try changing the initial value to change the
6 -- output of the program
7 V : constant Integer := 0;
8 begin
9 if V > 0 then
10 Put_Line ("Positive");
11 elsif V < 0 then
12 Put_Line ("Negative");
13 else
14 Put_Line ("Zero");
15 end if;
16 end Main;
Build output
Compile
[Ada] main.adb
Bind
[gprbind] main.bexch
[Ada] main.ali
Link
[link] main.adb
Runtime output
Zero
In Ada, everything that appears between the if and then keywords is the conditional expression,
no parentheses are required. Comparison operators are the same except for:
Operator C Ada
Equality == =
Inequality != /=
Not ! not
And && and
Or || or
9 switch(v) {
10 case 0:
11 printf("Zero\n");
(continues on next page)
25 return 0;
26 }
Runtime output
Zero
[Ada]
3 procedure Main
4 is
5 -- try changing the initial value to change the
6 -- output of the program
7 V : constant Integer := 0;
8 begin
9 case V is
10 when 0 =>
11 Put_Line ("Zero");
12 when 1 .. 9 =>
13 Put_Line ("Positive");
14 when 10 | 12 | 14 | 16 | 18 =>
15 Put_Line ("Even number between 10 and 18");
16 when others =>
17 Put_Line ("Something else");
18 end case;
19 end Main;
Build output
Compile
[Ada] main.adb
Bind
[gprbind] main.bexch
[Ada] main.ali
Link
[link] main.adb
Runtime output
Zero
Switch or Case?
A switch statement in C is the same as a case statement in Ada. This may be a little strange because
2.9. Conditions 21
Ada for the Embedded C Developer, Release 2021-06
C uses both keywords in the statement syntax. Let's make an analogy between C and Ada: C's
switch is to Ada's case as C's case is to Ada's when.
Notice that in Ada, the case statement does not use the break keyword. In C, we use break to
stop the execution of a case branch from falling through to the next branch. Here is an example:
[C]
7 switch(v) {
8 case 0:
9 printf("Zero\n");
10 case 1:
11 printf("One\n");
12 default:
13 printf("Other\n");
14 }
15
16 return 0;
17 }
Runtime output
Zero
One
Other
Run the above code with v = 0. What prints? What prints when we change the assignment to v
= 1?
When v = 0 the program outputs the strings Zero then One then Other. This is called fall through.
If you add the break statements back into the switch you can stop this fall through behavior from
happening. The reason why fall through is allowed in C is to allow the behavior from the previous
example where we want a specific branch to execute for multiple inputs. Ada solves this a different
way because it is possible, or even probable, that the developer might forget a break statement
accidentally. So Ada does not allow fall through. Instead, you can use Ada's syntax to identify
when a specific branch can be executed by more than one input. If you want a range of values for
a specific branch you can use the First .. Last notation. If you want a few non-consecutive
values you can use the Value1 | Value2 | Value3 notation.
Instead of using the word default to denote the catch-all case, Ada uses the others keyword.
2.10 Loops
50 return 0;
51 }
Runtime output
v = 128
v = 256
v = 30
v = 10
sum = 55
[Ada]
2.10. Loops 23
Ada for the Embedded C Developer, Release 2021-06
3 procedure Main is
4 V : Integer;
5 begin
6 -- this is a while loop
7 V := 1;
8 while V < 100 loop
9 V := V * 2;
10 end loop;
11 Ada.Text_IO.Put_Line ("V = " & Integer'Image (V));
12
Build output
Compile
[Ada] main.adb
Bind
[gprbind] main.bexch
[Ada] main.ali
Link
[link] main.adb
Runtime output
V = 128
V = 256
V = 30
V = 10
Sum = 55
The loop syntax in Ada is pretty straightforward. The loop and end loop keywords are used to
open and close the loop scope. Instead of using the break keyword to exit the loop, Ada has the
exit statement. The exit statement can be combined with a logic expression using the exit
when syntax.
The major deviation in loop syntax is regarding for loops. You'll notice, in C, that you sometimes
declare, and at least initialize a loop counter variable, specify a loop predicate, or an expression that
indicates when the loop should continue executing or complete, and last you specify an expression
to update the loop counter.
[C]
In Ada, you don't declare or initialize a loop counter or specify an update expression. You only
name the loop counter and give it a range to loop over. The loop counter is read-only! You cannot
modify the loop counter inside the loop like you can in C. And the loop counter will increment
consecutively along the specified range. But what if you want to loop over the range in reverse
order?
[C]
12 return 0;
13 }
Runtime output
10
9
8
7
6
5
4
3
2
1
0
[Ada]
2.10. Loops 25
Ada for the Embedded C Developer, Release 2021-06
3 procedure Main
4 is
5 My_Range : constant := 10;
6 begin
7 for I in reverse 0 .. My_Range loop
8 Put_Line (I'Img);
9 end loop;
10 end Main;
Build output
Compile
[Ada] main.adb
Bind
[gprbind] main.bexch
[Ada] main.ali
Link
[link] main.adb
Runtime output
10
9
8
7
6
5
4
3
2
1
0
Tick Image
Strangely enough, Ada people call the single apostrophe symbol, ', "tick". This "tick" says the we
are accessing an attribute of the variable. When we do 'Img on a variable of a numerical type, we
are going to return the string version of that numerical type. So in the for loop above, I'Img, or "I
tick image" will return the string representation of the numerical value stored in I. We have to do
this because Put_Line is expecting a string as an input parameter.
We'll discuss attributes in more details later in this chapter (page 41).
In the above example, we are traversing over the range in reverse order. In Ada, we use the re-
verse keyword to accomplish this.
In many cases, when we are writing a for loop, it has something to do with traversing an array. In
C, this is a classic location for off-by-one errors. Let's see an example in action:
[C]
19 return 0;
20 }
Runtime output
2 99 98 97 96 95 94 93 92 91 90 89 88 87 86 85 84 83 82 81 80 79 78 77 76 75 74 73␣
↪72 71 70 69 68 67 66 65 64 63 62 61 60 59 58 57 56 55 54 53 52 51 50 49 48 47 46␣
↪45 44 43 42 41 40 39 38 37 36 35 34 33 32 31 30 29 28 27 26 25 24 23 22 21 20 19␣
↪18 17 16 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1
[Ada]
3 procedure Main
4 is
5 type Int_Array is array (Natural range 1 .. 100) of Integer;
6
7 List : Int_Array;
8 begin
9
18 New_Line;
19 end Main;
Build output
Compile
[Ada] main.adb
Bind
[gprbind] main.bexch
[Ada] main.ali
Link
[link] main.adb
Runtime output
99 98 97 96 95 94 93 92 91 90 89 88 87 86 85 84 83 82 81 80 ␣
↪79 78 77 76 75 74 73 72 71 70 69 68 67 66 65 64 63 62 61 60page)
␣
(continues on next
↪59 58 57 56 55 54 53 52 51 50 49 48 47 46 45 44 43 42 41 40 ␣
↪39 38 37 36 35 34 33 32 31 30 29 28 27 26 25 24 23 22 21 20 ␣
↪19 Loops
2.10. 18 17 16 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0 27
Ada for the Embedded C Developer, Release 2021-06
The above Ada and C code should initialize an array using a for loop. The initial values in the array
should be contiguously decreasing from 99 to 0 as we index from the first index to the last index.
In other words, the first index has a value of 99, the next has 98, the next 97 ... the last has a value
of 0.
If you run both the C and Ada code above you'll notice that the outputs of the two programs are
different. Can you spot why?
In the C code there are two problems:
1. There's a buffer overflow in the first iteration of the loop. We would need to modify the loop
initialization to int i = LIST_LENGTH - 1;. The loop predicate should be modified to i
>= 0;
2. The C code also has another off-by-one problem in the math to compute the value stored in
list[i]. The expression should be changed to be list[i] = LIST_LENGTH - i - 1;.
These are typical off-by-one problems that plagues C programs. You'll notice that we didn't have
this problem with the Ada code because we aren't defining the loop with arbitrary numeric literals.
Instead we are accessing attributes of the array we want to manipulate and are using a keyword
to determine the indexing direction.
We can actually simplify the Ada for loop a little further using iterators:
[Ada]
3 procedure Main
4 is
5 type Int_Array is array (Natural range 1 .. 100) of Integer;
6
7 List : Int_Array;
8 begin
9
18 New_Line;
19 end Main;
Build output
Compile
[Ada] main.adb
Bind
[gprbind] main.bexch
[Ada] main.ali
Link
[link] main.adb
Runtime output
99 98 97 96 95 94 93 92 91 90 89 88 87 86 85 84 83 82 81 80 ␣
↪79 78 77 76 75 74 73 72 71 70 69 68 67 66 65 64 63 62 61 60 ␣
(continues on next page)
↪59 58 57 56 55 54 53 52 51 50 49 48 47 46 45 44 43 42 41 40 ␣
↪39 38 37 36 35 34 33 32 31 30 29 28 27 26 25 24 23 22 21 20 ␣
↪19 18 17 16 15 14 13 12 11 10 9 8 2.7 The
6 C5Developer's
4 3 2 1 0
28 Chapter Perspective on Ada
Ada for the Embedded C Developer, Release 2021-06
In the second for loop, we changed the syntax to for I of List. Instead of I being the index
counter, it is now an iterator that references the underlying element. This example of Ada code is
identical to the last bit of Ada code. We just used a different method to index over the second for
loop. There is no C equivalent to this Ada feature, but it is similar to C++'s range based for loop.
Ada is considered a "strongly typed" language. This means that the language does not define any
implicit type conversions. C does define implicit type conversions, sometimes referred to as integer
promotion. The rules for promotion are fairly straightforward in simple expressions but can get
confusing very quickly. Let's look at a typical place of confusion with implicit type conversion:
[C]
8 printf("Does a == b?\n");
9 if(a == b)
10 printf("Yes.\n");
11 else
12 printf("No.\n");
13
16 return 0;
17 }
Runtime output
Does a == b?
No.
a: 0x000000FF, b: 0xFFFFFFFF
Run the above code. You will notice that a != b! If we look at the output of the last printf
statement we will see the problem. a is an unsigned number where b is a signed number. We
stored a value of 0xFF in both variables, but a treated this as the decimal number 255 while b
treated this as the decimal number -1. When we compare the two variables, of course they aren't
equal; but that's not very intuitive. Let's look at the equivalent Ada example:
[Ada]
3 procedure Main
4 is
(continues on next page)
8 A : Char := 16#FF#;
9 B : Unsigned_Char := 16#FF#;
10 begin
11
14 if A = B then
15 Put_Line ("Yes");
16 else
17 Put_Line ("No");
18 end if;
19
20 end Main;
Compilation output
If you try to run this Ada example you will get a compilation error. This is because the compiler is
telling you that you cannot compare variables of two different types. We would need to explicitly
cast one side to make the comparison against two variables of the same type. By enforcing the
explicit cast we can't accidentally end up in a situation where we assume something will happen
implicitly when, in fact, our assumption is incorrect.
Another example: you can't divide an integer by a float. You need to perform the division operation
using values of the same type, so one value must be explicitly converted to match the type of the
other (in this case the more likely conversion is from integer to float). Ada is designed to guarantee
that what's done by the program is what's meant by the programmer, leaving as little room for
compiler interpretation as possible. Let's have a look at the following example:
[Ada]
3 procedure Strong_Typing is
4 Alpha : constant Integer := 1;
5 Beta : constant Integer := 10;
6 Result : Float;
7 begin
8 Result := Float (Alpha) / Float (Beta);
9
Build output
Compile
[Ada] strong_typing.adb
Bind
[gprbind] strong_typing.bexch
[Ada] strong_typing.ali
Link
[link] strong_typing.adb
Runtime output
1.00000E-01
[C]
10 printf("%f\n", result);
11 }
12
17 return 0;
18 }
Runtime output
0.000000
Are the three programs above equivalent? It may seem like Ada is just adding extra complexity by
forcing you to make the conversion from Integer to Float explicit. In fact, it significantly changes
the behavior of the computation. While the Ada code performs a floating point operation 1.0/10.0
and stores 0.1 in Result, the C version instead store 0.0 in result. This is because the C version
perform an integer operation between two integer variables: 1/10 is 0. The result of the integer
division is then converted to a float and stored. Errors of this sort can be very hard to locate in
complex pieces of code, and systematic specification of how the operation should be interpreted
helps to avoid this class of errors. If an integer division was actually intended in the Ada case, it is
still necessary to explicitly convert the final result to Float:
[Ada]
-- Perform an Integer division then convert to Float
Result := Float (Alpha / Beta);
3 procedure Strong_Typing is
4 Alpha : constant Integer := 1;
5 Beta : constant Integer := 10;
6 Result : Float;
7 begin
8 Result := Float (Alpha / Beta);
9
Build output
Compile
[Ada] strong_typing.adb
Bind
[gprbind] strong_typing.bexch
[Ada] strong_typing.ali
Link
[link] strong_typing.adb
Runtime output
0.00000E+00
The principal scalar types predefined by Ada are Integer, Float, Boolean, and Character.
These correspond to int, float, int (when used for Booleans), and char, respectively. The names
for these types are not reserved words; they are regular identifiers. There are other language-
defined integer and floating-point types as well. All have implementation-defined ranges and pre-
cision.
Ada's type system encourages programmers to think about data at a high level of abstraction. The
compiler will at times output a simple efficient machine instruction for a full line of source code
(and some instructions can be eliminated entirely). The careful programmer's concern that the
operation really makes sense in the real world would be satisfied, and so would the programmer's
concern about performance.
The next example below defines two different metrics: area and distance. Mixing these two metrics
must be done with great care, as certain operations do not make sense, like adding an area to
a distance. Others require knowledge of the expected semantics; for example, multiplying two
distances. To help avoid errors, Ada requires that each of the binary operators +, -, *, and / for
integer and floating-point types take operands of the same type and return a value of that type.
[Ada]
5 D1 : Distance := 2.0;
6 D2 : Distance := 3.0;
7 A : Area;
8 begin
9 D1 := D1 + D2; -- OK
10 D1 := D1 + A; -- NOT OK: incompatible types for "+"
11 A := D1 * D2; -- NOT OK: incompatible types for ":="
12 A := Area (D1 * D2); -- OK
13 end Main;
Compilation output
main.adb:10:13: error: invalid operand types for operator "+"
main.adb:10:13: error: left operand has type "Distance" defined at line 2
main.adb:10:13: error: right operand has type "Area" defined at line 3
main.adb:11:13: error: expected type "Area" defined at line 3
main.adb:11:13: error: found type "Distance" defined at line 2
Even though the Distance and Area types above are just Float, the compiler does not allow ar-
bitrary mixing of values of these different types. An explicit conversion (which does not necessarily
mean any additional object code) is necessary.
The predefined Ada rules are not perfect; they admit some problematic cases (for example multi-
plying two Distance yields a Distance) and prohibit some useful cases (for example multiplying
two Distances should deliver an Area). These situations can be handled through other mech-
anisms. A predefined operation can be identified as abstract to make it unavailable; overloading
can be used to give new interpretations to existing operator symbols, for example allowing an op-
erator to return a value from a type different from its operands; and more generally, GNAT has
introduced a facility that helps perform dimensionality checking.
Ada enumerations work similarly to C enum:
[Ada]
11 D : Day := Monday;
12 begin
13 null;
14 end Main;
Build output
Compile
[Ada] main.adb
Bind
[gprbind] main.bexch
[Ada] main.ali
Link
[link] main.adb
[C]
15 return 0;
16 }
But even though such enumerations may be implemented by the compiler as numeric values, at
the language level Ada will not confuse the fact that Monday is a Day and is not an Integer. You
can compare a Day with another Day, though. To specify implementation details like the numeric
values that correspond with enumeration values in C you include them in the original enum decla-
ration:
[C]
3 enum Day {
4 Monday = 10,
5 Tuesday = 11,
6 Wednesday = 12,
7 Thursday = 13,
8 Friday = 14,
9 Saturday = 15,
10 Sunday = 16
11 };
12
19 return 0;
20 }
Runtime output
d = 10
But in Ada you must use both a type definition for Day as well as a separate representation clause
for it like:
[Ada]
3 procedure Main is
4 type Day is
5 (Monday,
6 Tuesday,
7 Wednesday,
8 Thursday,
9 Friday,
10 Saturday,
(continues on next page)
23 D : Day := Monday;
24 V : Integer;
25 begin
26 V := Day'Enum_Rep (D);
27 Ada.Text_IO.Put_Line (Integer'Image (V));
28 end Main;
Build output
Compile
[Ada] main.adb
Bind
[gprbind] main.bexch
[Ada] main.ali
Link
[link] main.adb
Runtime output
10
Note that however, unlike C, values for enumerations in Ada have to be unique.
Contracts can be associated with types and variables, to refine values and define what are con-
sidered valid values. The most common kind of contract is a range constraint introduced with the
range reserved word, for example:
[Ada]
4 G1, G2 : Grade;
5 N : Integer;
6 begin
7 -- ... -- Initialization of N
8 G1 := 80; -- OK
9 G1 := N; -- Illegal (type mismatch)
10 G1 := Grade (N); -- Legal, run-time range check
11 G2 := G1 + 10; -- Legal, run-time range check
12 G1 := (G1 + G2) / 2; -- Legal, run-time range check
13 end Main;
Compilation output
In the above example, Grade is a new integer type associated with a range check. Range checks
are dynamic and are meant to enforce the property that no object of the given type can have a
value outside the specified range. In this example, the first assignment to G1 is correct and will not
raise a run-time exception. Assigning N to G1 is illegal since Grade is a different type than Integer.
Converting N to Grade makes the assignment legal, and a range check on the conversion confirms
that the value is within 0 .. 100. Assigning G1 + 10 to G2 is legal since + for Grade returns a
Grade (note that the literal 10 is interpreted as a Grade value in this context), and again there is a
range check.
The final assignment illustrates an interesting but subtle point. The subexpression G1 + G2 may
be outside the range of Grade, but the final result will be in range. Nevertheless, depending on
the representation chosen for Grade, the addition may overflow. If the compiler represents Grade
values as signed 8-bit integers (i.e., machine numbers in the range -128 .. 127) then the sum
G1 + G2 may exceed 127, resulting in an integer overflow. To prevent this, you can use explicit
conversions and perform the computation in a sufficiently large integer type, for example:
[Ada]
3 procedure Main is
4 type Grade is range 0 .. 100;
5
Build output
Compile
[Ada] main.adb
Bind
[gprbind] main.bexch
[Ada] main.ali
Link
[link] main.adb
Runtime output
99
Range checks are useful for detecting errors as early as possible. However, there may be some
impact on performance. Modern compilers do know how to remove redundant checks, and you
can deactivate these checks altogether if you have sufficient confidence that your code will function
correctly.
Types can be derived from the representation of any other type. The new derived type can be
associated with new constraints and operations. Going back to the Day example, one can write:
[Ada]
Build output
Compile
[Ada] main.adb
Bind
[gprbind] main.bexch
[Ada] main.ali
Link
[link] main.adb
Since these are new types, implicit conversions are not allowed. In this case, it's more natural to
create a new set of constraints for the same type, instead of making completely new ones. This
is the idea behind subtypes in Ada. A subtype is a type with optional additional constraints. For
example:
[Ada]
Build output
Compile
[Ada] main.adb
Bind
[gprbind] main.bexch
[Ada] main.ali
Link
[link] main.adb
These declarations don't create new types, just new names for constrained ranges of their base
types.
The purpose of numeric ranges is to express some application-specific constraint that we want the
compiler to help us enforce. More importantly, we want the compiler to tell us when that constraint
cannot be met — when the underlying hardware cannot support the range given. There are two
things to consider:
• just a range constraint, such as A : Integer range 0 .. 10;, or
• a type declaration, such as type Result is range 0 .. 1_000_000_000;.
Both represent some sort of application-specific constraint, but in addition, the type declaration
promotes portability because it won't compile on targets that do not have a sufficiently large hard-
ware numeric type. That's a definition of portability that is preferable to having something compile
anywhere but not run correctly, as in C.
Unsigned integer numbers are quite common in embedded applications. In C, you can use them
by declaring unsigned int variables. In Ada, you have two options:
• declare custom unsigned range types;
– In addition, you can declare custom range subtypes or use existing subtypes such as Nat-
ural.
• declare custom modular types.
The following table presents the main features of each type. We discuss these types right after.
When declaring custom range types in Ada, you may use the full range in the same way as in C. For
example, this is the declaration of a 32-bit unsigned integer type and the X variable in Ada:
[Ada]
3 procedure Main is
4 type Unsigned_Int_32 is range 0 .. 2 ** 32 - 1;
5
6 X : Unsigned_Int_32 := 42;
7 begin
8 Put_Line ("X = " & Unsigned_Int_32'Image (X));
9 end Main;
Build output
Compile
[Ada] main.adb
Bind
[gprbind] main.bexch
[Ada] main.ali
Link
[link] main.adb
Runtime output
X = 42
In C, when unsigned int has a size of 32 bits, this corresponds to the following declaration:
[C]
9 return 0;
10 }
Runtime output
x = 42
Another strategy is to declare subtypes for existing signed types and specify just the range that
excludes negative numbers. For example, let's declare a custom 32-bit signed type and its unsigned
subtype:
[Ada]
3 procedure Main is
4 type Signed_Int_32 is range -2 ** 31 .. 2 ** 31 - 1;
5
10 X : Unsigned_Int_31 := 42;
11 begin
12 Put_Line ("X = " & Unsigned_Int_31'Image (X));
13 end Main;
Build output
Compile
[Ada] main.adb
Bind
[gprbind] main.bexch
[Ada] main.ali
Link
[link] main.adb
Runtime output
X = 42
In this case, we're just skipping the sign bit of the Signed_Int_32 type. In other words, while
Signed_Int_32 has a size of 32 bits, Unsigned_Int_31 has a range of 31 bits, even if the base
type has 32 bits.
Note that the declaration above is actually similar to the existing Natural subtype. Ada provides
the following standard subtypes:
Since they're standard subtypes, you can declare variables of those subtypes directly in your im-
plementation, in the same way as you can declare Integer variables.
As indicated in the table above, however, there is a difference in behavior for the variables we just
declared, which occurs in case of overflow. Let's consider this C example:
[C]
11 return 0;
12 }
Runtime output
x = 0
3 procedure Main is
4 type Unsigned_Int_32 is range 0 .. 2 ** 32 - 1;
5
6 X : Unsigned_Int_32 := Unsigned_Int_32'Last + 1;
7 -- Overflow: exception is raised!
8 begin
9 Put_Line ("X = " & Unsigned_Int_32'Image (X));
10 end Main;
Build output
Compile
[Ada] main.adb
main.adb:6:48: warning: value not in range of type "Unsigned_Int_32" defined at␣
↪line 4 [enabled by default]
Bind
[gprbind] main.bexch
[Ada] main.ali
Link
[link] main.adb
Runtime output
While the C uses modulo arithmetic for unsigned integer, Ada doesn't use it for the Un-
signed_Int_32 type. Ada does, however, support modular types via type definitions using the
mod keyword. In this example, we declare a 32-bit modular type:
[Ada]
3 procedure Main is
4 type Unsigned_32 is mod 2**32;
5
6 X : Unsigned_32 := Unsigned_32'Last + 1;
7 -- Now: X = 0
8 begin
9 Put_Line ("X = " & Unsigned_32'Image (X));
10 end Main;
Build output
Compile
[Ada] main.adb
Bind
[gprbind] main.bexch
[Ada] main.ali
Link
[link] main.adb
Runtime output
X = 0
2.11.6 Attributes
Attributes start with a single apostrophe ("tick"), and they allow you to query properties of, and
perform certain actions on, declared entities such as types, objects, and subprograms. For exam-
ple, you can determine the first and last bounds of scalar types, get the sizes of objects and types,
and convert values to and from strings. This section provides an overview of how attributes work.
For more information on the many attributes defined by the language, you can refer directly to the
Ada Language Reference Manual.
The 'Image and 'Value attributes allow you to transform a scalar value into a String and vice-
versa. For example:
[Ada]
3 procedure Main is
4 A : Integer := 10;
5 begin
6 Put_Line (Integer'Image (A));
7 A := Integer'Value ("99");
8 Put_Line (Integer'Image (A));
9 end Main;
Build output
Compile
[Ada] main.adb
Bind
[gprbind] main.bexch
[Ada] main.ali
Link
[link] main.adb
Runtime output
10
99
Important
Semantically, attributes are equivalent to subprograms. For example, Integer'Image is defined
as follows:
Certain attributes are provided only for certain kinds of types. For example, the 'Val and 'Pos
attributes for an enumeration type associates a discrete value with its position among its peers.
One circuitous way of moving to the next character of the ASCII table is:
[Ada]
3 procedure Main is
4 C : Character := 'a';
5 begin
6 Put (C);
7 C := Character'Val (Character'Pos (C) + 1);
8 Put (C);
9 end Main;
Build output
Compile
[Ada] main.adb
Bind
[gprbind] main.bexch
[Ada] main.ali
(continues on next page)
Runtime output
ab
A more concise way to get the next value in Ada is to use the 'Succ attribute:
[Ada]
3 procedure Main is
4 C : Character := 'a';
5 begin
6 Put (C);
7 C := Character'Succ (C);
8 Put (C);
9 end Main;
Build output
Compile
[Ada] main.adb
Bind
[gprbind] main.bexch
[Ada] main.ali
Link
[link] main.adb
Runtime output
ab
You can get the previous value using the 'Pred attribute. Here is the equivalent in C:
[C]
10 return 0;
11 }
Runtime output
ab
Other interesting examples are the 'First and 'Last attributes which, respectively, return the
first and last values of a scalar type. Using 32-bit integers, for instance, Integer'First returns
−231 and Integer'Last returns 231 − 1.
C arrays are pointers with offsets, but the same is not the case for Ada. Arrays in Ada are not inter-
changeable with operations on pointers, and array types are considered first-class citizens. They
have dedicated semantics such as the availability of the array's boundaries at run-time. Therefore,
unhandled array overflows are impossible unless checks are suppressed. Any discrete type can
serve as an array index, and you can specify both the starting and ending bounds — the lower
bound doesn't necessarily have to be 0. Most of the time, array types need to be explicitly declared
prior to the declaration of an object of that array type.
Here's an example of declaring an array of 26 characters, initializing the values from 'a' to 'z':
[Ada]
3 procedure Main is
4 type Arr_Type is array (Integer range <>) of Character;
5 Arr : Arr_Type (1 .. 26);
6 C : Character := 'a';
7 begin
8 for I in Arr'Range loop
9 Arr (I) := C;
10 C := Character'Succ (C);
11
Build output
Compile
[Ada] main.adb
Bind
[gprbind] main.bexch
[Ada] main.ali
Link
[link] main.adb
Runtime output
a b c d e f g h i j k l m n o p q r s t u v w x y z
[C]
13 return 0;
14 }
Runtime output
a b c d e f g h i j k l m n o p q r s t u v w x y z
In C, only the size of the array is given during declaration. In Ada, array index ranges are specified
using two values of a discrete type. In this example, the array type declaration specifies the use
of Integer as the index type, but does not provide any constraints (use <>, pronounced box, to
specify "no constraints"). The constraints are defined in the object declaration to be 1 to 26, inclu-
sive. Arrays have an attribute called 'Range. In our example, Arr'Range can also be expressed
as Arr'First .. Arr'Last; both expressions will resolve to 1 .. 26. So the 'Range attribute
supplies the bounds for our for loop. There is no risk of stating either of the bounds incorrectly,
as one might do in C where I <= 26 may be specified as the end-of-loop condition.
As in C, Ada String is an array of Character. Ada strings, importantly, are not delimited with the
special character '\0' like they are in C. It is not necessary because Ada uses the array's bounds
to determine where the string starts and stops.
Ada's predefined String type is very straightforward to use:
[Ada]
3 procedure Main is
4 My_String : String (1 .. 19) := "This is an example!";
5 begin
6 Put_Line (My_String);
7 end Main;
Build output
Compile
[Ada] main.adb
Bind
[gprbind] main.bexch
[Ada] main.ali
Link
[link] main.adb
Runtime output
This is an example!
Unlike C, Ada does not offer escape sequences such as '\n'. Instead, explicit values from the ASCII
package must be concatenated (via the concatenation operator, &). Here for example, is how to
initialize a line of text ending with a new line:
[Ada]
3 procedure Main is
4 My_String : String := "This is a line" & ASCII.LF;
5 begin
6 Put (My_String);
7 end Main;
Build output
Compile
[Ada] main.adb
Bind
[gprbind] main.bexch
[Ada] main.ali
Link
[link] main.adb
Runtime output
This is a line
You see here that no constraints are necessary for this variable definition. The initial value given
allows the automatic determination of My_String's bounds.
Ada offers high-level operations for copying, slicing, and assigning values to arrays. We'll start
with assignment. In C, the assignment operator doesn't make a copy of the value of an array, but
only copies the address or reference to the target variable. In Ada, the actual array contents are
duplicated. To get the above behavior, actual pointer types would have to be defined and used.
[Ada]
3 procedure Main is
4 type Arr_Type is array (Integer range <>) of Integer;
5 A1 : Arr_Type (1 .. 2);
6 A2 : Arr_Type (1 .. 2);
7 begin
8 A1 (1) := 0;
9 A1 (2) := 1;
10
11 A2 := A1;
12
Build output
Compile
[Ada] main.adb
Bind
[gprbind] main.bexch
[Ada] main.ali
Link
[link] main.adb
Runtime output
0
1
[C]
9 A1 [0] = 0;
10 A1 [1] = 1;
11
18 return 0;
19 }
Runtime output
0
1
In all of the examples above, the source and destination arrays must have precisely the same num-
ber of elements. Ada allows you to easily specify a portion, or slice, of an array. So you can write
the following:
[Ada]
3 procedure Main is
4 type Arr_Type is array (Integer range <>) of Integer;
5 A1 : Arr_Type (1 .. 10) := (1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
6 A2 : Arr_Type (1 .. 5) := (1, 2, 3, 4, 5);
7 begin
8 A2 (1 .. 3) := A1 (4 .. 6);
9
Build output
Compile
[Ada] main.adb
Bind
[gprbind] main.bexch
[Ada] main.ali
Link
[link] main.adb
Runtime output
4
5
6
4
5
This assigns the 4th, 5th, and 6th elements of A1 into the 1st, 2nd, and 3rd elements of A2. Note
that only the length matters here: the values of the indexes don't have to be equal; they slide
automatically.
Ada also offers high level comparison operations which compare the contents of arrays as opposed
to their addresses:
[Ada]
3 procedure Main is
4 type Arr_Type is array (Integer range <>) of Integer;
5 A1 : Arr_Type (1 .. 2) := (10, 20);
6 A2 : Arr_Type (1 .. 2) := (10, 20);
7 begin
8 if A1 = A2 then
9 Put_Line ("A1 = A2");
10 else
11 Put_Line ("A1 /= A2");
12 end if;
13 end Main;
Build output
Compile
[Ada] main.adb
Bind
[gprbind] main.bexch
[Ada] main.ali
Link
[link] main.adb
Runtime output
A1 = A2
[C]
8 int eq = 1;
9
17 if (eq) {
18 printf("A1 == A2\n");
19 }
20 else {
21 printf("A1 != A2\n");
22 }
23
24 return 0;
25 }
Runtime output
A1 == A2
You can assign to all the elements of an array in each language in different ways. In Ada, the number
of elements to assign can be determined by looking at the right-hand side, the left-hand side, or
both sides of the assignment. When bounds are known on the left-hand side, it's possible to use
the others expression to define a default value for all the unspecified array elements. Therefore,
you can write:
[Ada]
3 procedure Main is
4 type Arr_Type is array (Integer range <>) of Integer;
5 A1 : Arr_Type (-2 .. 42) := (others => 0);
6 begin
7 -- use a slice to assign A1 elements 11 .. 19 to 1
8 A1 (11 .. 19) := (others => 1);
9
Build output
Compile
[Ada] main.adb
Bind
[gprbind] main.bexch
[Ada] main.ali
Link
[link] main.adb
Runtime output
---- A1 ----
-2 => 0
-1 => 0
0 => 0
1 => 0
2 => 0
(continues on next page)
In this example, we're specifying that A1 has a range between -2 and 42. We use (others => 0) to
initialize all array elements with zero. In the next example, the number of elements is determined
by looking at the right-hand side:
[Ada]
3 procedure Main is
4 type Arr_Type is array (Integer range <>) of Integer;
5 A1 : Arr_Type := (1, 2, 3, 4, 5, 6, 7, 8, 9);
6 begin
7 A1 := (1, 2, 3, others => 10);
8
Build output
Compile
[Ada] main.adb
Bind
[gprbind] main.bexch
[Ada] main.ali
Link
[link] main.adb
Runtime output
---- A1 ----
-2147483648 => 1
-2147483647 => 2
-2147483646 => 3
-2147483645 => 10
-2147483644 => 10
-2147483643 => 10
-2147483642 => 10
-2147483641 => 10
-2147483640 => 10
Since A1 is initialized with an aggregate of 9 elements, A1 automatically has 9 elements. Also, we're
not specifying any range in the declaration of A1. Therefore, the compiler uses the default range
of the underlying array type Arr_Type, which has an unconstrained range based on the Integer
type. The compiler selects the first element of that type (Integer'First) as the start index of A1.
If you replaced Integer range <> in the declaration of the Arr_Type by Positive range <>,
then A1's start index would be Positive'First — which corresponds to one.
The structure corresponding to a C struct is an Ada record. Here are some simple records:
[Ada]
3 procedure Main is
4 type R is record
5 A, B : Integer;
6 C : Float;
7 end record;
8
9 V : R;
10 begin
11 V.A := 0;
12 Put_Line ("V.A = " & Integer'Image (V.A));
13 end Main;
Build output
Compile
[Ada] main.adb
(continues on next page)
Runtime output
V.A = 0
[C]
3 struct R {
4 int A, B;
5 float C;
6 };
7
14 return 0;
15 }
Runtime output
V.A = 0
Ada allows specification of default values for fields just like C. The values specified can take the
form of an ordered list of values, a named list of values, or an incomplete list followed by others
=> <> to specify that fields not listed will take their default values. For example:
[Ada]
3 procedure Main is
4
5 type R is record
6 A, B : Integer := 0;
7 C : Float := 0.0;
8 end record;
9
23 begin
24 Put_R (V1, "V1");
25 Put_R (V2, "V2");
26 Put_R (V3, "V3");
27 Put_R (V4, "V4");
28 end Main;
Build output
Compile
[Ada] main.adb
Bind
[gprbind] main.bexch
[Ada] main.ali
Link
[link] main.adb
Runtime output
V1 = ( 1, 2, 1.00000E+00)
V2 = ( 1, 2, 1.00000E+00)
V3 = ( 1, 2, 1.00000E+00)
V4 = ( 0, 0, 1.00000E+00)
2.11.9 Pointers
As a foreword to the topic of pointers, it's important to keep in mind the fact that most situations
that would require a pointer in C do not in Ada. In the vast majority of cases, indirect memory
management can be hidden from the developer and thus saves from many potential errors. How-
ever, there are situation that do require the use of pointers, or said differently that require to make
memory indirection explicit. This section will present Ada access types, the equivalent of C point-
ers. A further section will provide more details as to how situations that require pointers in C can
be done without access types in Ada.
We'll continue this section by explaining the difference between objects allocated on the stack and
objects allocated on the heap using the following example:
[Ada]
3 procedure Main is
4 type R is record
5 A, B : Integer;
6 end record;
7
15 V1, V2 : R;
(continues on next page)
17 begin
18 V1.A := 0;
19 V2 := V1;
20 V2.A := 1;
21
Build output
Compile
[Ada] main.adb
Bind
[gprbind] main.bexch
[Ada] main.ali
Link
[link] main.adb
Runtime output
V1 = ( 0, 0)
V2 = ( 1, 0)
[C]
3 struct R {
4 int A, B;
5 };
6
20 print_r(&V1, "V1");
21 print_r(&V2, "V2");
22
23 return 0;
24 }
Runtime output
V1 = (0, 0)
V2 = (1, 0)
There are many commonalities between the Ada and C semantics above. In Ada and C, objects
are allocated on the stack and are directly accessed. V1 and V2 are two different objects and the
assignment statement copies the value of V1 into V2. V1 and V2 are two distinct objects.
3 procedure Main is
4 type R is record
5 A, B : Integer;
6 end record;
7
17 V1 : R_Access;
18 V2 : R_Access;
19 begin
20 V1 := new R;
21 V1.A := 0;
22 V2 := V1;
23 V2.A := 1;
24
Build output
Compile
[Ada] main.adb
Bind
[gprbind] main.bexch
[Ada] main.ali
Link
[link] main.adb
Runtime output
V1 = ( 1, 0)
V2 = ( 1, 0)
[C]
4 struct R {
5 int A, B;
6 };
7
22 print_r(V1, "V1");
23 print_r(V2, "V2");
24
25 return 0;
26 }
Runtime output
V1 = (1, 0)
V2 = (1, 0)
In this example, an object of type R is allocated on the heap. The same object is then referred to
through V1 and V2. As in C, there's no garbage collector in Ada, so objects allocated by the new
operator need to be expressly freed (which is not the case here).
Dereferencing is performed automatically in certain situations, for instance when it is clear that the
type required is the dereferenced object rather than the pointer itself, or when accessing record
members via a pointer. To explicitly dereference an access variable, append .all. The equivalent
of V1->A in C can be written either as V1.A or V1.all.A.
Pointers to scalar objects in Ada and C look like:
[Ada]
Build output
Compile
[Ada] main.adb
Bind
[gprbind] main.bexch
[Ada] main.ali
Link
[link] main.adb
[C]
Build output
Compile
[Ada] main.adb
Bind
[gprbind] main.bexch
[Ada] main.ali
Link
[link] main.adb
When using Ada pointers to reference objects on the stack, the referenced objects must be declared
as being aliased. This directs the compiler to implement the object using a memory region, rather
than using registers or eliminating it entirely via optimization. The access type needs to be declared
as either access all (if the referenced object needs to be assigned to) or access constant (if
the referenced object is a constant). The 'Access attribute works like the C & operator to get a
pointer to the object, but with a scope accessibility check to prevent references to objects that have
gone out of scope. For example:
[Ada]
Build output
Compile
[Ada] main.adb
Bind
[gprbind] main.bexch
[Ada] main.ali
Link
[link] main.adb
[C]
6 return 0;
7 }
To deallocate objects from the heap in Ada, it is necessary to use a deallocation subprogram that
accepts a specific access type. A generic procedure is provided that can be customized to fit your
needs, it's called Ada.Unchecked_Deallocation. To create your customized deallocator (that is,
to instantiate this generic), you must provide the object type as well as the access type as follows:
[Ada]
3 procedure Main is
4 type Integer_Access is access all Integer;
5 procedure Free is new Ada.Unchecked_Deallocation (Integer, Integer_Access);
6 My_Pointer : Integer_Access := new Integer;
7 begin
8 Free (My_Pointer);
9 end Main;
Build output
Compile
[Ada] main.adb
Bind
[gprbind] main.bexch
[Ada] main.ali
Link
[link] main.adb
[C]
8 return 0;
9 }
Subroutines in C are always expressed as functions which may or may not return a value. Ada
explicitly differentiates between functions and procedures. Functions must return a value and
procedures must not. Ada uses the more general term subprogram to refer to both functions and
procedures.
Parameters can be passed in three distinct modes:
• in, which is the default, is for input parameters, whose value is provided by the caller and
cannot be changed by the subprogram.
• out is for output parameters, with no initial value, to be assigned by the subprogram and
returned to the caller.
• in out is a parameter with an initial value provided by the caller, which can be modified
by the subprogram and returned to the caller (more or less the equivalent of a non-constant
pointer in C).
Ada also provides access and aliased parameters, which are in effect explicit pass-by-reference
indicators.
In Ada, the programmer specifies how the parameter will be used and in general the compiler
decides how it will be passed (i.e., by copy or by reference). C has the programmer specify how to
pass the parameter.
Important
There are some exceptions to the "general" rule in Ada. For example, parameters of scalar types
are always passed by copy, for all three modes.
3 procedure Proc
4 (Var1 : Integer;
5 Var2 : out Integer;
6 Var3 : in out Integer)
7 is
8 begin
9 Var2 := Func (Var1);
10 Var3 := Var3 + 1;
11 end Proc;
4 procedure Main is
5 V1, V2 : Integer;
6 begin
7 V2 := 2;
8 Proc (5, V1, V2);
9
Build output
Compile
[Ada] main.adb
[Ada] proc.adb
[Ada] func.adb
Bind
[gprbind] main.bexch
[Ada] main.ali
Link
[link] main.adb
Runtime output
V1: 6
V2: 3
[C]
3 void Proc
4 (int Var1,
5 int * Var2,
6 int * Var3)
7 {
8 *Var2 = Func (Var1);
9 *Var3 += 1;
10 }
8 v2 = 2;
9 Proc (5, &v1, &v2);
10
14 return 0;
15 }
Runtime output
v1: 6
v2: 3
The first two declarations for Proc and Func are specifications of the subprograms which are be-
ing provided later. Although optional here, it's still considered good practice to separately define
specifications and implementations in order to make it easier to read the program. In Ada and
C, a function that has not yet been seen cannot be used. Here, Proc can call Func because its
specification has been declared.
Parameters in Ada subprogram declarations are separated with semicolons, because commas are
reserved for listing multiple parameters of the same type. Parameter declaration syntax is the
same as variable declaration syntax (except for the modes), including default values for parame-
ters. If there are no parameters, the parentheses must be omitted entirely from both the declara-
tion and invocation of the subprogram.
In Ada 202X
Ada 202X allows for using static expression functions, which are evaluated at compile time. To
achieve this, we can use an aspect — we'll discuss aspects later in this chapter (page 65).
An expression function is static when the Static aspect is specified. For example:
procedure Main is
begin
null;
end Main;
In this example, we declare X1 using an expression. In the declaration of X2, we call the static
expression function If_Then_Else. Both X1 and X2 have the same constant value.
2.12.2 Overloading
In C, function names must be unique. Ada allows overloading, in which multiple subprograms can
share the same name as long as the subprogram signatures (the parameter types, and function
return types) are different. The compiler will be able to resolve the calls to the proper routines or
it will reject the calls. For example:
[Ada]
9 end Machine;
19 end Machine;
4 procedure Main is
5 S : Status;
6 C : Code;
7 T : Threshold;
8 begin
9 S := On;
10 C := Get (S);
11 T := Get (S);
12
Build output
Compile
[Ada] main.adb
[Ada] machine.adb
Bind
[gprbind] main.bexch
[Ada] main.ali
Link
[link] main.adb
Runtime output
S: ON
C: 3
T: 1.00000E+01
The Ada compiler knows that an assignment to C requires a Code value. So, it chooses the Get
function that returns a Code to satisfy this requirement.
Operators in Ada are functions too. This allows you to define local operators that override oper-
ators defined at an outer scope, and provide overloaded operators that operate on and compare
different types. To declare an operator as a function, enclose its "name" in quotes:
[Ada]
9 end Machine_2;
19 end Machine_2;
4 procedure Main is
5 I : Input;
6 begin
7 I := 3.0;
8 if I = Off then
9 Put_Line ("Machine is off.");
10 else
11 Put_Line ("Machine is not off.");
12 end if;
13 end Main;
Build output
Compile
[Ada] main.adb
[Ada] machine_2.adb
Bind
[gprbind] main.bexch
[Ada] main.ali
Link
[link] main.adb
Runtime output
2.12.3 Aspects
Aspect specifications allow you to define certain characteristics of a declaration using the with
keyword after the declaration:
procedure Some_Procedure is <procedure_definition>
with Some_Aspect => <aspect_specification>;
For example, you can inline a subprogram by specifying the Inline aspect:
[Ada]
8 end Float_Arrays;
9 end Float_Arrays;
Aspects and attributes might refer to the same kind of information. For example, we can use the
Size aspect to define the expected minimum size of objects of a certain type:
[Ada]
6 end My_Device_Types;
In the same way, we can use the size attribute to retrieve the size of a type or of an object:
[Ada]
5 procedure Show_Device_Types is
6 UInt10_Obj : constant UInt10 := 0;
7 begin
8 Put_Line ("Size of UInt10 type: " & Positive'Image (UInt10'Size));
9 Put_Line ("Size of UInt10 object: " & Positive'Image (UInt10_Obj'Size));
10 end Show_Device_Types;
We'll explain both Size aspect and Size attribute later in this course (page 95).
THREE
Concurrent and real-time programming are standard parts of the Ada language. As such, they have
the same semantics, whether executing on a native target with an OS such as Linux, on a real-time
operating system (RTOS) such as VxWorks, or on a bare metal target with no OS or RTOS at all.
For resource-constrained systems, two subsets of the Ada concurrency facilities are defined, known
as the Ravenscar and Jorvik profiles. Though restricted, these subsets have highly desirable prop-
erties, including: efficiency, predictability, analyzability, absence of deadlock, bounded blocking,
absence of priority inversion, a real-time scheduler, and a small memory footprint. On bare metal
systems, this means in effect that Ada comes with its own real-time kernel.
Enhanced portability and expressive power are the primary advantages of using the standard con-
currency facilities, potentially resulting in considerable cost savings. For example, with little effort,
it is possible to migrate from Windows to Linux to a bare machine without requiring any changes to
the code. Thread management and synchronization is all done by the implementation, transpar-
ently. However, in some situations, it’s critical to be able to access directly the services provided by
the platform. In this case, it’s always possible to make direct system calls from Ada code. Several
targets of the GNAT compiler provide this sort of API by default, for example win32ada for Windows
and Florist for POSIX systems.
On native and RTOS-based platforms GNAT typically provides the full concurrency facilities. In
contrast, on bare metal platforms GNAT typically provides the two standard subsets: Ravenscar
and Jorvik.
3.2 Tasks
Ada offers a high level construct called a task which is an independent thread of execution. In
GNAT, tasks are either mapped to the underlying OS threads, or use a dedicated kernel when not
available.
The following example will display the 26 letters of the alphabet twice, using two concurrent tasks.
Since there is no synchronization between the two threads of control in any of the examples, the
output may be interspersed.
[Ada]
67
Ada for the Embedded C Developer, Release 2021-06
Listing 1: main.adb
1 with Ada.Text_IO; use Ada.Text_IO;
2
6 task My_Task;
7
Build output
Compile
[Ada] main.adb
Bind
[gprbind] main.bexch
[Ada] main.ali
Link
[link] main.adb
Runtime output
ABCDEFGHIJKLMNOPQRSTUVWXYZ
ABCDEFGHIJKLMNOPQRSTUVWXYZ
Any number of Ada tasks may be declared in any declarative region. A task declaration is very
similar to a procedure or package declaration. They all start automatically when control reaches
the begin. A block will not exit until all sequences of statements defined within that scope, including
those in tasks, have been completed.
A task type is a generalization of a task object; each object of a task type has the same behavior. A
declared object of a task type is started within the scope where it is declared, and control does not
leave that scope until the task has terminated.
Task types can be parameterized; the parameter serves the same purpose as an argument to a
constructor in Java. The following example creates 10 tasks, each of which displays a subset of the
alphabet contained between the parameter and the 'Z' Character. As with the earlier example,
since there is no synchronization among the tasks, the output may be interspersed depending on
the underlying implementation of the task scheduling algorithm.
[Ada]
Listing 2: my_tasks.ads
1 package My_Tasks is
2
5 end My_Tasks;
Listing 3: my_tasks.adb
1 with Ada.Text_IO; use Ada.Text_IO;
2
13 end My_Tasks;
Listing 4: main.adb
1 with My_Tasks; use My_Tasks;
2
3 procedure Main is
4 Dummy_Tab : array (0 .. 3) of My_Task ('W');
5 begin
6 null;
7 end Main;
Build output
Compile
[Ada] main.adb
[Ada] my_tasks.adb
Bind
[gprbind] main.bexch
[Ada] main.ali
Link
[link] main.adb
Runtime output
WXYZ
WXYZ
WXYZ
WXYZ
In Ada, a task may be dynamically allocated rather than declared statically. The task will then start
as soon as it has been allocated, and terminates when its work is completed.
[Ada]
Listing 5: main.adb
1 with My_Tasks; use My_Tasks;
2
3 procedure Main is
4 type Ptr_Task is access My_Task;
5
6 T : Ptr_Task;
7 begin
8 T := new My_Task ('W');
9 end Main;
Build output
3.2. Tasks 69
Ada for the Embedded C Developer, Release 2021-06
Compile
[Ada] main.adb
[Ada] my_tasks.adb
Bind
[gprbind] main.bexch
[Ada] main.ali
Link
[link] main.adb
Runtime output
WXYZ
3.3 Rendezvous
A rendezvous is a synchronization between two tasks, allowing them to exchange data and coor-
dinate execution. Let's consider the following example:
[Ada]
Listing 6: main.adb
1 with Ada.Text_IO; use Ada.Text_IO;
2
3 procedure Main is
4
5 task After is
6 entry Go;
7 end After;
8
15 begin
16 Put_Line ("Before");
17 After.Go;
18 end Main;
Build output
Compile
[Ada] main.adb
Bind
[gprbind] main.bexch
[Ada] main.ali
Link
[link] main.adb
Runtime output
Before
After
The Go entry declared in After is the client interface to the task. In the task body, the accept
statement causes the task to wait for a call on the entry. This particular entry and accept pair
simply causes the task to wait until Main calls After.Go. So, even though the two tasks start
simultaneously and execute independently, they can coordinate via Go. Then, they both continue
execution independently after the rendezvous.
The entry/accept pair can take/pass parameters, and the accept statement can contain a se-
quence of statements; while these statements are executed, the caller is blocked.
Let's look at a more ambitious example. The rendezvous below accepts parameters and executes
some code:
[Ada]
Listing 7: main.adb
1 with Ada.Text_IO; use Ada.Text_IO;
2
3 procedure Main is
4
5 task After is
6 entry Go (Text : String);
7 end After;
8
16 begin
17 Put_Line ("Before");
18 After.Go ("Main");
19 end Main;
Build output
Compile
[Ada] main.adb
Bind
[gprbind] main.bexch
[Ada] main.ali
Link
[link] main.adb
Runtime output
Before
After: Main
In the above example, the Put_Line is placed in the accept statement. Here's a possible execution
trace, assuming a uniprocessor:
1. At the begin of Main, task After is started and the main procedure is suspended.
2. After reaches the accept statement and is suspended, since there is no pending call on the
Go entry.
3. The main procedure is awakened and executes the Put_Line invocation, displaying the string
"Before".
4. The main procedure calls the Go entry. Since After is suspended on its accept statement
for this entry, the call succeeds.
5. The main procedure is suspended, and the task After is awakened to execute the body of
the accept statement. The actual parameter "Main" is passed to the accept statement, and
the Put_Line invocation is executed. As a result, the string "After: Main" is displayed.
3.3. Rendezvous 71
Ada for the Embedded C Developer, Release 2021-06
6. When the accept statement is completed, both the After task and the main procedure are
ready to run. Suppose that the Main procedure is given the processor. It reaches its end, but
the local task After has not yet terminated. The main procedure is suspended.
7. The After task continues, and terminates since it is at its end. The main procedure is re-
sumed, and it too can terminate since its dependent task has terminated.
The above description is a conceptual model; in practice the implementation can perform various
optimizations to avoid unnecessary context switches.
The accept statement by itself can only wait for a single event (call) at a time. The select state-
ment allows a task to listen for multiple events simultaneously, and then to deal with the first event
to occur. This feature is illustrated by the task below, which maintains an integer value that is mod-
ified by other tasks that call Increment, Decrement, and Get:
[Ada]
Listing 8: counters.ads
1 package Counters is
2
3 task Counter is
4 entry Get (Result : out Integer);
5 entry Increment;
6 entry Decrement;
7 end Counter;
8
9 end Counters;
Listing 9: counters.adb
1 with Ada.Text_IO; use Ada.Text_IO;
2
29 end Counters;
4 procedure Main is
5 V : Integer;
6 begin
7 Put_Line ("Main started.");
8
9 Counter.Get (V);
10 Put_Line ("Got value. Value = " & Integer'Image (V));
11
12 Counter.Increment;
13 Put_Line ("Incremented value.");
14
15 Counter.Increment;
16 Put_Line ("Incremented value.");
17
18 Counter.Get (V);
19 Put_Line ("Got value. Value = " & Integer'Image (V));
20
21 Counter.Decrement;
22 Put_Line ("Decremented value.");
23
24 Counter.Get (V);
25 Put_Line ("Got value. Value = " & Integer'Image (V));
26
Build output
Compile
[Ada] main.adb
[Ada] counters.adb
Bind
[gprbind] main.bexch
[Ada] main.ali
Link
[link] main.adb
Runtime output
Main started.
Got value. Value = 0
Incremented value.
Incremented value.
Got value. Value = 2
Decremented value.
Got value. Value = 1
Main finished.
Exiting Counter task...
When the task's statement flow reaches the select, it will wait for all four events — three entries and
a delay — in parallel. If the delay of five seconds is exceeded, the task will execute the statements
following the delay statement (and in this case will exit the loop, in effect terminating the task).
The accept bodies for the Increment, Decrement, or Get entries will be otherwise executed as
they're called. These four sections of the select statement are mutually exclusive: at each iteration
of the loop, only one will be invoked. This is a critical point; if the task had been written as a pack-
age, with procedures for the various operations, then a race condition could occur where multiple
tasks simultaneously calling, say, Increment, cause the value to only get incremented once. In
the tasking version, if multiple tasks simultaneously call Increment then only one at a time will be
accepted, and the value will be incremented by each of the tasks when it is accepted.
More specifically, each entry has an associated queue of pending callers. If a task calls one of the
entries and Counter is not ready to accept the call (i.e., if Counter is not suspended at the select
statement) then the calling task is suspended, and placed in the queue of the entry that it is calling.
From the perspective of the Counter task, at any iteration of the loop there are several possibilities:
• There is no call pending on any of the entries. In this case Counter is suspended. It will be
awakened by the first of two events: a call on one of its entries (which will then be immediately
accepted), or the expiration of the five second delay (whose effect was noted above).
• There is a call pending on exactly one of the entries. In this case control passes to the select
branch with an accept statement for that entry.
• There are calls pending on more than one entry. In this case one of the entries with pending
callers is chosen, and then one of the callers is chosen to be de-queued. The choice of which
caller to accept depends on the queuing policy, which can be specified via a pragma defined
in the Real-Time Systems Annex of the Ada standard; the default is First-In First-Out.
Although the rendezvous may be used to implement mutually exclusive access to a shared data
object, an alternative (and generally preferable) style is through a protected object, an efficiently
implementable mechanism that makes the effect more explicit. A protected object has a public in-
terface (its protected operations) for accessing and manipulating the object's components (its pri-
vate part). Mutual exclusion is enforced through a conceptual lock on the object, and encapsulation
ensures that the only external access to the components are through the protected operations.
Two kinds of operations can be performed on such objects: read-write operations by procedures
or entries, and read-only operations by functions. The lock mechanism is implemented so that it's
possible to perform concurrent read operations but not concurrent write or read/write operations.
Let's reimplement our earlier tasking example with a protected object called Counter:
[Ada]
3 protected Counter is
4 function Get return Integer;
5 procedure Increment;
6 procedure Decrement;
7 private
8 Value : Integer := 0;
9 end Counter;
10
11 end Counters;
9 procedure Increment is
10 begin
11 Value := Value + 1;
12 end Increment;
13
14 procedure Decrement is
15 begin
16 Value := Value - 1;
17 end Decrement;
18 end Counter;
19
20 end Counters;
Having two completely different ways to implement the same paradigm might seem complicated.
However, in practice the actual problem to solve usually drives the choice between an active struc-
ture (a task) or a passive structure (a protected object).
A protected object can be accessed through prefix notation:
[Ada]
4 procedure Main is
5 begin
6 Counter.Increment;
7 Counter.Decrement;
8 Put_Line (Integer'Image (Counter.Get));
9 end Main;
Build output
Compile
[Ada] main.adb
[Ada] counters.adb
Bind
[gprbind] main.bexch
[Ada] main.ali
Link
[link] main.adb
Runtime output
A protected object may look like a package syntactically, since it contains declarations that can
be accessed externally using prefix notation. However, the declaration of a protected object is
extremely restricted; for example, no public data is allowed, no types can be declared inside, etc.
And besides the syntactic differences, there is a critical semantic distinction: a protected object has
a conceptual lock that guarantees mutual exclusion; there is no such lock for a package.
Like tasks, it's possible to declare protected types that can be instantiated several times:
declare
protected type Counter is
-- as above
end Counter;
C1 : Counter;
C2 : Counter;
begin
C1.Increment;
C2.Decrement;
.. .
end;
Protected objects and types can declare a procedure-like operation known as an entry. An entry
is somewhat similar to a procedure but includes a so-called barrier condition that must be true in
order for the entry invocation to succeed. Calling a protected entry is thus a two step process: first,
acquire the lock on the object, and then evaluate the barrier condition. If the condition is true then
the caller will execute the entry body. If the condition is false, then the caller is placed in the queue
for the entry, and relinquishes the lock. Barrier conditions (for entries with non-empty queues) are
reevaluated upon completion of protected procedures and protected entries.
Here's an example illustrating protected entries: a protected type that models a binary semaphore
/ persistent signal.
[Ada]
10 end Binary_Semaphores;
9 procedure Signal is
10 begin
11 Signaled := True;
12 end Signal;
13 end Binary_Semaphore;
14
15 end Binary_Semaphores;
4 procedure Main is
5 B : Binary_Semaphore;
6
7 task T1;
8 task T2;
9
10 task body T1 is
11 begin
12 Put_Line ("Task T1 waiting...");
13 B.Wait;
14
24 task body T2 is
25 begin
26 Put_Line ("Task T2 waiting...");
27 B.Wait;
28
38 begin
39 Put_Line ("Main started.");
40 B.Signal;
41 Put_Line ("Main finished.");
42 end Main;
Build output
Compile
[Ada] main.adb
[Ada] binary_semaphores.adb
Bind
[gprbind] main.bexch
[Ada] main.ali
Link
[link] main.adb
Runtime output
Task T1 waiting...
Main started.
Main finished.
(continues on next page)
Ada concurrency features provide much further generality than what's been presented here. For
additional information please consult one of the works cited in the References section.
3.6 Ravenscar
The Ravenscar profile is a subset of the Ada concurrency facilities that supports determinism,
schedulability analysis, constrained memory utilization, and certification to the highest integrity
levels. Four distinct application domains are intended:
• hard real-time applications requiring predictability,
• safety-critical systems requiring formal, stringent certification,
• high-integrity applications requiring formal static analysis and verification,
• embedded applications requiring both a small memory footprint and low execution over-
head.
Tasking constructs that preclude analysis, either technically or economically, are disallowed. You
can use the pragma Profile (Ravenscar) to indicate that the Ravenscar restrictions must be
observed in your program.
Some of the examples we've seen above will be rejected by the compiler when using the Ravenscar
profile. For example:
[Ada]
5 end My_Tasks;
13 end My_Tasks;
5 procedure Main is
6 Tab : array (0 .. 3) of My_Task ('W');
7 begin
8 null;
9 end Main;
Compilation output
This code violates the No_Task_Hierarchy restriction of the Ravenscar profile. This is due to the
declaration of Tab in the Main procedure. Ravenscar requires task declarations to be done at the
library level. Therefore, a simple solution is to create a separate package and reference it in the
main application:
[Ada]
3 package My_Task_Inst is
4
7 end My_Task_Inst;
3 with My_Task_Inst;
4
5 procedure Main is
6 begin
7 null;
8 end Main;
Build output
Compile
[Ada] main.adb
[Ada] my_task_inst.ads
[Ada] my_tasks.adb
Bind
[gprbind] main.bexch
[Ada] main.ali
Link
[link] main.adb
Runtime output
WXYZ
WXYZ
(continues on next page)
3.6. Ravenscar 79
Ada for the Embedded C Developer, Release 2021-06
Also, Ravenscar prohibits entries for tasks. For example, we're not allowed to write this declaration:
You can use, however, one entry per protected object. As an example, the declaration of the Bi-
nary_Semaphore type that we've discussed before compiles fine with Ravenscar:
We could add more procedures and functions to the declaration of Binary_Semaphore, but we
wouldn't be able to add another entry when using Ravenscar.
Similar to the previous example with the task array declaration, objects of Binary_Semaphore
cannot be declared in the main application:
procedure Main is
B : Binary_Semaphore;
begin
null;
end Main;
This violates the No_Local_Protected_Objects restriction. Again, Ravenscar expects this declaration
to be done on a library level, so a solution to make this code compile is to have this declaration in
a separate package and reference it in the Main procedure.
Ravenscar offers many additional restrictions. Covering those would exceed the scope of this chap-
ter. You can find more examples using the Ravenscar profile on this blog post10 .
10 https://blog.adacore.com/theres-a-mini-rtos-in-my-language
FOUR
Ada supports a high level of abstractness and expressiveness. In some cases, the compiler trans-
lates those constructs directly into machine code. However, there are many high-level constructs
for which a direct compilation would be difficult. In those cases, the compiler links to a library
containing an implementation of those high-level constructs: this is the so-called run-time library.
One typical example of high-level constructs that can be cumbersome for direct machine code
generation is Ada source-code using tasking. In this case, linking to a low-level implementation of
multithreading support — for example, an implementation using POSIX threads — is more straight-
forward than trying to make the compiler generate all the machine code.
In the case of GNAT, the run-time library is implemented using both C and Ada source-code. Also,
depending on the operating system, the library will interface with low-level functionality from the
target operating system.
There are basically two types of run-time libraries:
• the standard run-time library: in many cases, this is the run-time library available on desktop
operating systems or on some embedded platforms (such as ARM-Linux on a Raspberry-Pi).
• the configurable run-time library: this is a capability that is used to create custom run-time
libraries for specific target devices.
Configurable run-time libraries are usually used for constrained target devices where support for
the full library would be difficult or even impossible. In this case, configurable run-time libraries
may support just a subset of the full Ada language. There are many reasons that speak for this
approach:
• Some aspects of the Ada language may not translate well to limited operating systems.
• Memory constraints may require reducing the size of the run-time library, so that developers
may need to replace or even remove parts of the library.
• When certification is required, those parts of the library that would require too much certifi-
cation effort can be removed.
When using a configurable run-time library, the compiler checks whether the library supports cer-
tain features of the language. If a feature isn't supported, the compiler will give an error message.
You can find further information about the run-time library on this chapter of the GNAT User's
Guide Supplement for Cross Platforms11
11 https://docs.adacore.com/gnat_ugx-docs/html/gnat_ugx/gnat_ugx/the_gnat_configurable_run_time_facility.html
81
Ada for the Embedded C Developer, Release 2021-06
We've seen in the previous chapters how Ada can be used to describe high level semantics and
architecture. The beauty of the language, however, is that it can be used all the way down to the
lowest levels of the development, including embedded assembly code or bit-level data manage-
ment.
One very interesting feature of the language is that, unlike C, for example, there are no data rep-
resentation constraints unless specified by the developer. This means that the compiler is free to
choose the best trade-off in terms of representation vs. performance. Let's start with the following
example:
[Ada]
type R is record
V : Integer range 0 .. 255;
B1 : Boolean;
B2 : Boolean;
end record
with Pack;
[C]
struct R {
unsigned int v:8;
bool b1;
bool b2;
};
The Ada and the C code above both represent efforts to create an object that's as small as possible.
Controlling data size is not possible in Java, but the language does specify the size of values for the
primitive types.
Although the C and Ada code are equivalent in this particular example, there's an interesting se-
mantic difference. In C, the number of bits required by each field needs to be specified. Here, we're
stating that v is only 8 bits, effectively representing values from 0 to 255. In Ada, it's the other way
around: the developer specifies the range of values required and the compiler decides how to
represent things, optimizing for speed or size. The Pack aspect declared at the end of the record
specifies that the compiler should optimize for size even at the expense of decreased speed in ac-
cessing record components. We'll see more details about the Pack aspect in the sections about
bitwise operations (page 135) and mapping structures to bit-fields (page 137) in chapter 6.
Other representation clauses can be specified as well, along with compile-time consistency checks
between requirements in terms of available values and specified sizes. This is particularly useful
when a specific layout is necessary; for example when interfacing with hardware, a driver, or a com-
munication protocol. Here's how to specify a specific data layout based on the previous example:
[Ada]
type R is record
V : Integer range 0 .. 255;
B1 : Boolean;
B2 : Boolean;
end record;
We omit the with Pack directive and instead use a record representation clause following the
record declaration. The compiler is directed to spread objects of type R across two bytes. The
layout we're specifying here is fairly inefficient to work with on any machine, but you can have the
compiler construct the most efficient methods for access, rather than coding your own machine-
dependent bit-level methods manually.
When performing low-level development, such as at the kernel or hardware driver level, there can
be times when it is necessary to implement functionality with assembly code.
Every Ada compiler has its own conventions for embedding assembly code, based on the hardware
platform and the supported assembler(s). Our examples here will work with GNAT and GCC on the
x86 architecture.
All x86 processors since the Intel Pentium offer the rdtsc instruction, which tells us the number
of cycles since the last processor reset. It takes no inputs and places an unsigned 64-bit value split
between the edx and eax registers.
GNAT provides a subprogram called System.Machine_Code.Asm that can be used for assembly
code insertion. You can specify a string to pass to the assembler as well as source-level variables
to be used for input and output:
[Ada]
Listing 1: get_processor_cycles.adb
1 with System.Machine_Code; use System.Machine_Code;
2 with Interfaces; use Interfaces;
3
14 Counter :=
15 Unsigned_64 (High) * 2 ** 32 +
16 Unsigned_64 (Low);
17
18 return Counter;
19 end Get_Processor_Cycles;
We set the Volatile parameter to True to tell the compiler that invoking this instruction multiple
times with the same inputs can result in different outputs. This eliminates the possibility that the
compiler will optimize multiple invocations into a single call.
With optimization turned on, the GNAT compiler is smart enough to use the eax and edx registers
to implement the High and Low variables, resulting in zero overhead for the assembly interface.
The machine code insertion interface provides many features beyond what was shown here. More
information can be found in the GNAT User's Guide, and the GNAT Reference manual.
Handling interrupts is an important aspect when programming embedded devices. Interrupts are
used, for example, to indicate that a hardware or software event has happened. Therefore, by
handling interrupts, an application can react to external events.
Ada provides built-in support for handling interrupts. We can process interrupts by attaching a
handler — which must be a protected procedure — to it. In the declaration of the protected pro-
cedure, we use the Attach_Handler aspect and indicate which interrupt we want to handle.
Let's look into a code example that traps the quit interrupt (SIGQUIT) on Linux:
[Ada]
Listing 2: signal_handlers.ads
1 with System.OS_Interface;
2
3 package Signal_Handlers is
4
10 --
11 -- Declaration of an interrupt handler for the "quit" interrupt:
12 --
13 procedure Handle_Quit_Signal
14 with Attach_Handler => System.OS_Interface.SIGQUIT;
15 end Quit_Handler;
16
17 end Signal_Handlers;
Listing 3: signal_handlers.adb
1 with Ada.Text_IO; use Ada.Text_IO;
2
10 procedure Handle_Quit_Signal is
11 begin
12 Put_Line ("Quit request detected!");
13 Quit_Request := True;
14 end Handle_Quit_Signal;
(continues on next page)
16 end Quit_Handler;
17
18 end Signal_Handlers;
Listing 4: test_quit_handler.adb
1 with Ada.Text_IO; use Ada.Text_IO;
2 with Signal_Handlers;
3
4 procedure Test_Quit_Handler is
5 Quit : Signal_Handlers.Quit_Handler;
6
7 begin
8 while True loop
9 delay 1.0;
10 exit when Quit.Requested;
11 end loop;
12
The specification of the Signal_Handlers package from this example contains the declaration of
Quit_Handler, which is a protected type. In the private part of this protected type, we declare
the Handle_Quit_Signal procedure. By using the Attach_Handler aspect in the declaration
of Handle_Quit_Signal and indicating the quit interrupt (System.OS_Interface.SIGQUIT),
we're instructing the operating system to call this procedure for any quit request. So when the
user presses CTRL+\ on their keyboard, for example, the application will behave as follows:
• the operating system calls the Handle_Quit_Signal procedure , which displays a message
to the user ("Quit request detected!") and sets a Boolean variable — Quit_Request, which is
declared in the Quit_Handler type;
• the main application checks the status of the quit handler by calling the Requested function
as part of the while True loop;
– This call is in the exit when Quit.Requested line.
– The Requested function returns True in this case because the Quit_Request flag was
set by the Handle_Quit_Signal procedure.
• the main applications exits the loop, displays a message and finishes.
Note that the code example above isn't portable because it makes use of interrupts from the Linux
operating system. When programming embedded devices, we would use instead the interrupts
available on those specific devices.
Also note that, in the example above, we're declaring a static handler at compilation time. If you
need to make use of dynamic handlers, which can be configured at runtime, you can use the sub-
programs from the Ada.Interrupts package. This package includes not only a version of At-
tach_Handler as a procedure, but also other procedures such as:
• Exchange_Handler, which lets us exchange, at runtime, the current handler associated with
a specific interrupt by a different handler;
• Detach_Handler, which we can use to remove the handler currently associated with a given
interrupt.
Details about the Ada.Interrupts package are out of scope for this course. We'll discuss them
in a separate, more advanced course in the future. You can find some information about it in the
Interrupts appendix of the Ada Reference Manual12 .
12 https://www.adaic.org/resources/add_content/standards/12aarm/html/AA-C-3-2.html
Many numerical applications typically use floating-point types to compute values. However, in
some platforms, a floating-point unit may not be available. Other platforms may have a floating-
point unit, but using it in certain numerical algorithms can be prohibitive in terms of performance.
For those cases, fixed-point arithmetic can be a good alternative.
The difference between fixed-point and floating-point types might not be so obvious when looking
at this code snippet:
[Ada]
Listing 5: fixed_definitions.ads
1 package Fixed_Definitions is
2
7 end Fixed_Definitions;
Listing 6: show_float_and_fixed_point.adb
1 with Ada.Text_IO; use Ada.Text_IO;
2
5 procedure Show_Float_And_Fixed_Point is
6 Float_Value : Float := 0.25;
7 Fixed_Value : Fixed := 0.25;
8 begin
9
Build output
Compile
[Ada] show_float_and_fixed_point.adb
[Ada] fixed_definitions.ads
Bind
[gprbind] show_float_and_fixed_point.bexch
[Ada] show_float_and_fixed_point.ali
Link
[link] show_float_and_fixed_point.adb
Runtime output
Float_Value = 5.00000E-01
Fixed_Value = 0.5000000000
In this example, the application will show the value 0.5 for both Float_Value and Fixed_Value.
The major difference between floating-point and fixed-point types is in the way the values are
stored. Values of ordinary fixed-point types are, in effect, scaled integers. The scaling used for
ordinary fixed-point types is defined by the type's small, which is derived from the specified delta
and, by default, is a power of two. Therefore, ordinary fixed-point types are sometimes called
binary fixed-point types. In that sense, ordinary fixed-point types can be thought of being close
to the actual representation on the machine. In fact, ordinary fixed-point types make use of the
available integer shift instructions, for example.
Another difference between floating-point and fixed-point types is that Ada doesn't provide stan-
dard fixed-point types — except for the Duration type, which is used to represent an interval
of time in seconds. While the Ada standard specifies floating-point types such as Float and
Long_Float, we have to declare our own fixed-point types. Note that, in the previous example,
we have used a fixed-point type named Fixed: this type isn't part of the standard, but must be
declared somewhere in the source-code of our application.
The syntax for an ordinary fixed-point type is
By default, the compiler will choose a scale factor, or small, that is a power of 2 no greater than
<delta_value>.
For example, we may define a normalized range between -1.0 and 1.0 as following:
[Ada]
Listing 7: normalized_fixed_point_type.adb
1 with Ada.Text_IO; use Ada.Text_IO;
2
3 procedure Normalized_Fixed_Point_Type is
4 D : constant := 2.0 ** (-31);
5 type TQ31 is delta D range -1.0 .. 1.0 - D;
6 begin
7 Put_Line ("TQ31 requires " & Integer'Image (TQ31'Size) & " bits");
8 Put_Line ("The delta value of TQ31 is " & TQ31'Image (TQ31'Delta));
9 Put_Line ("The minimum value of TQ31 is " & TQ31'Image (TQ31'First));
10 Put_Line ("The maximum value of TQ31 is " & TQ31'Image (TQ31'Last));
11 end Normalized_Fixed_Point_Type;
Build output
Compile
[Ada] normalized_fixed_point_type.adb
Bind
[gprbind] normalized_fixed_point_type.bexch
[Ada] normalized_fixed_point_type.ali
Link
[link] normalized_fixed_point_type.adb
Runtime output
In this example, we are defining a 32-bit fixed-point data type for our normalized range. When
running the application, we notice that the upper bound is close to one, but not exactly one. This
is a typical effect of fixed-point data types — you can find more details in this discussion about the
Q format13 . We may also rewrite this code with an exact type definition:
[Ada]
13 https://en.wikipedia.org/wiki/Q_(number_format)
Listing 8: normalized_adapted_fixed_point_type.ads
1 package Normalized_Adapted_Fixed_Point_Type is
2
3 type TQ31 is delta 2.0 ** (-31) range -1.0 .. 1.0 - 2.0 ** (-31);
4
5 end Normalized_Adapted_Fixed_Point_Type;
Listing 9: custom_fixed_point_range.adb
1 with Ada.Text_IO; use Ada.Text_IO;
2 with Ada.Numerics; use Ada.Numerics;
3
4 procedure Custom_Fixed_Point_Range is
5 type Inv_Trig is delta 2.0 ** (-15) * Pi range -Pi / 2.0 .. Pi / 2.0;
6 begin
7 Put_Line ("Inv_Trig requires " & Integer'Image (Inv_Trig'Size)
8 & " bits");
9 Put_Line ("The delta value of Inv_Trig is "
10 & Inv_Trig'Image (Inv_Trig'Delta));
11 Put_Line ("The minimum value of Inv_Trig is "
12 & Inv_Trig'Image (Inv_Trig'First));
13 Put_Line ("The maximum value of Inv_Trig is "
14 & Inv_Trig'Image (Inv_Trig'Last));
15 end Custom_Fixed_Point_Range;
Build output
Compile
[Ada] custom_fixed_point_range.adb
Bind
[gprbind] custom_fixed_point_range.bexch
[Ada] custom_fixed_point_range.ali
Link
[link] custom_fixed_point_range.adb
Runtime output
In this example, we are defining a 16-bit type called Inv_Trig, which has a range from −𝜋/2 to
𝜋/2.
All standard operations are available for fixed-point types. For example:
[Ada]
3 procedure Fixed_Point_Op is
4 type TQ31 is delta 2.0 ** (-31) range -1.0 .. 1.0 - 2.0 ** (-31);
5
6 A, B, R : TQ31;
(continues on next page)
Build output
Compile
[Ada] fixed_point_op.adb
Bind
[gprbind] fixed_point_op.bexch
[Ada] fixed_point_op.ali
Link
[link] fixed_point_op.adb
Runtime output
R is 0.7500000000
4 #define SHIFT_FACTOR 32
5
31 printf("Original value\n");
32 display_fixed(fixed_value);
33
34 printf("... + 0.25\n");
35 fixed_value = add(fixed_value, TO_FIXED(0.25));
36 display_fixed(fixed_value);
37
38 printf("... * 0.5\n");
39 fixed_value = mult(fixed_value, TO_FIXED(0.5));
40 display_fixed(fixed_value);
41
42 return 0;
43 }
Runtime output
Original value
value (integer) = 536870912
value (float) = 0.25000
... + 0.25
value (integer) = 1073741824
value (float) = 0.50000
... * 0.5
value (integer) = 536870912
value (float) = 0.25000
Here, we declare the fixed-point type fixed based on int and two operations for it: addition (via
the add function) and multiplication (via the mult function). Note that, while fixed-point addition is
quite straightforward, multiplication requires right-shifting to match the correct internal represen-
tation. In Ada, since fixed-point operations are part of the language specification, they don't need
to be emulated. Therefore, no extra effort is required from the programmer.
Also note that the example above is very rudimentary, so it doesn't take some of the side-effects of
fixed-point arithmetic into account. In C, you have to manually take all side-effects deriving from
fixed-point arithmetic into account, while in Ada, the compiler takes care of selecting the right
operations for you.
Ada has built-in support for handling both volatile and atomic data. Let's start by discussing volatile
objects.
4.5.1 Volatile
A volatile14 object can be described as an object in memory whose value may change between
two consecutive memory accesses of a process A — even if process A itself hasn't changed the
value. This situation may arise when an object in memory is being shared by multiple threads. For
example, a thread B may modify the value of that object between two read accesses of a thread
A. Another typical example is the one of memory-mapped I/O15 , where the hardware might be
constantly changing the value of an object in memory.
Because the value of a volatile object may be constantly changing, a compiler cannot generate
code that stores the value of that object into a register and use the value from the register in
subsequent operations. Storing into a register is avoided because, if the value is stored there, it
would be outdated if another process had changed the volatile object in the meantime. Instead,
the compiler generates code in such a way that the process must read the value of the volatile
object from memory for each access.
Let's look at a simple example of a volatile variable in C:
[C]
14 return 0;
15 }
Runtime output
val: 999000.000
In this example, val has the modifier volatile, which indicates that the compiler must handle
val as a volatile object. Therefore, each read and write access in the loop is performed by accessing
the value of val in then memory.
This is the corresponding implementation in Ada:
[Ada]
3 procedure Show_Volatile_Object is
4 Val : Long_Float with Volatile;
5 begin
6 Val := 0.0;
7 for I in 0 .. 999 loop
8 Val := Val + 2.0 * Long_Float (I);
(continues on next page)
14 https://en.wikipedia.org/wiki/Volatile_(computer_programming)
15 https://en.wikipedia.org/wiki/Memory-mapped_I/O
Build output
Compile
[Ada] show_volatile_object.adb
Bind
[gprbind] show_volatile_object.bexch
[Ada] show_volatile_object.ali
Link
[link] show_volatile_object.adb
Runtime output
Val: 9.99000000000000E+05
In this example, Val has the Volatile aspect, which makes the object volatile. We can also use
the Volatile aspect in type declarations. For example:
[Ada]
3 procedure Show_Volatile_Type is
4 type Volatile_Long_Float is new Long_Float with Volatile;
5
6 Val : Volatile_Long_Float;
7 begin
8 Val := 0.0;
9 for I in 0 .. 999 loop
10 Val := Val + 2.0 * Volatile_Long_Float (I);
11 end loop;
12
Build output
Compile
[Ada] show_volatile_type.adb
Bind
[gprbind] show_volatile_type.bexch
[Ada] show_volatile_type.ali
Link
[link] show_volatile_type.adb
Runtime output
Val: 9.99000000000000E+05
Here, we're declaring a new type Volatile_Long_Float based on the Long_Float type and
using the Volatile aspect. Any object of this type is automatically volatile.
In addition to that, we can declare components of an array to be volatile. In this case, we can use
the Volatile_Components aspect in the array declaration. For example:
[Ada]
3 procedure Show_Volatile_Array_Components is
4 Arr : array (1 .. 2) of Long_Float with Volatile_Components;
5 begin
6 Arr := (others => 0.0);
7
Build output
Compile
[Ada] show_volatile_array_components.adb
Bind
[gprbind] show_volatile_array_components.bexch
[Ada] show_volatile_array_components.ali
Link
[link] show_volatile_array_components.adb
Runtime output
Note that it's possible to use the Volatile aspect for the array declaration as well:
[Ada]
4.5.2 Atomic
An atomic object is an object that only accepts atomic reads and updates. The Ada standard speci-
fies that "for an atomic object (including an atomic component), all reads and updates of the object
as a whole are indivisible." In this case, the compiler must generate Assembly code in such a way
that reads and updates of an atomic object must be done in a single instruction, so that no other
instruction could execute on that same object before the read or update completes.
In other contexts
Generally, we can say that operations are said to be atomic when they can be completed without
interruptions. This is an important requirement when we're performing operations on objects in
memory that are shared between multiple processes.
This definition of atomicity above is used, for example, when implementing databases. However,
for this section, we're using the term "atomic" differently. Here, it really means that reads and
updates must be performed with a single Assembly instruction.
For example, if we have a 32-bit object composed of four 8-bit bytes, the compiler cannot generate
code to read or update the object using four 8-bit store / load instructions, or even two 16-bit store
/ load instructions. In this case, in order to maintain atomicity, the compiler must generate code
using one 32-bit store / load instruction.
Because of this strict definition, we might have objects for which the Atomic aspect cannot be
specified. Lots of machines support integer types that are larger than the native word-sized integer.
For example, a 16-bit machine probably supports both 16-bit and 32-bit integers, but only 16-bit
integer objects can be marked as atomic — or, more generally, only objects that fit into at most 16
bits.
Atomicity may be important, for example, when dealing with shared hardware registers. In fact,
for certain architectures, the hardware may require that memory-mapped registers are handled
atomically. In Ada, we can use the Atomic aspect to indicate that an object is atomic. This is how
we can use the aspect to declare a shared hardware register:
[Ada]
3 procedure Show_Shared_HW_Register is
4 R : Integer
5 with Atomic, Address => System'To_Address (16#FFFF00A0#);
6 begin
7 null;
8 end Show_Shared_HW_Register;
Note that the Address aspect allows for assigning a variable to a specific location in the memory. In
this example, we're using this aspect to specify the address of the memory-mapped register. We'll
discuss more about the Address aspect later in the section about mapping structures to bit-fields
(page 137) (in chapter 6).
In addition to atomic objects, we can declare atomic types and atomic array components — similarly
to what we've seen before for volatile objects. For example:
[Ada]
3 procedure Show_Shared_HW_Register is
4 type Atomic_Integer is new Integer with Atomic;
5
In this example, we're declaring the Atomic_Integer type, which is an atomic type. Objects of
this type — such as R in this example — are automatically atomic. This example also includes the
declaration of the Arr array, which has atomic components.
Previously, we've seen that we can use representation clauses (page 82) to specify a particular layout
for a record type. As mentioned before, this is useful when interfacing with hardware, drivers,
or communication protocols. In this section, we'll extend this concept for two specific use-cases:
register overlays and data streams. Before we discuss those use-cases, though, we'll first explain
the Size aspect and the Size attribute.
The Size aspect indicates the minimum number of bits required to represent an object. When
applied to a type, the Size aspect is telling the compiler to not make record or array components
of a type T any smaller than X bits. Therefore, a common usage for this aspect is to just confirm
expectations: developers specify 'Size to tell the compiler that T should fit X bits, and the compiler
will tell them if they are right (or wrong).
When the specified size value is larger than necessary, it can cause objects to be bigger in memory
than they would be otherwise. For example, for some enumeration types, we could say for type
Enum'Size use 32; when the number of literals would otherwise have required only a byte.
That's useful for unchecked conversions because the sizes of the two types need to be the same.
Likewise, it's useful for interfacing with C, where enum types are just mapped to the int type, and
thus larger than Ada might otherwise require. We'll discuss unchecked conversions later in the
course (page 150).
Let's look at an example from an earlier chapter:
[Ada]
6 end My_Device_Types;
Here, we're saying that objects of type UInt10 must have at least 10 bits. In this case, if the code
compiles, it is a confirmation that such values can be represented in 10 bits when packed into an
5 procedure Show_Device_Types is
6 UInt10_Obj : constant UInt10 := 0;
7 begin
8 Put_Line ("Size of UInt10 type: " & Positive'Image (UInt10'Size));
9 Put_Line ("Size of UInt10 object: " & Positive'Image (UInt10_Obj'Size));
10 end Show_Device_Types;
Build output
Compile
[Ada] show_device_types.adb
[Ada] my_device_types.ads
Bind
[gprbind] show_device_types.bexch
[Ada] show_device_types.ali
Link
[link] show_device_types.adb
Runtime output
Here, we're retrieving the actual sizes of the UInt10 type and an object of that type. Note that the
sizes don't necessarily need to match. For example, although the size of UInt10 type is expected to
be 10 bits, the size of UInt10_Obj may be 16 bits, depending on the platform. Also, components
of this type within composite types (arrays, records) will probably be 16 bits as well unless they are
packed.
Register overlays make use of representation clauses to create a structure that facilitates manip-
ulating bits from registers. Let's look at a simplified example of a power management controller
containing registers such as a system clock enable register. Note that this example is based on an
actual architecture:
[Ada]
3 package Registers is
(continues on next page)
54 end Registers;
First, we declare the system clock enable register — this is PMC_SCER_Register type in the code
example. Most of the bits in that register are reserved. However, we're interested in bit #5, which
is used to activate or deactivate the system clock. To achieve a correct representation of this bit,
we do the following:
• We declare the USBCLK component of this record using the USB_Clock_Enable type, which
has a size of one bit; and
• we use a representation clause to indicate that the USBCLK component is specifically at bit
#5 of byte #0.
After declaring the system clock enable register and specifying its individual bits as compo-
nents of a record type, we declare the power management controller type — PMC_Peripheral
record type in the code example. Here, we declare two 16-bit registers as record components of
PMC_Peripheral. These registers are used to enable or disable the system clock. The strategy we
use in the declaration is similar to the one we've just seen above:
• We declare these registers as components of the PMC_Peripheral record type;
• we use a representation clause to specify that the PMC_SCER register is at byte #0 and the
PMC_SCDR register is at byte #2.
– Since these registers have 16 bits, we use a range of bits from 0 to 15.
The actual power management controller becomes accessible by the declaration of the
PMC_Periph object of PMC_Peripheral type. Here, we specify the actual address of the memory-
mapped registers (400E0600 in hexadecimal) using the Address aspect in the declaration. When
we use the Address aspect in an object declaration, we're indicating the address in memory of
that object.
Because we specify the address of the memory-mapped registers in the declaration of
PMC_Periph, this object is now an overlay for those registers. This also means that any opera-
tion on this object corresponds to an actual operation on the registers of the power management
controller. We'll discuss more details about overlays in the section about mapping structures to
bit-fields (page 137) (in chapter 6).
Finally, in a test application, we can access any bit of any register of the power management con-
troller with simple record component selection. For example, we can set the USBCLK bit of the
PMC_SCER register by using PMC_Periph.PMC_SCER.USBCLK:
[Ada]
3 procedure Enable_USB_Clock is
4 begin
5 Registers.PMC_Periph.PMC_SCER.USBCLK := 1;
6 end Enable_USB_Clock;
This code example makes use of many aspects and keywords of the Ada language. One of them
is the Volatile aspect, which we've discussed in the section about volatile and atomic objects
(page 90). Using the Volatile aspect for the PMC_SCER_Register type ensures that objects
of this type won't be stored in a register.
In the declaration of the PMC_SCER_Register record type of the example, we use the Bit_Order
aspect to specify the bit ordering of the record type. Here, we can select one of these options:
• High_Order_First: first bit of the record is the most significant bit;
• Low_Order_First: first bit of the record is the least significant bit.
The declarations from the Registers package also makes use of the Import, which is sometimes
necessary when creating overlays. When used in the context of object declarations, it avoids de-
fault initialization (for data types that have it.). Aspect Import will be discussed in the section that
explains how to map structures to bit-fields (page 137) in chapter 6. Please refer to that chapter for
more details.
That's what the aspect does for type PMC_SCER_Register in the example above, as well as for the
types Bit, UInt5 and UInt10. For example, we may declare a stand-alone object of type Bit:
3 procedure Show_Bit_Declaration is
4
8 B : constant Bit := 0;
9 -- ^ Although Bit'Size is 1, B'Size is almost certainly 8
10 begin
11 Put_Line ("Bit'Size = " & Positive'Image (Bit'Size));
12 Put_Line ("B'Size = " & Positive'Image (B'Size));
13 end Show_Bit_Declaration;
Build output
Compile
[Ada] show_bit_declaration.adb
Bind
[gprbind] show_bit_declaration.bexch
[Ada] show_bit_declaration.ali
Link
[link] show_bit_declaration.adb
Runtime output
Bit'Size = 1
B'Size = 8
In this case, B is almost certainly going to be 8-bits wide on a typical machine, even though the
language requires that Bit'Size is 1 by default.
In the declaration of the components of the PMC_Peripheral record type, we use the aliased
keyword to specify that those record components are accessible via other paths besides the com-
ponent name. Therefore, the compiler won't store them in registers. This makes sense because
we want to ensure that we're accessing specific memory-mapped registers, and not registers as-
signed by the compiler. Note that, for the same reason, we also use the aliased keyword in the
declaration of the PMC_Periph object.
Creating data streams — in the context of interfacing with devices — means the serialization of
arbitrary information and its transmission over a communication channel. For example, we might
want to transmit the content of memory-mapped registers as byte streams using a serial port. To
do this, we first need to get a serialized representation of those registers as an array of bytes, which
we can then transmit over the serial port.
Serialization of arbitrary record types — including register overlays — can be achieved by declar-
ing an array of bytes as an overlay. By doing this, we're basically interpreting the information from
those record types as bytes while ignoring their actual structure — i.e. their components and rep-
resentation clause. We'll discuss details about overlays in the section about mapping structures to
bit-fields (page 137) (in chapter 6).
Let's look at a simple example of serialization of an arbitrary record type:
[Ada]
9 end Arbitrary_Types;
9 --
10 -- We can access the serialized data in Raw_TX, which is our overlay
11 --
12 Raw_TX : UByte_Array (1 .. Some_Object'Size / 8)
13 with Address => Some_Object'Address;
14 begin
15 null;
16 --
17 -- Now, we could stream the data from Some_Object.
18 --
19 -- For example, we could send the bytes (from Raw_TX) via the
20 -- serial port.
21 --
22 end Serialize_Data;
4 procedure Data_Stream_Declaration is
5 Dummy_Object : Arbitrary_Types.Arbitrary_Record;
6
7 begin
8 Serialize_Data (Dummy_Object);
9 end Data_Stream_Declaration;
Build output
Compile
[Ada] data_stream_declaration.adb
[Ada] arbitrary_types.ads
(continues on next page)
The most important part of this example is the implementation of the Serialize_Data proce-
dure, where we declare Raw_TX as an overlay for our arbitrary object (Some_Object of Arbi-
trary_Record type). In simple terms, by writing with Address => Some_Object'Address; in
the declaration of Raw_TX, we're specifying that Raw_TX and Some_Object have the same address
in memory. Here, we are:
• taking the address of Some_Object — using the Address attribute —, and then
• using it as the address of Raw_TX — which is specified with the Address aspect.
By doing this, we're essentially saying that both Raw_TX and Some_Object are different represen-
tations of the same object in memory.
Because the Raw_TX overlay is completely agnostic about the actual structure of the record type,
the Arbitrary_Record type could really be anything. By declaring Raw_TX, we create an array of
bytes that we can use to stream the information from Some_Object.
We can use this approach and create a data stream for the register overlay example that we've
seen before. This is the corresponding implementation:
[Ada]
3 package Registers is
4
54 end Registers;
16 end Serial_Ports;
30 end Serial_Ports;
4 package Data_Stream is
5
12 end Data_Stream;
23 end Data_Stream;
3 with Registers;
4 with Data_Stream;
5 with Serial_Ports;
6
7 procedure Test_Data_Stream is
8
9 procedure Display_Registers is
10 use Ada.Text_IO;
11 begin
12 Put_Line ("---- Registers ----");
13 Put_Line ("PMC_SCER.USBCLK: "
14 & Registers.PMC_Periph.PMC_SCER.USBCLK'Image);
15 Put_Line ("PMC_SCDR.USBCLK: "
16 & Registers.PMC_Periph.PMC_SCDR.USBCLK'Image);
17 Put_Line ("-------------- ----");
18 end Display_Registers;
19
20 Port : Serial_Ports.Serial_Port;
21 begin
22 Registers.PMC_Periph.PMC_SCER.USBCLK := 1;
23 Registers.PMC_Periph.PMC_SCDR.USBCLK := 1;
24
25 Display_Registers;
26
33 Display_Registers;
34 end Test_Data_Stream;
Build output
Compile
[Ada] test_data_stream.adb
[Ada] data_stream.adb
[Ada] registers.ads
[Ada] serial_ports.adb
Bind
[gprbind] test_data_stream.bexch
[Ada] test_data_stream.ali
Link
[link] test_data_stream.adb
Runtime output
In this example, we can find the overlay in the implementation of the Send and Receive proce-
dures from the Data_Stream package. Because the overlay doesn't need to know the internals of
the PMC_Peripheral type, we're declaring it in the same way as in the previous example (where
we created an overlay for Some_Object). In this case, we're creating an overlay for the PMC pa-
rameter.
Note that, for this section, we're not really interested in the details about the serial port. Thus,
package Serial_Ports in this example is just a stub. However, because the Serial_Port type
in that package only sees arrays of bytes, after implementing an actual serial port interface for a
specific device, we could create data streams for any type.
As we've seen in the previous section about interfacing with devices (page 95), Ada offers powerful
features to describe low-level details about the hardware architecture without giving up its strong
typing capabilities. However, it can be cumbersome to create a specification for all those low-level
details when you have a complex architecture. Fortunately, for ARM Cortex-M devices, the GNAT
toolchain offers an Ada binding generator called svd2ada, which takes CMSIS-SVD descriptions
for those devices and creates Ada specifications that match the architecture. CMSIS-SVD descrip-
tion files are based on the Cortex Microcontroller Software Interface Standard (CMSIS), which is a
hardware abstraction layer for ARM Cortex microcontrollers.
Please refer to the svd2ada project page16 for details about this tool.
16 https://github.com/AdaCore/svd2ada
FIVE
In Ada, several common programming errors that are not already detected at compile-time are
detected instead at run-time, triggering "exceptions" that interrupt the normal flow of execution.
For example, an exception is raised by an attempt to access an array component via an index that
is out of bounds. This simple check precludes exploits based on buffer overflow. Several other
cases also raise language-defined exceptions, such as scalar range constraint violations and null
pointer dereferences. Developers may declare and raise their own application-specific exceptions
too. (Exceptions are software artifacts, although an implementation may map hardware events to
exceptions.)
Exceptions are raised during execution of what we will loosely define as a "frame." A frame is a
language construct that has a call stack entry when called, for example a procedure or function
body. There are a few other constructs that are also pertinent but this definition will suffice for
now.
Frames have a sequence of statements implementing their functionality. They can also have op-
tional "exception handlers" that specify the response when exceptions are "raised" by those state-
ments. These exceptions could be raised directly within the statements, or indirectly via calls to
other procedures and functions.
For example, the frame below is a procedure including three exceptions handlers:
Listing 1: p.adb
1 procedure P is
2 begin
3 Statements_That_Might_Raise_Exceptions;
4 exception
5 when A =>
6 Handle_A;
7 when B =>
8 Handle_B;
9 when C =>
10 Handle_C;
11 end P;
The three exception handlers each start with the word when (lines 5, 7, and 9). Next comes one or
more exception identifiers, followed by the so-called "arrow." In Ada, the arrow always associates
something on the left side with something on the right side. In this case, the left side is the exception
name and the right side is the handler's code for that exception.
Each handler's code consists of an arbitrary sequence of statements, in this case specific proce-
dures called in response to those specific exceptions. If exception A is raised we call procedure
Handle_A (line 6), dedicated to doing the actual work of handling that exception. The other two
exceptions are dealt with similarly, on lines 8 and 10.
107
Ada for the Embedded C Developer, Release 2021-06
Structurally, the exception handlers are grouped together and textually separated from the rest of
the code in a frame. As a result, the sequence of statements representing the normal flow of exe-
cution is distinct from the section representing the error handling. The reserved word exception
separates these two sections (line 4 above). This separation helps simplify the overall flow, increas-
ing understandability. In particular, status result codes are not required so there is no mixture of
error checking and normal processing. If no exception is raised the exception handler section is
automatically skipped when the frame exits.
Note how the syntactic structure of the exception handling section resembles that of an Ada case
statement. The resemblance is intentional, to suggest similar behavior. When something in the
statements of the normal execution raises an exception, the corresponding exception handler for
that specific exception is executed. After that, the routine completes. The handlers do not "fall
through" to the handlers below. For example, if exception B is raised, procedure Handle_B is
called but Handle_C is not called. There's no need for a break statement, just as there is no need
for it in a case statement. (There's no break statement in Ada anyway.)
So far, we've seen a frame with three specific exceptions handled. What happens if a frame has no
handler for the actual exception raised? In that case the run-time library code goes "looking" for
one.
Specifically, the active exception is propagated up the dynamic call chain. At each point in the chain,
normal execution in that caller is abandoned and the handlers are examined. If that caller has a
handler for the exception, the handler is executed. That caller then returns normally to its caller
and execution continues from there. Otherwise, propagation goes up one level in the call chain and
the process repeats. The search continues until a matching handler is found or no callers remain.
If a handler is never found the application terminates abnormally. If the search reaches the main
procedure and it has a matching handler it will execute the handler, but, as always, the routine
completes so once again the application terminates.
For a concrete example, consider the following:
Listing 2: arrays.ads
1 package Arrays is
2
7 end Arrays;
Listing 3: arrays.adb
1 package body Arrays is
2
8 end Arrays;
Listing 4: some_process.adb
1 with Ada.Text_IO; use Ada.Text_IO;
2 with Arrays; use Arrays;
3
4 procedure Some_Process is
5 L : constant List (1 .. 100) := (others => 42);
6 begin
7 Put_Line (Integer'Image (Value (L, 1, 10)));
(continues on next page)
Listing 5: main.adb
1 with Some_Process;
2 with Ada.Text_IO; use Ada.Text_IO;
3
4 procedure Main is
5 begin
6 Some_Process;
7 Put_Line ("Main completes normally");
8 end Main;
Procedure Main calls Some_Process, which in turn calls function Value (line 7). Some_Process
declares the array object L of type List on line 5, with bounds 1 through 100. The call to Value
has arguments, including variable L, leading to an attempt to access an array component via an
out-of-bounds index (1 + 10 * 10 = 101, beyond the last index of L). This attempt will trigger an
exception in Value prior to actually accessing the array object's memory. Function Value doesn't
have any exception handlers so the exception is propagated up to the caller Some_Process. Pro-
cedure Some_Process has an exception handler for Constraint_Error and it so happens that
Constraint_Error is the exception raised in this case. As a result, the code for that handler will
be executed, printing some messages on the screen. Then procedure Some_Process will return
to Main normally. Main then continues to execute normally after the call to Some_Process and
prints its completion message.
If procedure Some_Process had also not had a handler for Constraint_Error, that procedure
call would also have returned abnormally and the exception would have been propagated further
up the call chain to procedure Main. Normal execution in Main would likewise be abandoned
in search of a handler. But Main does not have any handlers so Main would have completed
abnormally, immediately, without printing its closing message.
This semantic model is the same as with many other programming languages, in which the execu-
tion of a frame's sequence of statements is unavoidably abandoned when an exception becomes
active. The model is a direct reaction to the use of status codes returned from functions as in C,
where it is all too easy to forget (intentionally or otherwise) to check the status values returned.
With the exception model errors cannot be ignored.
However, full exception propagation as described above is not the norm for embedded applica-
tions when the highest levels of integrity are required. The run-time library code implementing
exception propagation can be rather complex and expensive to certify. Those problems apply to
the application code too, because exception propagation is a form of control flow without any
explicit construct in the source. Instead of the full exception model, designers of high-integrity
applications often take alternative approaches.
One alternative consists of deactivating exceptions altogether, or more precisely, deactivating
language-defined checks, which means that the compiler will not generate code checking for condi-
tions giving rise to exceptions. Of course, this makes the code vulnerable to attacks, such as buffer
overflow, unless otherwise verified (e.g. through static analysis). Deactivation can be applied at the
unit level, through the -gnatp compiler switch, or locally within a unit via the pragma Suppress.
(Refer to the GNAT User’s Guide for Native Platforms17 for more details about the switch.)
For example, we can write the following. Note the pragma on line 4 of arrays.adb within function
Value:
17 https://docs.adacore.com/gnat_ugn-docs/html/gnat_ugn/gnat_ugn/building_executable_programs_with_gnat.html
Listing 6: arrays.ads
1 package Arrays is
2
7 end Arrays;
Listing 7: arrays.adb
1 package body Arrays is
2
9 end Arrays;
Listing 8: some_process.adb
1 with Ada.Text_IO; use Ada.Text_IO;
2 with Arrays; use Arrays;
3
4 procedure Some_Process is
5 L : constant List (1 .. 100) := (others => 42);
6 begin
7 Put_Line (Integer'Image (Value (L, 1, 10)));
8 exception
9 when Constraint_Error =>
10 Put_Line ("FAILURE");
11 end Some_Process;
This placement of the pragma will only suppress checks in the function body. However, that is
where the exception would otherwise have been raised, leading to incorrect and unpredictable
execution. (Run the program more than once. If it prints the right answer (42), or even the same
value each time, it's just a coincidence.) As you can see, suppressing checks negates the guarantee
of errors being detected and addressed at run-time.
Another alternative is to leave checks enabled but not retain the dynamic call-chain propagation.
There are a couple of approaches available in this alternative.
The first approach is for the run-time library to invoke a global "last chance handler" (LCH) when
any exception is raised. Instead of the sequence of statements of an ordinary exception handler,
the LCH is actually a procedure intended to perform "last-wishes" before the program terminates.
No exception handlers are allowed. In this scheme "propagation" is simply a direct call to the
LCH procedure. The default LCH implementation provided by GNAT does nothing other than loop
infinitely. Users may define their own replacement implementation.
The availability of this approach depends on the run-time library. Typically, Zero Footprint and
Ravenscar SFP run-times will provide this mechanism because they are intended for certification.
A user-defined LCH handler can be provided either in C or in Ada, with the following profiles:
[Ada]
[C]
We'll go into the details of the pragma Export in a further section on language interfacing. For
now, just know that the symbol __gnat_last_chance_handler is what the run-time uses to
branch immediately to the last-chance handler. Pragma Export associates that symbol with this
replacement procedure so it will be invoked instead of the default routine. As a consequence, the
actual procedure name in Ada is immaterial.
Here is an example implementation that simply blinks an LED forever on the target:
loop
Toggle (LCH_LED);
Next_Release := Next_Release + Period;
delay until Next_Release;
end loop;
end Last_Chance_Handler;
The LCH_LED is a constant referencing the LED used by the last-chance handler, declared else-
where. The infinite loop is necessary because a last-chance handler must never return to the caller
(hence the term "last-chance"). The LED changes state every half-second.
Unlike the approach in which there is only the last-chance handler routine, the other approach al-
lows exception handlers, but in a specific, restricted manner. Whenever an exception is raised, the
only handler that can apply is a matching handler located in the same frame in which the excep-
tion is raised. Propagation in this context is simply an immediate branch instruction issued by the
compiler, going directly to the matching handler's sequence of statements. If there is no matching
local handler the last chance handler is invoked. For example consider the body of function Value
in the body of package Arrays:
Listing 9: arrays.ads
1 package Arrays is
2
7 end Arrays;
11 end Arrays;
4 procedure Some_Process is
5 L : constant List (1 .. 100) := (others => 42);
6 begin
7 Put_Line (Integer'Image (Value (L, 1, 10)));
8 exception
9 when Constraint_Error =>
10 Put_Line ("FAILURE");
11 end Some_Process;
In both procedure Some_Process and function Value we have an exception handler for Con-
straint_Error. In this example the exception is raised in Value because the index check fails
there. A local handler for that exception is present so the handler applies and the function returns
zero, normally. Because the call to the function returns normally, the execution of Some_Process
prints zero and then completes normally.
Let's imagine, however, that function Value did not have a handler for Constraint_Error.
In the context of full exception propagation, the function call would return to the caller, i.e.,
Some_Process, and would be handled in that procedure's handler. But only local handlers are
allowed under the second alternative so the lack of a local handler in Value would result in the
last-chance handler being invoked. The handler for Constraint_Error in Some_Process under
this alternative approach.
So far we've only illustrated handling the Constraint_Error exception. It's possible to handle
other language-defined and user-defined exceptions as well, of course. It is even possible to define
a single handler for all other exceptions that might be encountered in the handled sequence of
statements, beyond those explicitly named. The "name" for this otherwise anonymous exception
is the Ada reserved word others. As in case statements, it covers all other choices not explicitly
mentioned, and so must come last. For example:
7 end Arrays;
13 end Arrays;
4 procedure Some_Process is
5 L : constant List (1 .. 100) := (others => 42);
6 begin
7 Put_Line (Integer'Image (Value (L, 1, 10)));
8 exception
9 when Constraint_Error =>
10 Put_Line ("FAILURE");
11 end Some_Process;
In the code above, the Value function has a handler specifically for Constraint_Error as be-
fore, but also now has a handler for all other exceptions. For any exception other than Con-
straint_Error, function Value returns -1. If you remove the function's handler for Con-
straint_Error (lines 7 and 8) then the other "anonymous" handler will catch the exception and
-1 will be returned instead of zero.
There are additional capabilities for exceptions, but for now you have a good basic understanding
of how exceptions work, especially their dynamic nature at run-time.
So far, we have discussed language-defined checks inserted by the compiler for verification at run-
time, leading to exceptions being raised. We saw that these dynamic checks verified semantic
conditions ensuring proper execution, such as preventing writing past the end of a buffer, or ex-
ceeding an application-specific integer range constraint, and so on. These checks are defined by
the language because they apply generally and can be expressed in language-defined terms.
Developers can also define dynamic checks. These checks specify component-specific or
application-specific conditions, expressed in terms defined by the component or application. We
will refer to these checks as "user-defined" for convenience. (Be sure you understand that we are
not talking about user-defined exceptions here.)
Like the language-defined checks, user-defined checks must be true at run-time. All checks con-
sist of Boolean conditions, which is why we can refer to them as assertions: their conditions are
asserted to be true by the compiler or developer.
Assertions come in several forms, some relatively low-level, such as a simple pragma Assert, and
some high-level, such as type invariants and contracts. These forms will be presented in detail in a
later section, but we will illustrate some of them here.
User-defined checks can be enabled at run-time in GNAT with the -gnata switch, as well as
with pragma Assertion_Policy. The switch enables all forms of these assertions, whereas the
pragma can be used to control specific forms. The switch is typically used but there are reason-
able use-cases in which some user-defined checks are enabled, and others, although defined, are
disabled.
By default in GNAT, language-defined checks are enabled but user-defined checks are disabled.
Here's an example of a simple program employing a low-level assertion. We can use it to show the
effects of the switches, including the defaults:
3 procedure Main is
4 X : Positive := 10;
5 begin
6 X := X * 5;
7 pragma Assert (X > 99);
8 X := X - 99;
9 Put_Line (Integer'Image (X));
10 end Main;
If we compiled this code we would get a warning about the assignment on line 8 after the pragma
Assert, but not one about the Assert itself on line 7.
gprbuild -q -P main.gpr
main.adb:8:11: warning: value not in range of type "Standard.Positive"
main.adb:8:11: warning: "Constraint_Error" will be raised at run time
No code is generated for the user-defined check expressed via pragma Assert but the language-
defined check is emitted. In this case the range constraint on X excludes zero and negative num-
bers, but X * 5 = 50, X - 99 = -49. As a result, the check for the last assignment would fail,
raising Constraint_Error when the program runs. These results are the expected behavior for
the default switch settings.
But now let's enable user-defined checks and build it. Different compiler output will appear.
3 procedure Main is
4 X : Positive := 10;
5 begin
6 X := X * 5;
7 pragma Assert (X > 99);
8 X := X - 99;
9 Put_Line (Integer'Image (X));
10 end Main;
Build output
Compile
[Ada] main.adb
main.adb:7:19: warning: assertion will fail at run time [-gnatw.a]
main.adb:8:11: warning: value not in range of type "Standard.Positive" [enabled by␣
↪default]
Bind
[gprbind] main.bexch
[Ada] main.ali
Link
[link] main.adb
Runtime output
Now we also get the compiler warning about the pragma Assert condition. When run, the failure
of pragma Assert on line 7 raises the exception Ada.Assertions.Assertion_Error. Accord-
ing to the expression in the assertion, X is expected (incorrectly) to be above 99 after the multi-
plication. (The exception name in the error message, SYSTEM.ASSERTIONS.ASSERT_FAILURE, is a
GNAT-specific alias for Ada.Assertions.Assertion_Error.)
It's interesting to see in the output that the compiler can detect some violations at compile-time:
main.adb:7:19: warning: assertion will fail at run time
main.adb:7:21: warning: condition can only be True if invalid values present
main.adb:8:11: warning: value not in range of type "Standard.Positive"
Generally speaking, a complete analysis is beyond the scope of compilers and they may not find all
errors prior to execution, even those we might detect ourselves by inspection. More errors can be
found by tools dedicated to that purpose, known as static analyzers. But even an automated static
analysis tool cannot guarantee it will find all potential problems.
A much more powerful alternative is formal proof, a form of static analysis that can (when possible)
give strong guarantees about the checks, for all possible conditions and all possible inputs. Proof
can be applied to both language-defined and user-defined checks.
Be sure you understand that formal proof, as a form of static analysis, verifies conditions prior to
execution, even prior to compilation. That earliness provides significant cost benefits. Removing
bugs earlier is far less expensive than doing so later because the cost to fix bugs increases ex-
ponentially over the phases of the project life cycle, especially after deployment. Preventing bug
introduction into the deployed system is the least expensive approach of all. Furthermore, cost
savings during the initial development will be possible as well, for reasons specific to proof. We will
revisit this topic later in this section.
Formal analysis for proof can be achieved through the SPARK subset of the Ada language combined
with the gnatprove verification tool. SPARK is a subset encompassing most of the Ada language,
except for features that preclude proof. As a disclaimer, this course is not aimed at providing a full
introduction to proof and the SPARK language, but rather to present in a few examples what it is
about and what it can do for us.
As it turns out, our procedure Main is already SPARK compliant so we can start verifying it.
3 procedure Main is
4 X : Positive := 10;
5 begin
6 X := X * 5;
7 pragma Assert (X > 99);
8 X := X - 99;
9 Put_Line (Integer'Image (X));
10 end Main;
Build output
Compile
[Ada] main.adb
main.adb:7:20: warning: assertion will fail at run time [-gnatw.a]
main.adb:8:12: warning: value not in range of type "Standard.Positive" [enabled by␣
↪default]
Prover output
Phase 1 of 2: generation of Global contracts ...
Phase 2 of 2: flow analysis and proof ...
main.adb:7:20: medium: assertion might fail
gnatprove: unproved check messages considered as errors
Runtime output
The "Prove" button invokes gnatprove on main.adb. You can ignore the parameters to the invo-
cation. For the purpose of this demonstration, the interesting output is this message:
main.adb:7:19: medium: assertion might fail, cannot prove X > 99 (e.g. when X = 50)
gnatprove can tell that the assertion X > 99 may have a problem. There's indeed a bug here, and
gnatprove even gives us the counterexample (when X is 50). As a result the code is not proven
and we know we have an error to correct.
Notice that the message says the assertion "might fail" even though clearly gnatprove has an ex-
ample for when failure is certain. That wording is a reflection of the fact that SPARK gives strong
guarantees when the assertions are proven to hold, but does not guarantee that flagged problems
are indeed problems. In other words, gnatprove does not give false positives but false negatives
are possible. The result is that if gnatprove does not indicate a problem for the code under anal-
ysis we can be sure there is no problem, but if gnatprove does indicate a problem the tool may
be wrong.
An immediate benefit from having our code compatible with the SPARK subset is that we can ask
gnatprove to verify initialization and correct data flow, as indicated by the absence of messages
during SPARK "flow analysis." Flow analysis detects programming errors such as reading uninitial-
ized data, problematic aliasing between formal parameters, and data races between concurrent
tasks.
In addition, gnatprove checks unit specifications for the actual data read or written, and the flow
of information from inputs to outputs. As you can imagine, this verification provides significant
benefits, and it can be reached with comparatively low cost.
For example, the following illustrates an initialization failure:
4 procedure Main is
5 B : Integer;
6 begin
7 Increment (B);
(continues on next page)
Prover output
Granted, Increment is a silly procedure as-is, but imagine it did useful things, and, as part of that,
incremented the argument. gnatprove tells us that the caller has not assigned a value to the
argument passed to Increment.
Consider this next routine, which contains a serious coding error. Flow analysis will find it for us.
Prover output
gnatprove tells us that Z might not be initialized (assigned a value) in Compute_Offset, and
indeed that is correct. Z is a mode out parameter so the routine should assign a value to it: Z
is an output, after all. The fact that Compute_Offset does not do so is a significant and nasty
bug. Why is it so nasty? In this case, formal parameter Z is of the scalar type Integer, and scalar
parameters are always passed by copy in Ada and SPARK. That means that, when returning to the
caller, an integer value is copied to the caller's argument passed to Z. But this procedure doesn't
always assign the value to be copied back, and in that case an arbitrary value — whatever is on the
stack — is copied to the caller's argument. The poor programmer must debug the code to find the
problem, yet the effect could appear well downstream from the call to Compute_Offset. That's
not only painful, it is expensive. Better to find the problem before we even compile the code.
So far, we've seen assertions in a routine's sequence of statements, either through implicit
language-defined checks (is the index in the right range?) or explicit user-defined checks. These
checks are already useful by themselves but they have an important limitation: the assertions are
in the implementation, hidden from the callers of the routine. For example, a call's success or
failure may depend upon certain input values but the caller doesn't have that information.
Generally speaking, Ada and SPARK put a lot of emphasis on strong, complete specifications for
the sake of abstraction and analysis. Callers need not examine the implementations to determine
whether the arguments passed to it are changed, for example. It is possible to go beyond that,
however, to specify implementation constraints and functional requirements. We use contracts to
do so.
At the language level, contracts are higher-level forms of assertions associated with specifications
and declarations rather than sequences of statements. Like other assertions they can be activated
or deactivated at run-time, and can be statically proven. We'll concentrate here on two kinds of
contracts, both associated especially (but not exclusively) with procedures and functions:
• Preconditions, those Boolean conditions required to be true prior to a call of the corresponding
subprogram
• Postconditions, those Boolean conditions required to be true after a call, as a result of the
corresponding subprogram's execution
In particular, preconditions specify the initial conditions, if any, required for the called routine
to correctly execute. Postconditions, on the other hand, specify what the called routine's execu-
tion must have done, at least, on normal completion. Therefore, preconditions are obligations on
callers (referred to as "clients") and postconditions are obligations on implementers. By the same
token, preconditions are guarantees to the implementers, and postconditions are guarantees to
clients.
Contract-based programming, then, is the specification and rigorous enforcement of these obliga-
tions and guarantees. Enforcement is rigorous because it is not manual, but tool-based: dynami-
cally at run-time with exceptions, or, with SPARK, statically, prior to build.
Preconditions are specified via the "Pre" aspect. Postconditions are specified via the "Post" aspect.
Usually subprograms have separate declarations and these aspects appear with those declara-
tions, even though they are about the bodies. Placement on the declarations allows the obligations
and guarantees to be visible to all parties. For example:
The precondition on line 2 specifies that, for any given call, the sum of the values passed to param-
eters X and Y must not be zero. (Perhaps we're dividing by X + Y in the body.) The declaration
also provides a guarantee about the function call's result, via the postcondition on line 3: for any
given call, the value returned will be greater than the value passed to X.
Consider a client calling this function:
4 procedure Demo is
5 A, B, C : Integer;
6 begin
(continues on next page)
Prover output
gnatprove indicates that the assignment to B (line 8) might fail because of the precondition, i.e.,
the sum of the inputs shouldn't be 0, yet -1 + 1 = 0. (We will address the other output message
elsewhere.)
Let's change the argument passed to Y in the second call (line 8). Instead of -1 we will pass -2:
4 procedure Demo is
5 A, B, C : Integer;
6 begin
7 A := Mid (1, 2);
8 B := Mid (1, -2);
9 C := Mid (A, B);
10 Put_Line (C'Image);
11 end Demo;
Prover output
The second call will no longer be flagged for the precondition. In addition, gnatprove will know
from the postcondition that A has to be greater than 1, as does B, because in both calls 1 was
passed to X. Therefore, gnatprove can deduce that the precondition will hold for the third call C
:= Mid (A, B); because the sum of two numbers greater than 1 will never be zero.
Postconditions can also compare the state prior to a call with the state after a call, using the 'Old
attribute. For example:
Prover output
The postcondition specifies that, on return, the argument passed to the parameter Value will be
one greater than it was immediately prior to the call (Value'Old).
One typical benefit of contract-based programming is the removal of defensive code in subprogram
implementations. For example, the Push operation for a stack type would need to ensure that the
given stack is not already full. The body of the routine would first check that, explicitly, and perhaps
raise an exception or set a status code. With preconditions we can make the requirement explicit
and gnatprove will verify that the requirement holds at all call sites.
This reduction has a number of advantages:
• The implementation is simpler, removing validation code that is often difficult to test, makes
the code more complex and leads to behaviors that are difficult to define.
• The precondition documents the conditions under which it's correct to call the subprogram,
moving from an implementer responsibility to mitigate invalid input to a user responsibility
to fulfill the expected interface.
• Provides the means to verify that this interface is properly respected, through code review,
dynamic checking at run-time, or formal static proof.
As an example, consider a procedure Read that returns a component value from an array. Both
the Data and Index are objects visible to the procedure so they are not formal parameters.
10 end P;
10 V := Data (Index);
11 Index := Index + 1;
12 end Read;
13 end P;
Prover output
Phase 1 of 2: generation of Global contracts ...
Phase 2 of 2: flow analysis and proof ...
In addition to procedure Read we would also have a way to load the array components in the first
place, but we can ignore that for the purpose of this discussion.
Procedure Read is responsible for reading an element of the array and then incrementing the
index. What should it do in case of an invalid index? In this implementation there is defensive code
that returns a value arbitrarily chosen. We could also redesign the code to return a status in this
case, or — better — raise an exception.
An even more robust approach would be instead to ensure that this subprogram is only called
when Index is within the indexing boundaries of Data. We can express that requirement with a
precondition (line 9).
11 end P;
9 end P;
Prover output
Phase 1 of 2: generation of Global contracts ...
Phase 2 of 2: flow analysis and proof ...
Now we don't need the defensive code in the procedure body. That's safe because SPARK will
attempt to prove statically that the check will not fail at the point of each call.
Assuming that procedure Read is intended to be the only way to get values from the array, in a real
application (where the principles of software engineering apply) we would take advantage of the
compile-time visibility controls that packages offer. Specifically, we would move all the variables'
declarations to the private part of the package, or even the package body, so that client code could
not possibly access the array directly. Only procedure Read would remain visible to clients, thus
remaining the only means of accessing the array. However, that change would entail others, and in
this chapter we are only concerned with introducing the capabilities of SPARK. Therefore, we keep
the examples as simple as possible.
Earlier we said that gnatprove will verify both language-defined and user-defined checks. Proving
that the language-defined checks will not raise exceptions at run-time is known as proving "Absence
of Run-Time Errors" or AoRTE for short. Successful proof of these checks is highly significant in
itself.
One of the major resulting benefits is that we can deploy the final executable with checks disabled.
That has obvious performance benefits, but it is also a safety issue. If we disable the checks we also
disable the run-time library support for them, but in that case the language does not define what
happens if indeed an exception is raised. Formally speaking, anything could happen. We must
have good reason for thinking that exceptions cannot be raised.
This is such an important issue that proof of AoRTE can be used to comply with the objectives of
certification standards in various high-integrity domains (for example, DO-178B/C in avionics, EN
50128 in railway, IEC 61508 in many safety-related industries, ECSS-Q-ST-80C in space, IEC 60880
in nuclear, IEC 62304 in medical, and ISO 26262 in automotive).
As a result, the quality of the program can be guaranteed to achieve higher levels of integrity than
would be possible in other programming languages.
However, successful proof of AoRTE may require additional assertions, especially preconditions.
We can see that with procedure Increment, the procedure that takes an Integer argument and
increments it by one. But of course, if the incoming value of the argument is the largest possible
positive value, the attempt to increment it would overflow, raising Constraint_Error. (As you
have likely already concluded, Constraint_Error is the most common exception you will have
to deal with.) We added a precondition to allow only the integer values up to, but not including,
the largest positive value:
Prover output
Prove it, then comment-out the precondition and try proving it again. Not only will gnatprove tell
us what is wrong, it will suggest a solution as well.
Without the precondition the check it provides would have to be implemented as defensive code
in the body. One or the other is critical here, but note that we should never need both.
The postcondition on Increment expresses what is, in fact, a unit-level requirement. Successfully
proving such requirements is another significant robustness and cost benefit. Together with the
proofs for initialization and AoRTE, these proofs ensure program integrity, that is, the program
executes within safe boundaries: the control flow of the program is correctly programmed and
cannot be circumvented through run-time errors, and data cannot be corrupted.
We can go even further. We can use contracts to express arbitrary abstract properties when such
exist. Safety and security properties, for instance, could be expressed as postconditions and then
proven by gnatprove.
For example, imagine we have a procedure to move a train to a new position on the track, and we
want to do so safely, without leading to a collision with another train. Procedure Move, therefore,
takes two inputs: a train identifier specifying which train to move, and the intended new position.
The procedure's output is a value indicating a motion command to be given to the train in order to
go to that new position. If the train cannot go to that new position safely the output command is
to stop the train. Otherwise the command is for the train to continue at an indicated speed:
procedure Move
(Train : in Train_Id;
New_Position : in Train_Position;
Result : out Move_Result)
with
Pre => Valid_Id (Train) and
Valid_Move (Trains (Train), New_Position) and
At_Most_One_Train_Per_Track and
Safe_Signaling,
Post => At_Most_One_Train_Per_Track and
Safe_Signaling;
The preconditions specify that, given a safe initial state and a valid move, the result of the call will
also be a safe state: there will be at most one train per track section and the track signaling system
will not allow any unsafe movements.
Make sure you understand that gnatprove does not attempt to prove the program correct as a
whole. It attempts to prove language-defined and user-defined assertions about parts of the pro-
gram, especially individual routines and calls to those routines. Furthermore, gnatprove proves
the routines correct only to the extent that the user-defined assertions correctly and sufficiently
describe and constrain the implementation of the corresponding routines.
Although we are not proving whole program correctness, as you will have seen — and done —
we can prove properties than make our software far more robust and bug-free than is possible
otherwise. But in addition, consider what proving the unit-level requirements for your procedures
and functions would do for the cost of unit testing and system integration. The tests would pass
the first time.
However, within the scope of what SPARK can do, not everything can be proven. In some cases
that is because the software behavior is not amenable to expression as boolean conditions (for
example, a mouse driver). In other cases the source code is beyond the capabilities of the analyzers
that actually do the mathematical proof. In these cases the combination of proof and actual test is
appropriate, and still less expensive that testing alone.
There is, of course, much more to be said about what can be done with SPARK and gnatprove.
Those topics are reserved for the Introduction to SPARK18 course.
18 https://learn.adacore.com/courses/intro-to-spark/index.html
SIX
One question that may arise relatively soon when converting from C to Ada is the style of source
code presentation. The Ada language doesn't impose any particular style and for many reasons, it
may seem attractive to keep a C-like style — for example, camel casing — to the Ada program.
However, the code in the Ada language standard, most third-party code, and the libraries provided
by GNAT follow a specific style for identifiers and reserved words. Using a different style for the
rest of the program leads to inconsistencies, thereby decreasing readability and confusing auto-
matic style checkers. For those reasons, it's usually advisable to adopt the Ada style — in which
each identifier starts with an upper case letter, followed by lower case letters (or digits), with an
underscore separating two "distinct" words within the identifier. Acronyms within identifiers are
in upper case. For example, there is a language-defined package named Ada.Text_IO. Reserved
words are all lower case.
Following this scheme doesn't preclude adding additional, project-specific rules.
Before even considering translating code from C to Ada, it's worthwhile to evaluate the possibility
of keeping a portion of the C code intact, and only translating selected modules to Ada. This is a
necessary evil when introducing Ada to an existing large C codebase, where re-writing the entire
code upfront is not practical nor cost-effective.
Fortunately, Ada has a dedicated set of features for interfacing with other languages. The Inter-
faces package hierarchy and the pragmas Convention, Import, and Export allow you to make
inter-language calls while observing proper data representation for each language.
Let's start with the following C code:
[C]
Listing 1: call.c
1 #include <stdio.h>
2
3 struct my_struct {
4 int A, B;
5 };
6
125
Ada for the Embedded C Developer, Release 2021-06
To call that function from Ada, the Ada compiler requires a description of the data structure to
pass as well as a description of the function itself. To capture how the C struct my_struct is
represented, we can use the following record along with a pragma Convention. The pragma
directs the compiler to lay out the data in memory the way a C compiler would.
[Ada]
Listing 2: use_my_struct.adb
1 with Ada.Text_IO; use Ada.Text_IO;
2 with Interfaces.C;
3
4 procedure Use_My_Struct is
5
Build output
Compile
[Ada] use_my_struct.adb
Bind
[gprbind] use_my_struct.bexch
[Ada] use_my_struct.ali
Link
[link] use_my_struct.adb
Runtime output
V = ( 1 2)
Describing a foreign subprogram call to Ada code is called binding and it is performed in two stages.
First, an Ada subprogram specification equivalent to the C function is coded. A C function returning
a value maps to an Ada function, and a void function maps to an Ada procedure. Then, rather than
implementing the subprogram using Ada code, we use a pragma Import:
procedure Call (V : my_struct);
pragma Import (C, Call, "call"); -- Third argument optional
The Import pragma specifies that whenever Call is invoked by Ada code, it should invoke the
Call function with the C calling convention.
And that's all that's necessary. Here's an example of a call to Call:
[Ada]
Listing 3: use_my_struct.adb
1 with Interfaces.C;
2
3 procedure Use_My_Struct is
4
The easiest way to build an application using mixed C / Ada code is to create a simple project file
for gprbuild and specify C as an additional language. By default, when using gprbuild we only
compile Ada source files. To compile C code files as well, we use the Languages attribute and
specify c as an option, as in the following example of a project file named default.gpr:
project Default is
end Default;
Then, we use this project file to build the application by simply calling gprbuild. Alternatively, we
can specify the project file on the command-line with the -P option — for example, gprbuild -P
default.gpr. In both cases, gprbuild compiles all C source-code file found in the directory and
links the corresponding object files to build the executable.
In order to include debug information, you can use gprbuild -cargs -g. This option adds
debug information based on both C and Ada code to the executable. Alternatively, you can specify
a Builder package in the project file and include global compilation switches for each language
using the Global_Compilation_Switches attribute. For example:
project Default is
package Builder is
for Global_Compilation_Switches ("Ada") use ("-g");
for Global_Compilation_Switches ("C") use ("-g");
end Builder;
end Default;
In this case, you can simply run gprbuild -P default.gpr to build the executable.
To debug the executable, you can use programs such as gdb or ddd, which are suitable for debug-
ging both C and Ada source-code. If you prefer a complete IDE, you may want to look into GNAT
Studio, which supports building and debugging an application within a single environment, and
remotely running applications loaded to various embedded devices. You can find more informa-
tion about gprbuild and GNAT Studio in the Introduction to GNAT Toolchain19 course.
19 https://learn.adacore.com/courses/GNAT_Toolchain_Intro/index.html
It may be useful to start interfacing Ada and C by using automatic binding generators. These can be
done either by invoking gcc -fdump-ada-spec option (to generate an Ada binding to a C header
file) or -gnatceg option (to generate a C binding to an Ada specification file). For example:
The level of interfacing is very low level and typically requires either massaging (changing the gen-
erated files) or wrapping (calling the generated files from a higher level interface). For example,
numbers bound from C to Ada are only standard numbers where user-defined types may be de-
sirable. C uses a lot of by-pointer parameters which may be better replaced by other parameter
modes, etc.
However, the automatic binding generator helps having a starting point which ensures compatibil-
ity of the Ada and the C code.
It is relatively straightforward to pass an array from Ada to C. In particular, with the GNAT compiler,
passing an array is equivalent to passing a pointer to its first element. Of course, as there's no
notion of boundaries in C, the length of the array needs to be passed explicitly. For example:
[C]
Listing 4: p.h
1 void p (int * a, int length);
[Ada]
Listing 5: main.adb
1 procedure Main is
2 type Arr is array (Integer range <>) of Integer;
3
7 X : Arr (5 .. 15);
8 begin
9 P (X, X'Length);
10 end Main;
The other way around — that is, retrieving an array that has been creating on the C side — is more
difficult. Because C doesn't explicitly carry boundaries, they need to be recreated in some way.
The first option is to actually create an Ada array without boundaries. This is the most flexible, but
also the least safe option. It involves creating an array with indices over the full range of Integer
without ever creating it from Ada, but instead retrieving it as an access from C. For example:
[C]
Listing 6: f.h
1 int * f ();
[Ada]
Listing 7: main.adb
1 procedure Main is
2 type Arr is array (Integer) of Integer;
3 type Arr_A is access all Arr;
4
Note that Arr is a constrained type (it doesn't have the range <> notation for indices). For that
reason, as it would be for C, it's possible to iterate over the whole range of integer, beyond the
memory actually allocated for the array.
A somewhat safer way is to overlay an Ada array over the C one. This requires having access to
the length of the array. This time, let's consider two cases, one with an array and its size accessi-
ble through functions, another one on global variables. This time, as we're using an overlay, the
function will be directly mapped to an Ada function returning an address:
[C]
Listing 8: fg.h
1 int * f_arr (void);
2 int f_size (void);
3
4 int * g_arr;
5 int g_size;
[Ada]
Listing 9: fg.ads
1 with System;
2
3 package Fg is
4
15 G_Size : Integer;
16 pragma Import (C, G_Size, "g_size");
17
21 end Fg;
With all solutions though, importing an array from C is a relatively unsafe pattern, as there's only
so much information on the array as there would be on the C side in the first place. These are good
places for careful peer reviews.
When interfacing Ada and C, the rules of parameter passing are a bit different with regards to
what's a reference and what's a copy. Scalar types and pointers are passed by value, whereas
record and arrays are (almost) always passed by reference. However, there may be cases where
the C interface also passes values and not pointers to objects. Here's a slightly modified version of
a previous example to illustrate this point:
[C]
3 struct my_struct {
4 int A, B;
5 };
6
In Ada, a type can be modified so that parameters of this type can always be passed by copy.
[Ada]
3 procedure Main is
4 type my_struct is record
5 A : Interfaces.C.int;
6 B : Interfaces.C.int;
7 end record
8 with Convention => C_Pass_By_Copy;
9
Note that this cannot be done at the subprogram declaration level, so if there is a mix of by-copy
and by-reference calls, two different types need to be used on the Ada side.
Because of the absence of namespaces, any global name in C tends to be very long. And because
of the absence of overloading, they can even encode type names in their type.
In Ada, the package is a namespace — two entities declared in two different packages are clearly
identified and can always be specifically designated. The C names are usually a good indication of
the names of the future packages and should be stripped — it is possible to use the full name if
useful. For example, here's how the following declaration and call could be translated:
[C]
7 return 0;
8 }
[Ada]
7 end Register_Interface;
3 procedure Main is
4 begin
5 Register_Interface.Initialize (15);
6 end Main;
Note that in the above example, a use clause on Register_Interface could allow us to omit the
prefix.
6.8 Pointers
The first thing to ask when translating pointers from C to Ada is: are they needed in the first place?
In Ada, pointers (or access types) should only be used with complex structures that cannot be allo-
cated at run-time — think of a linked list or a graph for example. There are many other situations
that would need a pointer in C, but do not in Ada, in particular:
• Arrays, even when dynamically allocated
• Results of functions
• Passing large structures as parameters
• Access to registers
• ... others
This is not to say that pointers aren't used in these cases but, more often than not, the pointer
is hidden from the user and automatically handled by the code generated by the compiler; thus
avoiding possible mistakes from being made. Generally speaking, when looking at C code, it's good
practice to start by analyzing how many pointers are used and to translate as many as possible into
pointerless Ada structures.
Here are a few examples of such patterns — additional examples can be found throughout this
document.
Dynamically allocated arrays can be directly allocated on the stack:
[C]
3 int main() {
4 int *a = malloc(sizeof(int) * 10);
5
6 return 0;
7 }
[Ada]
Build output
Compile
[Ada] main.adb
main.adb:3:04: warning: variable "A" is never read and never assigned [-gnatwv]
Bind
[gprbind] main.bexch
[Ada] main.ali
Link
[link] main.adb
It's even possible to create a such an array within a structure, provided that the size of the array is
known when instantiating this object, using a type discriminant:
[C]
3 typedef struct {
4 int * a;
5 } S;
6
13 return 0;
14 }
[Ada]
8 V : S (9);
9 begin
10 null;
11 end Main;
Build output
Compile
[Ada] main.adb
main.adb:8:04: warning: variable "V" is never read and never assigned [-gnatwv]
Bind
[gprbind] main.bexch
[Ada] main.ali
Link
[link] main.adb
With regards to parameter passing, usage mode (input / output) should be preferred to implemen-
tation mode (by copy or by reference). The Ada compiler will automatically pass a reference when
needed. This works also for smaller objects, so that the compiler will copy in an out when needed.
One of the advantages of this approach is that it clarifies the nature of the object: in particular, it
differentiates between arrays and scalars. For example:
[C]
[Ada]
Most of the time, access to registers end up in some specific structures being mapped onto a
specific location in memory. In Ada, this can be achieved through an Address clause associated
to a variable, for example:
[C]
5 return 0;
6 }
[Ada]
3 procedure Test is
4 R : Integer with Address => System'To_Address (16#FFFF00A0#);
5 begin
6 null;
7 end Test;
Build output
Compile
[Ada] test.adb
Bind
[gprbind] test.bexch
[Ada] test.ali
Link
[link] test.adb
These are some of the most common misuse of pointers in Ada. Previous sections of the document
deal with specifically using access types if absolutely necessary.
Bitwise operations such as masks and shifts in Ada should be relatively rarely needed, and, when
translating C code, it's good practice to consider alternatives. In a lot of cases, these operations are
used to insert several pieces of data into a larger structure. In Ada, this can be done by describ-
ing the structure layout at the type level through representation clauses, and then accessing this
structure as any other.
Consider the case of using a C primitive type as a container for single bit boolean flags. In C, this
would be done through masks, e.g.:
[C]
12 return 0;
13 }
In Ada, the above can be represented through a Boolean array of enumerate values:
[Ada]
6 Value : Value_Array :=
7 (Flag_2 => True,
8 Flag_4 => True,
9 others => False);
10 begin
11 null;
12 end Main;
Build output
Compile
[Ada] main.adb
Bind
[gprbind] main.bexch
[Ada] main.ali
Link
[link] main.adb
Note the Pack directive for the array, which requests that the array takes as little space as possible.
It is also possible to map records on memory when additional control over the representation is
needed or more complex data are used:
[C]
5 value = (2 << 1) | 1;
6
7 return 0;
8 }
[Ada]
Build output
Compile
[Ada] main.adb
Bind
[gprbind] main.bexch
[Ada] main.ali
Link
[link] main.adb
3 procedure Main is
4 type Value_Type is mod 2 ** 32;
(continues on next page)
7 Value : Value_Type;
8 begin
9 Value := Shift_Left (2, 1) or 1;
10 Put_Line ("Value = " & Value_Type'Image (Value));
11 end Main;
Build output
Compile
[Ada] main.adb
Bind
[gprbind] main.bexch
[Ada] main.ali
Link
[link] main.adb
Runtime output
Value = 5
In the previous section, we've seen how to perform bitwise operations. In this section, we look at
how to interpret a data type as a bit-field and perform low-level operations on it.
In general, you can create a bit-field from any arbitrary data type. First, we declare a bit-field type
like this:
[Ada]
As we've seen previously, the Pack aspect declared at the end of the type declaration indicates that
the compiler should optimize for size. We must use this aspect to be able to interpret data types
as a bit-field.
Then, we can use the Size and the Address attributes of an object of any type to declare a bit-field
for this object. We've discussed the Size attribute earlier in this course (page 95).
The Address attribute indicates the address in memory of that object. For example, assuming
we've declare a variable V, we can declare an actual bit-field object by referring to the Address
attribute of V and using it in the declaration of the bit-field, as shown here:
[Ada]
Note that, in this declaration, we're using the Address attribute of V for the Address aspect of B.
This technique is called overlays for serialization. Now, any operation that we perform on B will
have a direct impact on V, since both are using the same memory location.
The approach that we use in this section relies on the Address aspect. Another approach would
be to use unchecked conversions, which we'll discuss in the next section (page 150).
We should add the Volatile aspect to the declaration to cover the case when both objects can
still be changed independently — they need to be volatile, otherwise one change might be missed.
This is the updated declaration:
[Ada]
Using the Volatile aspect is important at high level of optimizations. You can find further details
about this aspect in the section about the Volatile and Atomic aspects (page 90).
Another important aspect that should be added is Import. When used in the context of object dec-
larations, it'll avoid default initialization which could overwrite the existing content while creating
the overlay — see an example in the admonition below. The declaration now becomes:
B : Bit_Field (0 .. V'Size - 1)
with
Address => V'Address, Import, Volatile;
3 procedure Simple_Bitfield is
4 type Bit_Field is array (Natural range <>) of Boolean with Pack;
5
6 V : Integer := 0;
7 B : Bit_Field (0 .. V'Size - 1)
8 with Address => V'Address, Import, Volatile;
9 begin
10 B (2) := True;
11 Put_Line ("V = " & Integer'Image (V));
12 end Simple_Bitfield;
Build output
Compile
[Ada] simple_bitfield.adb
Bind
[gprbind] simple_bitfield.bexch
[Ada] simple_bitfield.ali
Link
[link] simple_bitfield.adb
Runtime output
V = 4
In this example, we first initialize V with zero. Then, we use the bit-field B and set the third element
(B (2)) to True. This automatically sets bit #3 of V to 1. Therefore, as expected, the application
displays the message V = 4, which corresponds to 22 = 4.
Note that, in the declaration of the bit-field type above, we could also have used a positive range.
For example:
B : Bit_Field (1 .. V'Size)
with Address => V'Address, Import, Volatile;
The only difference in this case is that the first bit is B (1) instead of B (0).
In C, we would rely on bit-shifting and masking to set that specific bit:
[C]
7 v = v | (1 << 2);
8
11 return 0;
12 }
Runtime output
v = 4
Important
Ada has the concept of default initialization. For example, you may set the default value of record
components:
[Ada]
3 procedure Main is
4
10 R : Rec;
11 begin
12 Put_Line ("R.X = " & Integer'Image (R.X));
13 Put_Line ("R.Y = " & Integer'Image (R.Y));
14 end Main;
Build output
Compile
[Ada] main.adb
Bind
[gprbind] main.bexch
[Ada] main.ali
Link
[link] main.adb
Runtime output
R.X = 10
R.Y = 11
In the code above, we don't explicitly initialize the components of R, so they still have the default
values 10 and 11, which are displayed by the application.
Likewise, the Default_Value aspect can be used to specify the default value in other kinds of type
declarations. For example:
[Ada]
3 procedure Main is
4
8 P : Percentage;
9 begin
10 Put_Line ("P = " & Percentage'Image (P));
11 end Main;
Build output
Compile
[Ada] main.adb
Bind
[gprbind] main.bexch
[Ada] main.ali
Link
[link] main.adb
Runtime output
P = 10
When declaring an object whose type has a default value, the object will automatically be initialized
with the default value. In the example above, P is automatically initialized with 10, which is the
default value of the Percentage type.
Some types have an implicit default value. For example, access types have a default value of null.
As we've just seen, when declaring objects for types with associated default values, automatic ini-
tialization will happen. This can also happens when creating an overlay with the Address aspect.
The default value is then used to overwrite the content at the memory location indicated by the
address. However, in most situations, this isn't the behavior we expect, since overlays are usually
created to analyze and manipulate existing values. Let's look at an example where this happens:
[Ada]
3 package body P is
(continues on next page)
16 end P;
3 with P; use P;
4
5 procedure Main is
6 V : Integer := 10;
7 begin
8 Put_Line ("V = " & Integer'Image (V));
9 Display_Bytes_Increment (V);
10 Put_Line ("V = " & Integer'Image (V));
11 end Main;
Build output
Compile
[Ada] main.adb
[Ada] p.adb
p.adb:7:14: warning: default initialization of "Bf" may modify "V" [enabled by␣
↪default]
p.adb:7:14: warning: use pragma Import for "Bf" to suppress initialization (RM B.
↪1(24)) [enabled by default]
Bind
[gprbind] main.bexch
[Ada] main.ali
Link
[link] main.adb
Runtime output
V = 10
Byte = 0
Byte = 0
Byte = 0
Byte = 0
Now incrementing...
V = 1
initialization in the declaration because the object is imported. Let's look at the corrected example:
[Ada]
3 package body P is
4
16 end P;
3 with P; use P;
4
5 procedure Main is
6 V : Integer := 10;
7 begin
8 Put_Line ("V = " & Integer'Image (V));
9 Display_Bytes_Increment (V);
10 Put_Line ("V = " & Integer'Image (V));
11 end Main;
Build output
Compile
[Ada] main.adb
[Ada] p.adb
Bind
[gprbind] main.bexch
[Ada] main.ali
Link
[link] main.adb
Runtime output
V = 10
(continues on next page)
This unwanted side-effect of the initialization by the Default_Value aspect that we've just seen
can also happen in these cases:
• when we set a default value for components of a record type declaration,
• when we use the Default_Component_Value aspect for array types, or
• when we set use the Initialize_Scalars pragma for a package.
Again, using the Import aspect when declaring the overlay eliminates this side-effect.
We can use this pattern for objects of more complex data types like arrays or records. For example:
[Ada]
3 procedure Int_Array_Bitfield is
4 type Bit_Field is array (Natural range <>) of Boolean with Pack;
5
Build output
Compile
[Ada] int_array_bitfield.adb
Bind
[gprbind] int_array_bitfield.bexch
[Ada] int_array_bitfield.ali
Link
[link] int_array_bitfield.adb
Runtime output
A ( 1)= 4
A ( 2)= 0
In the Ada example above, we're using the bit-field to set bit #3 of the first element of the array
(A (1)). We could set bit #4 of the second element by using the size of the data type (in this case,
Integer'Size):
[Ada]
B (Integer'Size + 3) := True;
In C, we would select the specific array position and, again, rely on bit-shifting and masking to set
that specific bit:
[C]
15 return 0;
16 }
Runtime output
a[0] = 4
a[1] = 0
Since we can use this pattern for any arbitrary data type, this allows us to easily create a subpro-
gram to serialize data types and, for example, transmit complex data structures as a bitstream. For
example:
[Ada]
7 end Serializer;
15 begin
16 Put ("Bits: ");
(continues on next page)
23 end Serializer;
8 end My_Recs;
4 procedure Main is
5 R : Rec := (5, "abc");
6 B : Bit_Field (0 .. R'Size - 1)
7 with Address => R'Address, Import, Volatile;
8 begin
9 Transmit (B);
10 end Main;
Build output
Compile
[Ada] main.adb
main.adb:9:14: warning: volatile actual passed by copy (RM C.6(19)) [enabled by␣
↪default]
[Ada] my_recs.ads
[Ada] serializer.adb
Bind
[gprbind] main.bexch
[Ada] main.ali
Link
[link] main.adb
Runtime output
Bits: 1010000000000000000000000000000010000110010001101100011000000000
In this example, the Transmit procedure from Serializer package displays the individual bits
of a bit-field. We could have used this strategy to actually transmit the information as a bitstream.
In the main application, we call Transmit for the object R of record type Rec. Since Transmit has
the bit-field type as a parameter, we can use it for any type, as long as we have a corresponding
bit-field representation.
In C, we interpret the input pointer as an array of bytes, and then use shifting and masking to
access the bits of that byte. Here, we use the char type because it has a size of one byte in most
platforms.
[C]
3 #include <stdio.h>
4 #include <assert.h>
5
11 assert(sizeof(char) == 1);
12
13 printf("Bits: ");
14 for (i = 0; i < len / (sizeof(char) * 8); i++)
15 {
16 for (j = 0; j < sizeof(char) * 8; j++)
17 {
18 printf("%d", c[i] >> j & 1);
19 }
20 }
21 printf("\n");
22 }
3 #include "my_recs.h"
4 #include "serializer.h"
5
12 return 0;
13 }
Runtime output
Bits: 1010000000000000000000000000000010000110010001101100011000000000
Similarly, we can write a subprogram that converts a bit-field — which may have been received as
a bitstream — to a specific type. We can add a To_Rec subprogram to the My_Recs package to
convert a bit-field to the Rec type. This can be used to convert a bitstream that we received into
the actual data type representation.
As you know, we may write the To_Rec subprogram as a procedure or as a function. Since we
need to use slightly different strategies for the implementation, the following example has both
versions of To_Rec.
This is the updated code for the My_Recs package and the Main procedure:
[Ada]
7 end Serializer;
15 begin
16 Put ("Bits: ");
17 for E of B loop
18 Show_Bit (E);
19 end loop;
20 New_Line;
21 end Transmit;
22
23 end Serializer;
3 package My_Recs is
4
17 end My_Recs;
22 return R;
23 end To_Rec;
24
31 end My_Recs;
5 procedure Main is
6 R1 : Rec := (5, "abc");
7 R2 : Rec := (0, "zzz");
8
9 B1 : Bit_Field (0 .. R1'Size - 1)
10 with Address => R1'Address, Import, Volatile;
11 begin
12 Put ("R2 = ");
13 Display (R2);
14 New_Line;
15
Build output
Compile
[Ada] main.adb
main.adb:18:12: warning: volatile actual passed by copy (RM C.6(19)) [enabled by␣
↪default]
[Ada] my_recs.adb
[Ada] serializer.adb
Bind
[gprbind] main.bexch
[Ada] main.ali
Link
[link] main.adb
Runtime output
R2 = ( 0, zzz)
New bitstream received!
R2 = ( 5, abc)
In both versions of To_Rec, we declare the record object B_R as an overlay of the input bit-field. In
the procedure version of To_Rec, we then simply copy the data from B_R to the output parameter
R. In the function version of To_Rec, however, we need to declare a local record object R, which
we return after the assignment.
In C, we can interpret the input pointer as an array of bytes, and copy the individual bytes. For
example:
[C]
3 #include <stdio.h>
4 #include <assert.h>
5
9 printf("r2 = ");
10 display_r (&r2);
11 printf("\n");
12
20 return 0;
21 }
Runtime output
r2 = {0, zzz}
New bitstream received!
r2 = {5, abc}
Here, to_r casts both pointer parameters to pointers to char to get a byte-aligned pointer. Then,
it simply copies the data byte-by-byte.
Unchecked conversions are another way of converting between unrelated data types. This con-
version is done by instantiating the generic Unchecked_Conversions function for the types you
want to convert. Let's look at a simple example:
[Ada]
4 procedure Simple_Unchecked_Conversion is
5 type State is (Off, State_1, State_2)
6 with Size => Integer'Size;
7
13 I : Integer;
14 begin
15 I := As_Integer (State_2);
16 Put_Line ("I = " & Integer'Image (I));
17 end Simple_Unchecked_Conversion;
Build output
Compile
[Ada] simple_unchecked_conversion.adb
Bind
[gprbind] simple_unchecked_conversion.bexch
[Ada] simple_unchecked_conversion.ali
Link
[link] simple_unchecked_conversion.adb
Runtime output
I = 64
3 procedure Simple_Overlay is
4 type State is (Off, State_1, State_2)
5 with Size => Integer'Size;
6
7 for State use (Off => 0, State_1 => 32, State_2 => 64);
8
9 S : State;
10 I : Integer
11 with Address => S'Address, Import, Volatile;
12 begin
13 S := State_2;
14 Put_Line ("I = " & Integer'Image (I));
15 end Simple_Overlay;
Build output
Compile
[Ada] simple_overlay.adb
Bind
[gprbind] simple_overlay.bexch
[Ada] simple_overlay.ali
Link
[link] simple_overlay.adb
Runtime output
I = 64
Let's look at another example of converting between different numeric formats. In this case, we
want to convert between a 16-bit fixed-point and a 16-bit integer data type. This is how we can do
it using Unchecked_Conversion:
[Ada]
4 procedure Fixed_Int_Unchecked_Conversion is
5 Delta_16 : constant := 1.0 / 2.0 ** (16 - 1);
6 Max_16 : constant := 2 ** 15;
7
18 I : Int_16 := 0;
19 F : Fixed_16 := 0.0;
20 begin
21 F := Fixed_16'Last;
22 I := As_Int_16 (F);
23
Build output
Compile
[Ada] fixed_int_unchecked_conversion.adb
Bind
[gprbind] fixed_int_unchecked_conversion.bexch
[Ada] fixed_int_unchecked_conversion.ali
Link
[link] fixed_int_unchecked_conversion.adb
Runtime output
F = 0.99997
I = 32767
Here, we instantiate Unchecked_Conversion for the Int_16 and Fixed_16 types, and we call
the instantiated functions explicitly. In this case, we call As_Int_16 to get the integer value corre-
sponding to Fixed_16'Last.
This is how we can rewrite the implementation above using overlays:
[Ada]
3 procedure Fixed_Int_Overlay is
4 Delta_16 : constant := 1.0 / 2.0 ** (16 - 1);
5 Max_16 : constant := 2 ** 15;
6
12 I : Int_16 := 0;
13 F : Fixed_16
14 with Address => I'Address, Import, Volatile;
15 begin
16 F := Fixed_16'Last;
17
Build output
Compile
[Ada] fixed_int_overlay.adb
Bind
[gprbind] fixed_int_overlay.bexch
[Ada] fixed_int_overlay.ali
Link
[link] fixed_int_overlay.adb
Runtime output
F = 0.99997
I = 32767
Here, the conversion to the integer value is implicit, so we don't need to call a conversion function.
Using Unchecked_Conversion has the advantage of making it clear that a conversion is happen-
ing, since the conversion is written explicitly in the code. With overlays, that conversion is auto-
matic and therefore implicit. In that sense, using Unchecked_Conversion is a cleaner and safer
approach. On the other hand, Unchecked_Conversion requires a copy, so it's less efficient than
overlays, where no copy is performed — because one change in the source object is automatically
reflected in the target object (and vice-versa). In the end, the choice between unchecked conver-
sions and overlays depends on the level of performance that you want to achieve.
Also note that Unchecked_Conversion can only be instantiated for constrained types. In order
to rewrite the examples using bit-fields that we've seen in the previous section, we cannot simply
instantiate Unchecked_Conversion with the Target indicating the unconstrained bit-field, such
as:
Instead, we have to declare a subtype for the specific range we're interested in. This is how we can
rewrite one of the previous examples:
[Ada]
4 procedure Simple_Bitfield_Conversion is
5 type Bit_Field is array (Natural range <>) of Boolean with Pack;
6
7 V : Integer := 4;
8
23 B : Integer_Bit_Field;
24 begin
25 B := As_Bit_Field (V);
26
Build output
Compile
[Ada] simple_bitfield_conversion.adb
Bind
[gprbind] simple_bitfield_conversion.bexch
[Ada] simple_bitfield_conversion.ali
Link
[link] simple_bitfield_conversion.adb
Runtime output
V = 4
In this example, we first declare the subtype Integer_Bit_Field as a bit-field with a length that
fits the V variable we want to convert to. Then, we can use that subtype in the instantiation of
Unchecked_Conversion.
SEVEN
It is common to see embedded software being used in a variety of configurations that require small
changes to the code for each instance. For example, the same application may need to be portable
between two different architectures (ARM and x86), or two different platforms with different set of
devices available. Maybe the same application is used for two different generations of the product,
so it needs to account for absence or presence of new features, or it's used for different projects
which may select different components or configurations. All these cases, and many others, require
variability in the software in order to ensure its reusability.
In C, variability is usually achieved through macros and function pointers, the former being tied to
static variability (variability in different builds) the latter to dynamic variability (variability within the
same build decided at run-time).
Ada offers many alternatives for both techniques, which aim at structuring possible variations of
the software. When Ada isn't enough, the GNAT compilation system also provides a layer of capa-
bilities, in particular selection of alternate bodies.
If you're familiar with object-oriented programming (OOP) — supported in languages such as C++
and Java —, you might also be interested in knowing that OOP is supported by Ada and can be
used to implement variability. This should, however, be used with care, as OOP brings its own set
of problems, such as loss of efficiency — dispatching calls can't be inlined and require one level
of indirection — or loss of analyzability — the target of a dispatching call isn't known at run time.
As a rule of thumb, OOP should be considered only for cases of dynamic variability, where several
versions of the same object need to exist concurrently in the same application.
7.2.1 Genericity
One usage of C macros involves the creation of functions that works regardless of the type they're
being called upon. For example, a swap macro may look like:
[C]
Listing 1: main.c
1 #include <stdio.h>
2 #include <stdlib.h>
3
155
Ada for the Embedded C Developer, Release 2021-06
10 int main()
11 {
12 int a = 10;
13 int b = 42;
14
21 return 0;
22 }
Runtime output
a = 10, b = 42
a = 42, b = 10
Ada offers a way to declare this kind of functions as a generic, that is, a function that is written after
static arguments, such as a parameter:
[Ada]
Listing 2: main.adb
1 with Ada.Text_IO; use Ada.Text_IO;
2
3 procedure Main is
4
5 generic
6 type A_Type is private;
7 procedure Swap (Left, Right : in out A_Type);
8
18 A : Integer := 10;
19 B : Integer := 42;
20
21 begin
22 Put_Line ("A = "
23 & Integer'Image (A)
24 & ", B = "
25 & Integer'Image (B));
26
Build output
Compile
[Ada] main.adb
Bind
[gprbind] main.bexch
[Ada] main.ali
Link
[link] main.adb
Runtime output
A = 10, B = 42
A = 42, B = 10
There are a few key differences between the C and the Ada version here. In C, the macro can be
used directly and essentially get expanded by the preprocessor without any kind of checks. In Ada,
the generic will first be checked for internal consistency. It then needs to be explicitly instantiated
for a concrete type. From there, it's exactly as if there was an actual version of this Swap function,
which is going to be called as any other function. All rules for parameter modes and control will
apply to this instance.
In many respects, an Ada generic is a way to provide a safe specification and implementation of
such macros, through both the validation of the generic itself and its usage.
Subprograms aren't the only entities that can me made generic. As a matter of fact, it's much more
common to render an entire package generic. In this case the instantiation creates a new version
of all the entities present in the generic, including global variables. For example:
[Ada]
Listing 3: gen.ads
1 generic
2 type T is private;
3 package Gen is
4 type C is tagged record
5 V : T;
6 end record;
7
8 G : Integer;
9 end Gen;
Listing 4: main.adb
1 with Gen;
2
3 procedure Main is
4 package I1 is new Gen (Integer);
5 package I2 is new Gen (Integer);
6 subtype Str10 is String (1 .. 10);
7 package I3 is new Gen (Str10);
8 begin
9 I1.G := 0;
10 I2.G := 1;
11 I3.G := 2;
12 end Main;
Build output
Compile
[Ada] main.adb
[Ada] gen.ads
Bind
[gprbind] main.bexch
[Ada] main.ali
Link
[link] main.adb
Listing 5: sort.ads
1 generic
2 type Component is private;
3 type Index is (<>);
4 with function "<" (Left, Right : Component) return Boolean;
5 type Array_Type is array (Index range <>) of Component;
6 procedure Sort (A : in out Array_Type);
The declaration above states that we need a type (Component), a discrete type (Index), a compari-
son subprogram ("<"), and an array definition (Array_Type). Given these, it's possible to write an
algorithm that can sort any Array_Type. Note the usage of the with reserved word in front of the
function name: it exists to differentiate between the generic parameter and the beginning of the
generic subprogram.
Here is a non-exhaustive overview of the kind of constraints that can be put on types:
For a more complete list please reference the Generic Formal Types Appendix20 .
Let's take a case where a codebase needs to handle small variations of a given device, or maybe
different generations of a device, depending on the platform it's running on. In this example, we're
assuming that each platform will lead to a different binary, so the code can statically resolve which
set of services are available. However, we want an easy way to implement a new device based on a
previous one, saying "this new device is the same as this previous device, with these new services
and these changes in existing services".
We can implement such patterns using Ada's simple derivation — as opposed to tagged derivation,
which is OOP-related and discussed in a later section.
Let's start from the following example:
20 https://learn.adacore.com/courses/intro-to-ada/chapters/appendices.html#appendix-a-generic-formal-types
[Ada]
Listing 6: drivers_1.ads
1 package Drivers_1 is
2
9 end Drivers_1;
Listing 7: drivers_1.adb
1 package body Drivers_1 is
2
17 end Drivers_1;
In the above example, Device_1 is an empty record type. It may also have some fields if required,
or be a different type such as a scalar. Then the four procedures Startup, Send, Send_Fast and
Receive are primitives of this type. A primitive is essentially a subprogram that has a parameter
or return type directly referencing this type and declared in the same scope. At this stage, there's
nothing special with this type: we're using it as we would use any other type. For example:
[Ada]
Listing 8: main.adb
1 with Ada.Text_IO; use Ada.Text_IO;
2 with Drivers_1; use Drivers_1;
3
4 procedure Main is
5 D : Device_1;
6 I : Integer;
7 begin
8 Startup (D);
9 Send_Fast (D, 999);
10 Receive (D, I);
11 Put_Line (Integer'Image (I));
12 end Main;
Build output
Compile
[Ada] main.adb
[Ada] drivers_1.adb
(continues on next page)
Runtime output
42
Let's now assume that we need to implement a new generation of device, Device_2. This new
device works exactly like the first one, except for the startup code that has to be done differently.
We can create a new type that operates exactly like the previous one, but modifies only the behavior
of Startup:
[Ada]
Listing 9: drivers_2.ads
1 with Drivers_1; use Drivers_1;
2
3 package Drivers_2 is
4
7 overriding
8 procedure Startup (Device : Device_2);
9
10 end Drivers_2;
3 overriding
4 procedure Startup (Device : Device_2) is null;
5
6 end Drivers_2;
Here, Device_2 is derived from Device_1. It contains all the exact same properties and primitives,
in particular, Startup, Send, Send_Fast and Receive. However, here, we decided to change the
Startup function and to provide a different implementation. We override this function. The main
subprogram doesn't change much, except for the fact that it now relies on a different type:
[Ada]
4 procedure Main is
5 D : Device_2;
6 I : Integer;
7 begin
8 Startup (D);
9 Send_Fast (D, 999);
10 Receive (D, I);
11 Put_Line (Integer'Image (I));
12 end Main;
Build output
Compile
[Ada] main.adb
[Ada] drivers_2.adb
[Ada] drivers_1.adb
Bind
[gprbind] main.bexch
[Ada] main.ali
Link
[link] main.adb
Runtime output
42
We can continue with this approach and introduce a new generation of devices. This new device
doesn't implement the Send_Fast service so we want to remove it from the list of available ser-
vices. Furthermore, for the purpose of our example, let's assume that the hardware team went
back to the Device_1 way of implementing Startup. We can write this new device the following
way:
[Ada]
3 package Drivers_3 is
4
7 overriding
8 procedure Startup (Device : Device_3);
9
13 end Drivers_3;
3 overriding
4 procedure Startup (Device : Device_3) is null;
5
6 end Drivers_3;
The is abstract definition makes illegal any call to a function, so calls to Send_Fast on Device_3
will be flagged as being illegal. To then implement Startup of Device_3 as being the same as the
Startup of Device_1, we can convert the type in the implementation:
[Ada]
3 overriding
4 procedure Startup (Device : Device_3) is
5 begin
6 Drivers_1.Startup (Device_1 (Device));
7 end Startup;
(continues on next page)
9 end Drivers_3;
4 procedure Main is
5 D : Device_3;
6 I : Integer;
7 begin
8 Startup (D);
9 Send_Fast (D, 999);
10 Receive (D, I);
11 Put_Line (Integer'Image (I));
12 end Main;
Compilation output
7 end Drivers_1;
11 end Drivers_1;
3 package Drivers_2 is
4
9 end Drivers_2;
11 end Drivers_2;
4 procedure Main is
5 D : Transceiver;
6 I : Integer;
7 begin
8 Send (D, 999);
9 Receive (D, I);
10 Put_Line (Integer'Image (I));
11 end Main;
Build output
Compile
[Ada] main.adb
[Ada] drivers.ads
[Ada] drivers_1.adb
Bind
[gprbind] main.bexch
[Ada] main.ali
Link
[link] main.adb
Runtime output
42
In the above example, the whole code can rely on drivers.ads, instead of relying on the specific
driver. Here, Drivers is another name for Driver_1. In order to switch to Driver_2, the project
only has to replace that one drivers.ads file.
In the following section, we'll go one step further and demonstrate that this selection can be done
through a configuration switch selected at build time instead of a manual code modification.
Configuration pragmas are a set of pragmas that modify the compilation of source-code files. You
may use them to either relax or strengthen requirements. For example:
In this example, we're suppressing the overflow check, thereby relaxing a requirement. Normally,
the following program would raise a constraint error due to a failed overflow check:
[Ada]
4 procedure Main is
5 I : Integer := Integer'Last;
6 begin
7 I := Add_Max (I);
8 Put_Line ("I = " & Integer'Image (I));
9 end Main;
Build output
Compile
[Ada] main.adb
[Ada] p.adb
Bind
[gprbind] main.bexch
[Ada] main.ali
Link
[link] main.adb
Runtime output
When suppressing the overflow check, however, the program doesn't raise an exception, and the
value that Add_Max returns is -2, which is a wraparound of the sum of the maximum integer values
(Integer'Last + Integer'Last).
We could also strengthen requirements, as in this example:
Here, the restriction forbids the use of floating-point types and objects. The following program
would violate this restriction, so the compiler isn't able to compile the program when the restriction
is used:
procedure Main is
F : Float := 0.0;
-- Declaration is not possible with No_Floating_Point restriction.
begin
null;
end Main;
Restrictions are especially useful for high-integrity applications. In fact, the Ada Reference Manual
has a separate section for them21 .
When creating a project, it is practical to list all configuration pragmas in a separate file. This is
called a configuration pragma file, and it usually has an .adc file extension. If you use GPRbuild
for building Ada applications, you can specify the configuration pragma file in the corresponding
project file. For example, here we indicate that gnat.adc is the configuration pragma file for our
project:
project Default is
package Compiler is
for Local_Configuration_Pragmas use "gnat.adc";
end Compiler;
end Default;
In C, preprocessing flags are used to create blocks of code that are only compiled under certain
circumstances. For example, we could have a block that is only used for debugging:
[C]
4 int func(int x)
5 {
6 return x % 4;
(continues on next page)
21 http://www.ada-auth.org/standards/12rm/html/RM-H-4.html
9 int main()
10 {
11 int a, b;
12
13 a = 10;
14 b = func(a);
15
16 #ifdef DEBUG
17 printf("func(%d) => %d\n", a, b);
18 #endif
19
20 return 0;
21 }
Here, the block indicated by the DEBUG flag is only included in the build if we define this preprocess-
ing flag, which is what we expect for a debug version of the build. In the release version, however,
we want to keep debug information out of the build, so we don't use this flag during the build
process.
Ada doesn't define a preprocessor as part of the language. Some Ada toolchains — like the GNAT
toolchain — do have a preprocessor that could create code similar to the one we've just seen.
When programming in Ada, however, the recommendation is to use configuration packages to
select code blocks that are meant to be included in the application.
When using a configuration package, the example above can be written as:
[Ada]
5 end Config;
5 procedure Main is
6 A, B : Integer;
7 begin
8 A := 10;
9 B := Func (A);
(continues on next page)
11 if Config.Debug then
12 Put_Line ("Func(" & Integer'Image (A) & ") => "
13 & Integer'Image (B));
14 end if;
15 end Main;
Build output
Compile
[Ada] main.adb
[Ada] config.ads
[Ada] func.adb
Bind
[gprbind] main.bexch
[Ada] main.ali
Link
[link] main.adb
In this example, Config is a configuration package. The version of Config we're seeing here is
the release version. The debug version of the Config package looks like this:
package Config is
end Config;
The compiler makes sure to remove dead code. In the case of the release version, since Config.
Debug is constant and set to False, the compiler is smart enough to remove the call to Put_Line
from the build.
As you can see, both versions of Config are very similar to each other. The general idea is to create
packages that declare the same constants, but using different values.
In C, we differentiate between the debug and release versions by selecting the appropriate prepro-
cessing flags, but in Ada, we select the appropriate configuration package during the build process.
Since the file name is usually the same (config.ads for the example above), we may want to store
them in distinct directories. For the example above, we could have:
• src/debug/config.ads for the debug version, and
• src/release/config.ads for the release version.
Then, we simply select the appropriate configuration package for each version of the build by indi-
cating the correct path to it. When using GPRbuild, we can select the appropriate directory where
the config.ads file is located. We can use scenario variables in our project, which allow for cre-
ating different versions of a build. For example:
project Default is
end Default;
In this example, we're defining a scenario type called Mode_Type. Then, we're declaring the sce-
nario variable Mode and using it in the Source_Dirs declaration to complete the path to the sub-
directory containing the config.ads file. The expression "src/" & Mode concatenates the user-
specified mode to select the appropriate subdirectory.
We can then set the mode on the command-line. For example:
In addition to selecting code blocks for the build, we could also specify values that depend on
the target build. For our example above, we may want to create two versions of the application,
each one having a different version of a MOD_VALUE that is used in the implementation of func().
In C, we can achieve this by using preprocessing flags and defining the corresponding version in
APP_VERSION. Then, depending on the value of APP_VERSION, we define the corresponding value
of MOD_VALUE.
[C]
5 #if APP_VERSION == 1
6 #define MOD_VALUE 4
7 #endif
8
9 #if APP_VERSION == 2
10 #define MOD_VALUE 5
11 #endif
4 #include "defs.h"
5
6 int func(int x)
7 {
8 return x % MOD_VALUE;
9 }
10
11 int main()
12 {
13 int a, b;
14
15 a = 10;
16 b = func(a);
17
18 return 0;
19 }
If not defined outside, the code above will compile version #1 of the application. We can change
this by specifying a value for APP_VERSION during the build (e.g. as a Makefile switch).
For the Ada version of this code, we can create two configuration packages for each version of the
application. For example:
[Ada]
3 package App_Defs is
4
7 end App_Defs;
3 procedure Main is
4 A, B : Integer;
5 begin
6 A := 10;
7 B := Func (A);
8 end Main;
Build output
Compile
[Ada] main.adb
[Ada] func.adb
[Ada] app_defs.ads
Bind
[gprbind] main.bexch
[Ada] main.ali
Link
[link] main.adb
The code above shows the version #1 of the configuration package. The corresponding implemen-
tation for version #2 looks like this:
-- ./src/app_2/app_defs.ads
package App_Defs is
end App_Defs;
Again, we just need to select the appropriate configuration package for each version of the build,
which we can easily do when using GPRbuild.
In basic terms, records with discriminants are records that include "parameters" in their type defi-
nitions. This allows for adding more flexibility to the type definition. In the section about pointers
(page 132), we've seen this example:
[Ada]
8 V : S (9);
9 begin
10 null;
11 end Main;
Build output
Compile
[Ada] main.adb
main.adb:8:04: warning: variable "V" is never read and never assigned [-gnatwv]
Bind
[gprbind] main.bexch
[Ada] main.ali
Link
[link] main.adb
Here, Last is the discriminant for type S. When declaring the variable V as S (9), we specify the
actual index of the last position of the array component A by setting the Last discriminant to 9.
We can create an equivalent implementation in C by declaring a struct with a pointer to an array:
[C]
4 typedef struct {
5 int * a;
6 const int last;
7 } S;
8
Here, we need to explicitly allocate the a array of the S struct via a call to malloc(), which allocates
memory space on the heap. In the Ada version, in contrast, the array (V.A) is allocated on the stack
and we don't need to explicitly allocate it.
Note that the information that we provide as the discriminant to the record type (in the Ada code)
is constant, so we cannot assign a value to it. For example, we cannot write:
[Ada]
V.Last := 10; -- COMPILATION ERROR!
In the C version, we declare the last field constant to get the same behavior.
[C]
v.last = 10; // COMPILATION ERROR!
Note that the information provided as discriminants is visible. In the example above, we could
display Last by writing:
[Ada]
Put_Line ("Last : " & Integer'Image (V.Last));
Also note that, even if a type is private, we can still access the information of the discriminants if
they are visible in the public part of the type declaration. Let's rewrite the example above:
[Ada]
6 private
7 type S (Last : Integer) is record
8 A : Arr (0 .. Last);
9 end record;
10
11 end Array_Definition;
4 procedure Main is
5 V : S (9);
6 begin
7 Put_Line ("Last : " & Integer'Image (V.Last));
8 end Main;
Build output
Compile
[Ada] main.adb
main.adb:5:04: warning: variable "V" is read but never assigned [-gnatwv]
(continues on next page)
Runtime output
Last : 9
Even though the S type is now private, we can still display Last because this discriminant is visible
in the non-private part of package Array_Definition.
In simple terms, a variant record — a discriminated record in Ada terminology — is a record with
discriminants that allows for changing its structure. Basically, it's a record containing a case. This
is the general structure:
[Ada]
type Var_Rec (V : F) is record
case V is
when Opt_1 => F1 : Type_1;
when Opt_2 => F2 : Type_2;
end case;
end record;
3 procedure Main is
4
Build output
Compile
[Ada] main.adb
Bind
[gprbind] main.bexch
[Ada] main.ali
Link
[link] main.adb
Runtime output
Here, we declare F containing a floating-point value, and I containing an integer value. In the
Display procedure, we present the correct information to the user according to the Use_Float
discriminant of the Float_Int type.
We can implement this example in C by using unions:
[C]
4 typedef struct {
5 int use_float;
6 union {
7 float f;
8 int i;
9 };
10 } float_int;
11
16 v.use_float = 1;
17 v.f = f;
18 return v;
19 }
20
25 v.use_float = 0;
26 v.i = i;
27 return v;
28 }
29
45 display (f);
46 display (i);
47
48 return 0;
49 }
Runtime output
Similar to the Ada code, we declare f containing a floating-point value, and i containing an integer
value. One difference is that we use the init_float() and init_int() functions to initialize the
float_int struct. These functions initialize the correct field of the union and set the use_float
field accordingly.
There is, however, a difference in accessibility between variant records in Ada and unions in C. In
C, we're allowed to access any field of the union regardless of the initialization:
[C]
This feature is useful to create overlays. In this specific example, however, the information dis-
played to the user doesn't make sense, since the union was initialized with a floating-point value
(v.f) and, by accessing the integer field (v.i), we're displaying it as if it was an integer value.
In Ada, accessing the wrong component would raise an exception at run-time ("discriminant check
failed"), since the component is checked before being accessed:
[Ada]
Using this method prevents wrong information being used in other parts of the program.
To get the same behavior in Ada as we do in C, we need to explicitly use the Unchecked_Union
aspect in the type declaration. This is the modified example:
[Ada]
3 procedure Main is
4
15 begin
16 Put_Line ("Integer value: " & Integer'Image (V.I));
17 end Main;
Build output
Compile
[Ada] main.adb
Bind
[gprbind] main.bexch
[Ada] main.ali
Link
[link] main.adb
Runtime output
Now, we can display the integer component (V.I) even though we initialized the floating-point
component (V.F). As expected, the information displayed by the test application in this case doesn't
make sense.
Note that, when using the Unchecked_Union aspect in the declaration of a variant record, the ref-
erence discriminant is not available anymore, since it isn't stored as part of the record. Therefore,
we cannot access the Use_Float discriminant as in the following code:
[Ada]
Unchecked unions are particularly useful in Ada when creating bindings for C code.
We can also use variant records to specify optional components of a record. For example:
[Ada]
3 procedure Main is
4 type Arr is array (Integer range <>) of Integer;
5
11 case Has_Extra_Info is
12 when No => null;
13 when Yes => B : Arr (0 .. Last);
14 end case;
15 end record;
16
Build output
Compile
[Ada] main.adb
main.adb:17:04: warning: variable "V1" is read but never assigned [-gnatwv]
main.adb:18:04: warning: variable "V2" is read but never assigned [-gnatwv]
Bind
[gprbind] main.bexch
[Ada] main.ali
Link
[link] main.adb
Runtime output
Here, in the declaration of S_Var, we don't have any component in case Has_Extra_Info is false.
The component is simply set to null in this case.
When running the example above, we see that the size of V1 is greater than the size of V2 due to
the extra B component — which is only included when Has_Extra_Info is true.
We can use optional components to prevent subprograms from generating invalid information that
could be misused by the caller. Consider the following example:
[C]
40 return 0;
41 }
Runtime output
Calculation error!
Value = 0.500000
In this code, we're using the output parameter success of the calculate() function to indicate
whether the calculation was successful or not. This approach has a major problem: there's no
way to prevent that the invalid value returned by calculate() in case of an error is misused in
another computation. For example:
[C]
return 0;
}
We cannot prevent access to the returned value or, at least, force the caller to evaluate success
before using the returned value.
This is the corresponding code in Ada:
[Ada]
3 procedure Main is
4
26 F : Float;
27 Success : Boolean;
28 begin
29 F := Calculate (1.0, 0.5, Success);
30 Display (F, Success);
31
Build output
Compile
[Ada] main.adb
(continues on next page)
Runtime output
Calculation error!
Value = 5.00000E-01
The Ada code above suffers from the same drawbacks as the C code. Again, there's no way to
prevent misuse of the invalid value returned by Calculate in case of errors.
However, in Ada, we can use variant records to make the component unavailable and therefore
prevent misuse of this information. Let's rewrite the original example and wrap the returned value
in a variant record:
[Ada]
3 procedure Main is
4
30 begin
31 Display (Calculate (1.0, 0.5));
32 Display (Calculate (0.5, 1.0));
33 end Main;
Build output
Compile
[Ada] main.adb
Bind
(continues on next page)
Runtime output
Calculation error!
Value = 5.00000E-01
In this example, we can determine whether the calculation was successful or not by evaluating the
Success component of the Opt_Float. If the calculation wasn't successful, we won't be able to ac-
cess the F component of the Opt_Float. As mentioned before, trying to access the component in
this case would raise an exception. Therefore, in case of errors, we can ensure that no information
is misused after the call to Calculate.
In the previous section (page 170), we've seen that we can add variability to records by using dis-
criminants. Another approach is to use tagged records, which are the base for object-oriented
programming in Ada.
A tagged record type is declared by adding the tagged keyword. For example:
[Ada]
11 R1 : Rec;
12 R2 : Tagged_Rec;
13
Build output
Compile
[Ada] main.adb
Bind
[gprbind] main.bexch
[Ada] main.ali
Link
[link] main.adb
In this simple example, there isn't much difference between the Rec and Tagged_Rec type. How-
ever, tagged types can be derived and extended. For example:
[Ada]
23 R1 : Rec;
24 R2 : Tagged_Rec;
25 R3 : Ext_Tagged_Rec;
26
Build output
Compile
[Ada] main.adb
Bind
[gprbind] main.bexch
[Ada] main.ali
Link
[link] main.adb
As indicated in the example, a type derived from an untagged type cannot have an extension. The
compiler indicates this error if you uncomment the declaration of the Ext_Rec type above. In
contrast, we can extend a tagged type, as we did in the declaration of Ext_Tagged_Rec. In this
case, Ext_Tagged_Rec has all the components of the Tagged_Rec type (V, in this case) plus the
additional components from its own type declaration (V2, in this case).
Previously, we've seen that subprograms can be overriden. For example, if we had implemented a
Reset and a Display procedure for the Rec type that we declared above, these procedures would
be available for an Ext_Rec type derived from Rec. Also, we could override these procedures for
the Ext_Rec type. In Ada, we don't need object-oriented programming features to do that: simple
(untagged) records can be used to derive types, inherit operations and override them. However, in
applications where the actual subprogram to be called is determined dynamically at run-time, we
need dispatching calls. In this case, we must use tagged types to implement this.
Let's discuss the similarities and differences between untagged and tagged types based on this
example:
[Ada]
30 end P;
3 package body P is
4
62 end P;
4 procedure Main is
(continues on next page)
22 --
23 -- Use new operations when available
24 --
25 New_Op (X_New_Rec);
26 X_Ext_Tagged_Rec.New_Op;
27
28 --
29 -- Display all objects
30 --
31 Display (X_Rec);
32 Display (X_New_Rec);
33 X_Tagged_Rec.Display; -- we could write "Display (X_Tagged_Rec)" as well
34 X_Ext_Tagged_Rec.Display;
35
36 --
37 -- Resetting and display objects of Tagged_Rec'Class
38 --
39 Put_Line ("Operations on Tagged_Rec'Class");
40 Put_Line ("------------------------------");
41 for E of X_Tagged_Rec_Array loop
42 E.Reset;
43 E.Display;
44 end loop;
45 end Main;
Build output
Compile
[Ada] main.adb
[Ada] p.adb
Bind
[gprbind] main.bexch
[Ada] main.ali
Link
[link] main.adb
Runtime output
TYPE: REC
Rec.V = 0
TYPE: NEW_REC
New_Rec.V = 1
TYPE: P.EXT_TAGGED_REC
Ext_Tagged_Rec.V = 1
Ext_Tagged_Rec.V2 = 0
Operations on Tagged_Rec'Class
------------------------------
TYPE: P.TAGGED_REC
Tagged_Rec.V = 0
TYPE: P.EXT_TAGGED_REC
Ext_Tagged_Rec.V = 0
Ext_Tagged_Rec.V2 = 0
Let's look more closely at the dispatching calls implemented above. First, we declare the
X_Tagged_Rec_Array array and initialize it with the access to objects of both parent and derived
tagged types:
[Ada]
Here, we use the aliased keyword to be able to get access to the objects (via the 'Access at-
tribute).
Then, we loop over this array and call the Reset and Display procedures:
[Ada]
Since we're using dispatching calls, the actual procedure that is selected depends on the type of
the object. For the first element (X_Tagged_Rec_Array (1)), this is Tagged_Rec, while for the
second element (X_Tagged_Rec_Array (2)), this is Ext_Tagged_Rec.
Dispatching calls are only possible for a type class — for example, the Tagged_Rec'Class. When
the type of an object is known at compile time, the calls won't dispatch at runtime. For example, the
call to the Reset procedure of the X_Ext_Tagged_Rec object (X_Ext_Tagged_Rec.Reset) will
always take the overriden Reset procedure of the Ext_Tagged_Rec type. Similarly, if we perform
a view conversion by writing Tagged_Rec (A_Ext_Tagged_Rec).Display, we're instructing the
compiler to interpret A_Ext_Tagged_Rec as an object of type Tagged_Rec, so that the compiler
selects the Display procedure of the Tagged_Rec type.
7.3.3.5 Interfaces
Another useful feature of object-oriented programming is the use of interfaces. In this case, we
can define abstract operations, and implement them in the derived tagged types. We declare an
interface by simply writing type T is interface. For example:
[Ada]
All operations on an interface type are abstract, so we need to write is abstract in the signature
— as we did in the declaration of Op above. Also, since interfaces are abstract types and don't have
an actual implementation, we cannot declare objects for it.
We can derive tagged types from an interface and implement the actual operations of that inter-
face:
[Ada]
Note that we're not using the tagged keyword in the declaration because any type derived from
an interface is automatically tagged.
Let's look at an example with an interface and two derived tagged types:
[Ada]
12 end P;
3 package body P is
4
17 end P;
3 procedure Main is
4 D_Small : Small_Display_Type;
5 D_Big : Big_Display_Type;
6
12 begin
13 Dispatching_Display (D_Small);
14 Dispatching_Display (D_Big);
15 end Main;
Build output
Compile
[Ada] main.adb
[Ada] p.adb
Bind
[gprbind] main.bexch
[Ada] main.ali
Link
[link] main.adb
Runtime output
Using Small_Display_Type
Using Big_Display_Type
In this example, we have an interface type Display_Interface and two tagged types that are
derived from Display_Interface: Small_Display_Type and Big_Display_Type.
Both types (Small_Display_Type and Big_Display_Type) implement the interface by overrid-
ing the Display procedure. Then, in the inner procedure Dispatching_Display of the Main
procedure, we perform a dispatching call depending on the actual type of D.
We may derive a type from multiple interfaces by simply writing type Derived_T is new T1
and T2 with null record. For example:
[Ada]
17 end Transceivers;
17 end Transceivers;
3 procedure Main is
4 D : Transceiver;
5 begin
(continues on next page)
Build output
Compile
[Ada] main.adb
[Ada] transceivers.adb
Bind
[gprbind] main.bexch
[Ada] main.ali
Link
[link] main.adb
Runtime output
Sending data...
Receiving data...
In this example, we're declaring two interfaces (Send_Interface and Receive_Interface) and
the tagged type Transceiver that derives from both interfaces. Since we need to implement the
interfaces, we implement both Send and Receive for Transceiver.
We may also declare abstract tagged types. Note that, because the type is abstract, we cannot use
it to declare objects for it — this is the same as for interfaces. We can only use it to derive other
types. Let's look at the abstract tagged type declared in the Abstract_Transceivers package:
[Ada]
3 package Abstract_Transceivers is
4
11 end Abstract_Transceivers;
11 end Abstract_Transceivers;
3 procedure Main is
4 D : Abstract_Transceiver;
5 begin
6 D.Send;
7 D.Receive;
8 end Main;
Compilation output
main.adb:4:09: error: type of object cannot be abstract
main.adb:7:06: error: call to abstract procedure must be dispatching
In this example, we declare the abstract tagged type Abstract_Transceiver. Here, we're only
partially implementing the interfaces from which this type is derived: we're implementing Send,
but we're skipping the implementation of Receive. Therefore, Receive is an abstract operation
of Abstract_Transceiver. Since any tagged type that has abstract operations is abstract, we
must indicate this by adding the abstract keyword in type declaration.
Also, when compiling this example, we get an error because we're trying to declare an object of
Abstract_Transceiver (in the Main procedure), which is not possible. Naturally, if we derive
another type from Abstract_Transceiver and implement Receive as well, then we can declare
objects of this derived type. This is what we do in the Full_Transceivers below:
[Ada]
3 package Full_Transceivers is
4
8 end Full_Transceivers;
11 end Full_Transceivers;
3 procedure Main is
4 D : Full_Transceiver;
5 begin
(continues on next page)
Build output
Compile
[Ada] main.adb
[Ada] full_transceivers.adb
[Ada] abstract_transceivers.adb
[Ada] transceivers.adb
Bind
[gprbind] main.bexch
[Ada] main.ali
Link
[link] main.adb
Runtime output
Sending data...
Receiving data...
Here, we implement the Receive procedure for the Full_Transceiver. Therefore, the type
doesn't have any abstract operation, so we can use it to declare objects.
In the section about simple derivation (page 158), we've seen an example where the actual selection
was done at implementation time by renaming one of the packages:
[Ada]
with Drivers_1;
Although this approach is useful in many cases, there might be situations where we need to select
the actual driver dynamically at runtime. Let's look at how we could rewrite that example using
interfaces, tagged types and dispatching calls:
[Ada]
9 end Drivers_Base;
3 package Drivers_1 is
(continues on next page)
11 end Drivers_1;
19 end Drivers_1;
3 package Drivers_2 is
4
11 end Drivers_2;
19 end Drivers_2;
3 with Drivers_Base;
4 with Drivers_1;
5 with Drivers_2;
6
7 procedure Main is
8 D1 : aliased Drivers_1.Transceiver;
9 D2 : aliased Drivers_2.Transceiver;
10 D : access Drivers_Base.Transceiver'Class;
11
12 I : Integer;
13
26 begin
27 Select_Driver (1);
28 D.Send (999);
29 D.Receive (I);
30 Put_Line (Integer'Image (I));
31
32 Select_Driver (2);
33 D.Send (999);
34 D.Receive (I);
35 Put_Line (Integer'Image (I));
36 end Main;
Build output
Compile
[Ada] main.adb
[Ada] drivers_1.adb
[Ada] drivers_2.adb
[Ada] drivers_base.ads
Bind
[gprbind] main.bexch
[Ada] main.ali
Link
[link] main.adb
Runtime output
Using Drivers_1
42
Using Drivers_2
7
In this example, we declare the Transceiver interface in the Drivers_Base package. This inter-
face is then used to derive the tagged types Transceiver from both Drivers_1 and Drivers_2
packages.
In the Main procedure, we use the access to Transceiver'Class — from the interface declared
in the Drivers_Base package — to declare D. This object D contains the access to the actual driver
loaded at any specific time. We select the driver at runtime in the inner Select_Driver procedure,
which initializes D (with the access to the selected driver). Then, any operation on D triggers a
dispatching call to the selected driver.
14 int main()
15 {
16 int selection = 1;
17 void (*current_show_msg) (char *);
18
19 switch (selection)
20 {
21 case 1: current_show_msg = &show_msg_v1; break;
22 case 2: current_show_msg = &show_msg_v2; break;
23 default: current_show_msg = NULL; break;
24 }
25
35 return 0;
36 }
Runtime output
The example above contains two versions of the show_msg() function: show_msg_v1() and
show_msg_v2(). The function is selected depending on the value of selection, which initializes
the function pointer current_show_msg. If there's no corresponding value, current_show_msg
is set to null — alternatively, we could have selected a default version of show_msg() func-
tion. By calling current_show_msg ("Hello there!"), we're calling the function that cur-
rent_show_msg is pointing to.
This is the corresponding implementation in Ada:
[Ada]
3 procedure Show_Subprogram_Selection is
4
18 Current_Show_Msg : Show_Msg_Proc;
19 Selection : Natural;
20
21 begin
22 Selection := 1;
23
24 case Selection is
25 when 1 => Current_Show_Msg := Show_Msg_V1'Access;
26 when 2 => Current_Show_Msg := Show_Msg_V2'Access;
27 when others => Current_Show_Msg := null;
28 end case;
29
36 end Show_Subprogram_Selection;
Build output
Compile
[Ada] show_subprogram_selection.adb
Bind
[gprbind] show_subprogram_selection.bexch
[Ada] show_subprogram_selection.ali
Link
[link] show_subprogram_selection.adb
Runtime output
The structure of the code above is very similar to the one used in the C code. Again, we have two
version of Show_Msg: Show_Msg_V1 and Show_Msg_V2. We set Current_Show_Msg according to
the value of Selection. Here, we use 'Access to get access to the corresponding procedure. If
no version of Show_Msg is available, we set Current_Show_Msg to null.
Pointers to subprograms are also typically used as callback functions. This approach is extensively
used in systems that process events, for example. Here, we could have a two-layered system:
• A layer of the system (an event manager) triggers events depending on information from
sensors.
– For each event, callback functions can be registered.
– The event manager calls registered callback functions when an event is triggered.
• Another layer of the system registers callback functions for specific events and decides what
to do when those events are triggered.
This approach promotes information hiding and component decoupling because:
• the layer of the system responsible for managing events doesn't need to know what the call-
back function actually does, while
• the layer of the system that implements callback functions remains agnostic to implementa-
tion details of the event manager — for example, how events are implemented in the event
manager.
Let's see an example in C where we have a process_values() function that calls a callback func-
tion (process_one) to process a list of values:
[C]
4 #include "process_values.h"
5
11 # define LEN_VALUES 5
12
13 int main()
14 {
15
16 int values[LEN_VALUES] = { 1, 2, 3, 4, 5 };
17 int i;
18
26 return 0;
27 }
Runtime output
Value [0] = 11
Value [1] = 12
Value [2] = 13
Value [3] = 14
Value [4] = 15
As mentioned previously, process_values() doesn't have any knowledge about what pro-
cess_one() does with the integer value it receives as a parameter. Also, we could re-
place proc_10() by another function without having to change the implementation of pro-
cess_values().
Note that process_values() calls an assert() for the function pointer to compare it against
null. Here, instead of checking the validity of the function pointer, we're expecting the caller of
process_values() to provide a valid pointer.
This is the corresponding implementation in Ada:
[Ada]
11 end Values_Processing;
11 end Values_Processing;
6 procedure Show_Callback is
7 Values : Integer_Array := (1, 2, 3, 4, 5);
8 begin
9 Process_Values (Values, Proc_10'Access);
10
Build output
Compile
[Ada] show_callback.adb
[Ada] proc_10.adb
[Ada] values_processing.adb
Bind
[gprbind] show_callback.bexch
[Ada] show_callback.ali
Link
[link] show_callback.adb
Runtime output
Value [ 1] = 11
Value [ 2] = 12
Value [ 3] = 13
Value [ 4] = 14
Value [ 5] = 15
Similar to the implementation in C, the Process_Values procedure receives the access to a call-
back routine, which is then called for each value of the Values array.
Note that the declaration of Process_One_Callback makes use of the not null access decla-
ration. By using this approach, we ensure that any parameter of this type has a valid value, so we
can always call the callback routine.
In the previous sections, we have shown how to use packages to create separate components of
a system. As we know, when designing a complex system, it is advisable to separate concerns into
distinct units, so we can use Ada packages to represent each unit of a system. In this section, we
go one step further and create separate dynamic libraries for each component, which we'll then
link to the main application.
Let's suppose we have a main system (Main_System) and a component A (Component_A) that we
want to use in the main system. For example:
[Ada]
10 end Component_A;
15 end Component_A;
8 procedure Main_System is
9 Values : constant Float_Array := (10.0, 11.0, 12.0, 13.0);
10 Average_Value : Float;
11 begin
12 Average_Value := Average (Values);
13 Put_Line ("Average = " & Float'Image (Average_Value));
14 end Main_System;
Build output
Compile
[Ada] main_system.adb
[Ada] component_a.adb
Bind
[gprbind] main_system.bexch
[Ada] main_system.ali
Link
[link] main_system.adb
Runtime output
Average = 1.15000E+01
Note that, in the source-code example above, we're indicating the name of each file. We'll now see
how to organize those files in a structure that is suitable for the GNAT build system (GPRbuild).
In order to discuss how to create dynamic libraries, we need to dig into some details about the
build system. With GNAT, we can use project files for GPRbuild to easily design dynamic libraries.
Let's say we use the following directory structure for the code above:
|- component_a
| | component_a.gpr
| |- src
| | | component_a.adb
(continues on next page)
Here, we have two directories: component_a and main_system. Each directory contains a project file
(with the .gpr file extension) and a source-code directory (src).
In the source-code example above, we've seen the content of files component_a.ads,
component_a.adb and main_system.adb. Now, let's discuss how to write the project file for
Component_A (component_a.gpr), which will build the dynamic library for this component:
end Component_A;
The project is defined as a library project instead of project. This tells GPRbuild to build a library
instead of an executable binary. We then specify the library name using the Library_Name attribute,
which is required, so it must appear in a library project. The next two library-related attributes are
optional, but important for our use-case. We use:
• Library_Kind to specify that we want to create a dynamic library — by default, this attribute is
set to static;
• Library_Dir to specify the directory where the library is stored.
In the project file of our main system (main_system.gpr), we just need to reference the project
of Component_A using a with clause and indicating the correct path to that project file:
with "../component_a/component_a.gpr";
project Main_System is
for Source_Dirs use ("src");
for Object_Dir use "obj";
for Create_Missing_Dirs use "True";
for Main use ("main_system.adb");
end Main_System;
GPRbuild takes care of selecting the correct settings to link the dynamic library created for Com-
ponent_A with the main application (Main_System) and build an executable.
We can use the same strategy to create a Component_B and dynamically link to it in the
Main_System. We just need to create the separate structure for this component — with the ap-
propriate Ada packages and project file — and include it in the project file of the main system using
a with clause:
with "../component_a/component_a.gpr";
with "../component_b/component_b.gpr";
...
Again, GPRbuild takes care of selecting the correct settings to link both dynamic libraries together
with the main application.
You can find more details and special setting for library projects in the GPRbuild documentation22 .
22 https://docs.adacore.com/gprbuild-docs/html/gprbuild_ug/gnat_project_manager.html#library-projects
EIGHT
PERFORMANCE CONSIDERATIONS
All in all, there should not be significant performance differences between code written in Ada and
code written in C, provided that they are semantically equivalent. Taking the current GNAT imple-
mentation and its GCC C counterpart for example, most of the code generation and optimization
phases are shared between C and Ada — so there's not one compiler more efficient than the other.
Furthermore, the two languages are fairly similar in the way they implement imperative semantics,
in particular with regards to memory management or control flow. They should be equivalent on
average.
When comparing the performance of C and Ada code, differences might be observed. This usually
comes from the fact that, while the two piece appear semantically equivalent, they happen to be
actually quite different; C code semantics do not implicitly apply the same run-time checks that Ada
does. This section will present common ways for improving Ada code performance.
Clever use of compilation switches might optimize the performance of an application significantly.
In this section, we'll briefly look into some of the switches available in the GNAT toolchain.
Optimization levels can be found in many compilers for multiple languages. On the lowest level, the
GNAT compiler doesn't optimize the code at all, while at the higher levels, the compiler analyses
the code and optimizes it by removing unnecessary operations and making the most use of the
target processor's capabilities.
By being part of GCC, GNAT offers the same -O_ switches as GCC:
Switch Description
-O0 No optimization: the generated code is completely unoptimized. This is the default opti-
mization level.
-O1 Moderate optimization.
-O2 Full optimization.
-O3 Same optimization level as for -O2. In addition, further optimization strategies, such as
aggressive automatic inlining and vectorization.
Note that the higher the level, the longer the compilation time. For fast compilation during devel-
opment phase, unless you're working on benchmarking algorithms, using -O0 is probably a good
idea.
203
Ada for the Embedded C Developer, Release 2021-06
In addition to the levels presented above, GNAT also has the -Os switch, which allows for optimizing
code and data usage.
8.2.2 Inlining
As we've seen in the previous section, automatic inlining depends on the optimization level. The
highest optimization level (-O3), for example, performs aggressive automatic inlining. This could
mean that this level inlines too much rather than not enough. As a result, the cache may become
an issue and the overall performance may be worse than the one we would achieve by compiling
the same code with optimization level 2 (-O2). Therefore, the general recommendation is to not
just select -O3 for the optimized version of an application, but instead compare it the optimized
version built with -O2.
In some cases, it's better to reduce the optimization level and perform manual inlining instead of
automatic inlining. We do that by using the Inline aspect. Let's reuse an example from a previous
chapter and inline the Average function:
[Ada]
Listing 1: float_arrays.ads
1 package Float_Arrays is
2
8 end Float_Arrays;
Listing 2: float_arrays.adb
1 package body Float_Arrays is
2
12 end Float_Arrays;
Listing 3: compute_average.adb
1 with Ada.Text_IO; use Ada.Text_IO;
2
5 procedure Compute_Average is
6 Values : constant Float_Array := (10.0, 11.0, 12.0, 13.0);
7 Average_Value : Float;
8 begin
9 Average_Value := Average (Values);
10 Put_Line ("Average = " & Float'Image (Average_Value));
11 end Compute_Average;
Build output
Compile
[Ada] compute_average.adb
[Ada] float_arrays.adb
Bind
[gprbind] compute_average.bexch
[Ada] compute_average.ali
Link
[link] compute_average.adb
Runtime output
Average = 1.15000E+01
When compiling this example, GNAT will inline Average in the Compute_Average procedure.
In order to effectively use this aspect, however, we need to set the optimization level to at least -O1
and use the -gnatn switch, which instructs the compiler to take the Inline aspect into account.
Note, however, that the Inline aspect is just a recommendation to the compiler. Sometimes, the
compiler might not be able to follow this recommendation, so it won't inline the subprogram. In
this case, we get a compilation warning from GNAT.
These are some examples of situations where the compiler might not be able to inline a subpro-
gram:
• when the code is too large,
• when it's too complicated — for example, when it involves exception handling —, or
• when it contains tasks, etc.
In addition to the Inline aspect, we also have the Inline_Always aspect. In contrast to the for-
mer aspect, however, the Inline_Always aspect isn't primarily related to performance. Instead,
it should be used when the functionality would be incorrect if inlining was not performed by the
compiler. Examples of this are procedures that insert Assembly instructions that only make sense
when the procedure is inlined, such as memory barriers.
Similar to the Inline aspect, there might be situations where a subprogram has the In-
line_Always aspect, but the compiler is unable to inline it. In this case, we get a compilation
error from GNAT.
8.3.1 Checks
Ada provides many runtime checks to ensure that the implementation is working as expected. For
example, when accessing an array, we would like to make sure that we're not accessing a memory
position that is not allocated for that array. This is achieved by an index check.
Another example of runtime check is the verification of valid ranges. For example, when adding
two integer numbers, we would like to ensure that the result is still in the valid range — that the
value is neither too large nor too small. This is achieved by an range check. Likewise, arithmetic
operations shouldn't overflow or underflow. This is achieved by an overflow check.
Although runtime checks are very useful and should be used as much as possible, they can also
increase the overhead of implementations at certain hot-spots. For example, checking the index
of an array in a sorting algorithm may significantly decrease its performance. In those cases, sup-
pressing the check may be an option. We can achieve this suppression by using pragma Suppress
(Index_Check). For example:
[Ada]
In case of overflow checks, we can use pragma Suppress (Overflow_Check) to suppress them:
function Some_Computation (A, B : Int32) return Int32 is
pragma Suppress (Overflow_Check);
begin
-- (implementation removed...)
null;
end Sort;
We can also deactivate overflow checks for integer types using the -gnato switch when compiling
a source-code file with GNAT. In this case, overflow checks in the whole file are deactivated.
It is also possible to suppress all checks at once using pragma Suppress (All_Checks). In
addition, GNAT offers a compilation switch called -gnatp, which has the same effect on the whole
file.
Note, however, that this kind of suppression is just a recommendation to the compiler. There's no
guarantee that the compiler will actually suppress any of the checks because the compiler may not
be able to do so — typically because the hardware happens to do it. For example, if the machine
traps on any access via address zero, requesting the removal of null access value checks in the
generated code won't prevent the checks from happening.
It is important to differentiate between required and redundant checks. Let's consider the following
example in C:
[C]
Listing 4: main.c
1 #include <stdio.h>
2
7 res = a / b;
8
12 return 0;
13 }
Because C doesn't have language-defined checks, as soon as the application tries to divide a value
by zero in res = a / b, it'll break — on Linux, for example, you may get the following error
message by the operating system: Floating point exception (core dumped). Therefore,
we need to manually introduce a check for zero before this operation. For example:
[C]
Listing 5: main.c
1 #include <stdio.h>
2
7 if (b != 0) {
8 res = a / b;
9
19 return 0;
20 }
Runtime output
Listing 6: show_division_by_zero.adb
1 with Ada.Text_IO; use Ada.Text_IO;
2
3 procedure Show_Division_By_Zero is
4 A : Integer := 8;
5 B : Integer := 0;
6 Res : Integer;
7 begin
8 Res := A / B;
9
Build output
Compile
[Ada] show_division_by_zero.adb
show_division_by_zero.adb:8:13: warning: division by zero [enabled by default]
show_division_by_zero.adb:8:13: warning: "Constraint_Error" will be raised at run␣
↪time [enabled by default]
Bind
[gprbind] show_division_by_zero.bexch
[Ada] show_division_by_zero.ali
Link
[link] show_division_by_zero.adb
Runtime output
Similar to the first version of the C code, we're not explicitly checking for a potential division by zero
here. In Ada, however, this check is automatically inserted by the language itself. When running the
application above, an exception is raised when the application tries to divide the value in A by zero.
We could introduce exception handling in our example, so that we get the same message as we
did in the second version of the C code:
[Ada]
Listing 7: show_division_by_zero.adb
1 with Ada.Text_IO; use Ada.Text_IO;
2
3 procedure Show_Division_By_Zero is
4 A : Integer := 8;
5 B : Integer := 0;
6 Res : Integer;
7 begin
8 Res := A / B;
9
Build output
Compile
[Ada] show_division_by_zero.adb
show_division_by_zero.adb:8:13: warning: division by zero [enabled by default]
show_division_by_zero.adb:8:13: warning: "Constraint_Error" will be raised at run␣
↪time [enabled by default]
Bind
[gprbind] show_division_by_zero.bexch
[Ada] show_division_by_zero.ali
Link
[link] show_division_by_zero.adb
Runtime output
This example demonstrates that the division check for Res := A / B is required and shouldn't be
suppressed. In contrast, a check is redundant — and therefore not required — when we know that
the condition that leads to a failure can never happen. In many cases, the compiler itself detects
redundant checks and eliminates them (for higher optimization levels). Therefore, when improving
the performance of your application, you should:
1. keep all checks active for most parts of the application;
2. identify the hot-spots of your application;
3. identify which checks haven't been eliminated by the optimizer on these hot-spots;
4. identify which of those checks are redundant;
5. only suppress those checks that are redundant, and keep the required ones.
8.3.2 Assertions
We've already discussed assertions in this section of the SPARK chapter (page 113). Assertions are
user-defined checks that you can add to your code using the pragma Assert. For example:
[Ada]
return Res;
end Sort;
Assertions that are specified with pragma Assert are not enabled by default. You can enable
them by setting the assertion policy to check — using pragma Assertion_Policy (Check) —
or by using the -gnata switch when compiling with GNAT.
Similar to the checks discussed previously, assertions can generate significant overhead when used
at hot-spots. Restricting those assertions to development (e.g. debug version) and turning them
off on the release version may be an option. In this case, formal proof — as discussed in the SPARK
chapter (page 107) — can help you. By formally proving that assertions will never fail at run-time,
you can safely deactivate them.
Ada generally speaking provides more ways than C or C++ to write simple dynamic structures, that
is to say structures that have constraints computed after variables. For example, it's quite typical
to have initial values in record types:
[Ada]
type R is record
F : Some_Field := Call_To_Some_Function;
end record;
However, the consequences of the above is that any declaration of a instance of this type without
an explicit value for V will issue a call to Call_To_Some_Function. More subtle issue may arise
with elaboration. For example, it's possible to write:
Listing 8: some_functions.ads
1 package Some_Functions is
2
7 end Some_Functions;
Listing 9: values.ads
1 with Some_Functions; use Some_Functions;
2
3 package Values is
4 A_Start : Integer := Some_Function_Call;
(continues on next page)
3 package Arr_Def is
4 type Arr is array (Integer range A_Start .. A_End) of Integer;
5 end Arr_Def;
It may indeed be appealing to be able to change the values of A_Start and A_End at startup so
as to align a series of arrays dynamically. The consequence, however, is that these values will
not be known statically, so any code that needs to access to boundaries of the array will need to
read data from memory. While it's perfectly fine most of the time, there may be situations where
performances are so critical that static values for array boundaries must be enforced.
Here's a last case which may also be surprising:
[Ada]
In the code above, R contains two arrays, F1 and F2, respectively constrained by the discriminant D1
and D2. The consequence is, however, that to access F2, the run-time needs to know how large F1
is, which is dynamically constrained when creating an instance. Therefore, accessing to F2 requires
a computation involving D1 which is slower than, let's say, two pointers in an C array that would
point to two different arrays.
Generally speaking, when values are used in data structures, it's useful to always consider where
they're coming from, and if their value is static (computed by the compiler) or dynamic (only known
at run-time). There's nothing fundamentally wrong with dynamically constrained types, unless they
appear in performance-critical pieces of the application.
In the section about pointers (page 132), we mentioned that the Ada compiler will automatically
pass parameters by reference when needed. Let's look into what "when needed" means. The
fundamental point to understand is that the parameter types determine how the parameters are
passed in and/or out. The parameter modes do not control how parameters are passed.
Specifically, the language standards specifies that scalar types are always passed by value, and that
some other types are always passed by reference. It would not make sense to make a copy of a
task when passing it as a parameter, for example. So parameters that can be passed reasonably
by value will be, and those that must be passed by reference will be. That's the safest approach.
But the language also specifies that when the parameter is an array type or a record type, and
the record/array components are all by-value types, then the compiler decides: it can pass the
parameter using either mechanism. The critical case is when such a parameter is large, e.g., a
large matrix. We don't want the compiler to pass it by value because that would entail a large copy,
and indeed the compiler will not do so. But if the array or record parameter is small, say the same
size as an address, then it doesn't matter how it is passed and by copy is just as fast as by reference.
That's why the language gives the choice to the compiler. Although the language does not mandate
that large parameters be passed by reference, any reasonable compiler will do the right thing.
The modes do have an effect, but not in determining how the parameters are passed. Their effect,
for parameters passed by value, is to determine how many times the value is copied. For mode
in and mode out there is just one copy. For mode in out there will be two copies, one in each
direction.
Therefore, unlike C, you don't have to use access types in Ada to get better performance when
passing arrays or records to subprograms. The compiler will almost certainly do the right thing for
you.
Let's look at this example:
[C]
3 struct Data {
4 int prev, curr;
5 };
6
27 return 0;
28 }
Runtime output
Prev : 1
Curr : 3
In this C code example, we're using pointers to pass D1 as a reference to update and display.
In contrast, the equivalent code in Ada simply uses the parameter modes to specify the data flow
directions. The mechanisms used to pass the values do not appear in the source code.
[Ada]
3 procedure Update_Record is
4
25 begin
26 Update (D1, 3);
27 Display (D1);
28 end Update_Record;
Build output
Compile
[Ada] update_record.adb
Bind
[gprbind] update_record.bexch
[Ada] update_record.ali
Link
[link] update_record.adb
Runtime output
Prev: 1
Curr: 3
In the calls to Update and Display, D1 is always be passed by reference. Because no extra copy
takes place, we get a performance that is equivalent to the C version. If we had used arrays in the
example above, D1 would have been passed by reference as well:
[Ada]
3 procedure Update_Array is
4
23 begin
24 Update (D1, 3);
25 Display (D1);
26 end Update_Array;
Build output
Compile
[Ada] update_array.adb
Bind
[gprbind] update_array.bexch
[Ada] update_array.ali
Link
[link] update_array.adb
Runtime output
Prev: 1
Curr: 3
Again, no extra copy is performed in the calls to Update and Display, which gives us optimal
performance when dealing with arrays and avoids the need to use access types to optimize the
code.
Previously, we've discussed the cost of passing complex records as arguments to subprograms.
We've seen that we don't have to use explicit access type parameters to get better performance in
Ada. In this section, we'll briefly discuss the cost of function returns.
In general, we can use either procedures or functions to initialize a data structure. Let's look at this
example in C:
[C]
3 struct Data {
4 int prev, curr;
5 };
6
17 return d;
18 }
19
24 D1 = get_init_data();
25
26 init_data(&D1);
27
28 return 0;
29 }
This code example contains two subprograms that initialize the Data structure:
• init_data(), which receives the data structure as a reference (using a pointer) and initializes
it, and
• get_init_data(), which returns the initialized structure.
In C, we generally avoid implementing functions such as get_init_data() because of the extra
copy that is needed for the function return.
This is the corresponding implementation in Ada:
[Ada]
19 D1 : Data;
20
25 Init (D1);
26 end Init_Record;
Build output
Compile
[Ada] init_record.adb
Bind
[gprbind] init_record.bexch
[Ada] init_record.ali
Link
[link] init_record.adb
In this example, we have two versions of Init: one using a procedural form, and the other one
using a functional form. Note that, because of Ada's support for subprogram overloading, we can
use the same name for both subprograms.
The issue is that assignment of a function result entails a copy, just as if we assigned one variable
to another. For example, when assigning a function result to a constant, the function result is
copied into the memory for the constant. That's what is happening in the above examples for the
initialized variables.
Therefore, in terms of performance, the same recommendations apply: for large types we should
avoid writing functions like the Init function above. Instead, we should use the procedural form of
Init. The reason is that the compiler necessarily generates a copy for the Init function, while the
Init procedure uses a reference for the output parameter, so that the actual record initialization
is performed in place in the caller's argument.
An exception to this is when we use functions returning values of limited types, which by definition
do not allow assignment. Here, to avoid allowing something that would otherwise look suspiciously
like an assignment, the compiler generates the function body so that it builds the result directly into
the object being assigned. No copy takes place.
We could, for example, rewrite the example above using limited types:
[Ada]
16 D1 : Data := Init;
17
Build output
Compile
[Ada] init_limited_record.adb
Bind
(continues on next page)
In this example, D1 : Data := Init; has the same cost as the call to the procedural form —
Init (D1); — that we've seen in the previous example. This is because the assignment is done
in place.
Note that limited types require the use of the extended return statements (return ... do ...
end return) in function implementations. Also note that, because the Data type is limited, we
can only use the Init function in the declaration of D1; a statement in the code such as D1 :=
Init; is therefore forbidden.
NINE
The technical benefits of a migration from C to Ada are usually relatively straightforward to demon-
strate. Hopefully, this course provides a good basis for it. However, when faced with an actual busi-
ness decision to make, additional considerations need to be taken into account, such as return on
investment, perennity of the solution, tool support, etc. This section will cover a number of usual
questions and provide elements of answers.
Switching from one technology to another is a cost, may that be in terms of training, transition of
the existing environment or acquisition of new tools. This investment needs to be matched with an
expected return on investment, or ROI, to be consistent. Of course, it's incredibly difficult to provide
a firm answer to how much money can be saved by transitioning, as this is highly dependent on
specific project objectives and constraints. We're going to provide qualitative and quantitative ar-
guments here, from the perspective of a project that has to reach a relatively high level of integrity,
that is to say a system where the occurrence of a software failure is a relatively costly event.
From a qualitative standpoint, there are various times in the software development life cycle where
defects can be found:
1. on the developer's desk
2. during component testing
3. during integration testing
4. after deployment
5. during maintenance
Numbers from studies vary greatly on the relative costs of defects found at each of these phases,
but there's a clear ordering between them. For example, a defect found while developing is orders
of magnitude less expensive to fix than a defect found e.g. at integration time, which may involve
costly debugging sessions and slow down the entire system acceptance. The whole purpose of Ada
and SPARK is to push defect detection to the developer's desk as much as possible; at least for all
of these defects that can be identified at that level. While the strict act of writing software may be
taking more effort because of all of the additional safeguards, this should have a significant and
positive impact down the line and help to control costs overall. The exact value this may translate
into is highly business dependent.
From a quantitative standpoint, two studies have been done almost 25 years apart and provide
similar insights:
• Rational Software in 1995 found that the cost of developing software in Ada was overall half
as much as the cost of developing software in C.
• VDC ran a study in 2018, finding that the cost savings of developing with Ada over C ranged
from 6% to 38% in savings.
217
Ada for the Embedded C Developer, Release 2021-06
From a qualitative standpoint, in particular with regards to Ada and C from a formal proof per-
spective, an interesting presentation was made in 2017 by two researchers. They tried to apply
formal proof on the same piece of code, developed in Ada/SPARK on one end and C/Frama-C on
the other. Their results indicate that the Ada/SPARK technology is indeed more conducive to formal
proof methodologies.
Although all of these studies have their own biases, they provide a good idea of what to expect in
terms of savings once the initial investment in switching to Ada is made. This is assuming everything
else is equal, in particular that the level of integrity is the same. In many situations, the migration
to Ada is justified by an increase in terms of integrity expectations, in which case it's expected that
development costs will rise (it's more expensive to develop better software) and Ada is viewed as
a means to mitigate this rise in development costs.
That being said, the point of this argument is not to say that it's not possible to write very safe and
secure software with languages different than Ada. With the right expertise, the right processes
and the right tools, it's done every day. The point is that Ada overall reduces the level of processes,
expertise and tools necessary and will allow to reach the same target at a lower cost.
Ada was initially born as a DoD project, and thus got its initial customer base in aerospace and
defence (A&D). At the time these lines are written and from the perspective of AdaCore, A&D is still
the largest consumer of Ada today and covers about 70% of the market. This creates a consistent
and long lasting set of established users as these project last often for decades, using the same
codebase migrating from platform to platform.
More recently however, there has been an emerging interest for Ada in new communities of users
such as automotive, medical device, industrial automation and overall cyber-security. This can
probably be explained by a rise of safety, reliability and cyber-security requirements. The market
is moving relatively rapidly today and we're anticipating an increase of the Ada footprint in these
domains, while still remaining a technology of choice for the development of mission critical soft-
ware.
The first piece of the answer lies in the user base of the Ada language, as seen in the previous
question. Projects using Ada in the aerospace and defence domain maintain source code over
decades, providing healthy funding foundation for Ada-based technologies.
AdaCore being the author of this course, it's difficult for us to be fair in our description of other
Ada compilation technologies. We will leave to the readers the responsibility of forging their own
opinion. If they present a credible alternative to the GNAT compiler, then this whole section can
be considered as void.
Assuming GNAT is the only option available, and acknowledging that this is an argument that we're
hearing from a number of Ada adopters, let's discuss the "sole source" issue.
First of all, it's worth noting that industries are using a lot of software that is provided by only one
source, so while non-ideal, these situations are also quite common.
In the case of the GNAT compiler however, while AdaCore is the main maintainer, this maintenance
is done as part of an open-source community. This means that nothing prevents a third party to
start selling a competing set of products based on the same compiler, provided that it too adopts
the open-source approach. Our job is to be more cost-effective than the alternative, and indeed
for the vast part this has prevented a competing offering to emerge. However, should AdaCore
disappear or switch focus, Ada users would not be prevented from carrying on using its software
(there is no lock) and a third party could take over maintenance. This is not a theoretical case, this
has been done in the past either by companies looking at supporting their own version of GNAT,
vendors occupying a specific niche that was left uncovered , or hobbyists developing their own
builds.
With that in mind, it's clear that the "sole source" provider issue is a circumstantial — nothing is
preventing other vendors from emerging if the conditions are met.
A language by itself is of little use for the development of safety-critical software. Instead, a com-
plete toolset is needed to accompany the development process, in particular tools for edition, test-
ing, static analysis, etc.
AdaCore provides a number of these tools either in through its core or add-on package. These
include (as of 2019):
• An IDE (GNAT Studio)
• An Eclipse plug-in (GNATbench)
• A debugger (GDB)
• A testing tool (GNATtest)
• A structural code coverage tool (GNATcoverage)
• A metric computation tool (GNATmetric)
• A coding standard checker (GNATcheck)
• Static analysis tools (CodePeer, SPARK Pro)
• A Simulink code generator (QGen)
• An Ada parser to develop custom tools (libadalang)
Ada is, however, an internationally standardized language, and many companies are providing
third party solutions to complete the toolset. Overall, the language can be and is used with tools
on par with their equivalent C counterparts.
A common question from teams on the verge of selecting Ada and SPARK is how to manage the
developer team growth and turnover. While Ada and SPARK are taught by a growing number of
universities worldwide, it may still be challenging to hire new staff with prior Ada experience.
Fortunately, Ada's base semantics are very close to those of C/C++, so that a good embedded soft-
ware developer should be able to learn it relatively easily. This course is definitely a resource avail-
able to get started. Online training material is also available, together with on-site in person train-
ing.
In general, getting an engineer operational in Ada and SPARK shouldn't take more than a few weeks
worth of time.
The most common scenario when introducing Ada and SPARK to a project or a team is to do it
within a pre-existing C codebase, which can already spread over hundreds of thousands if not
millions lines of code. Re-writing this software to Ada or SPARK is of course not practical and coun-
terproductive.
Most teams select either a small piece of existing code which deserves particular attention, or new
modules to develop, and concentrate on this. Developing this module or part of the application
will also help in developing the coding patterns to be used for the particular project and company.
This typically concentrates an effort of a few people on a few thousands lines of code. The resulting
code can be linked to the rest of the C application. From there, the newly established practices and
their benefit can slowly spread through the rest of the environment.
Establishing this initial core in Ada and SPARK is critical, and while learning the language isn't a
particularly difficult task, applying it to its full capacity may require some expertise. One possibility
to accelerate this initial process is to use AdaCore mentorship services.
TEN
CONCLUSION
Although Ada's syntax might seem peculiar to C developers at first glance, it was designed to in-
crease readability and maintainability, rather than making it faster to write in a condensed manner
— as it is often the case in C.
Especially in the embedded domain, C developers are used to working at a very low level, which
includes mathematical operations on pointers, complex bit shifts, and logical bitwise operations.
C is well designed for such operations because it was designed to replace Assembly language for
faster, more efficient programming.
Ada can be used to describe high level semantics and architectures. The beauty of the language,
however, is that it can be used all the way down to the lowest levels of the development, including
embedded Assembly code or bit-level data management. However, although Ada supports bitwise
operations such as masks and shifts, they should be relatively rarely needed. When translating C
code to Ada, it's good practice to consider alternatives. In a lot of cases, these operations are used
to insert several pieces of data into a larger structure. In Ada, this can be done by describing the
structure layout at the type level through representation clauses, and then accessing this structure
as any other. For example, we can interpret an arbitrary data type as a bit-field and perform low-
level operations on it.
Because Ada is a strongly typed language, it doesn't define any implicit type conversions like C. If
we try to compile Ada code that contains type mismatches, we'll get a compilation error. Because
the compiler prevents mixing variables of different types without explicit type conversion, we can't
accidentally end up in a situation where we assume something will happen implicitly when, in fact,
our assumption is incorrect. In this sense, Ada's type system encourages programmers to think
about data at a high level of abstraction. Ada supports overlays and unchecked conversions as a
way of converting between unrelated data type, which are typically used for interfacing with low-
level elements such as registers.
In Ada, arrays aren't interchangeable with operations on pointers like in C. Also, array types are
considered first-class citizens and have dedicated semantics such as the availability of the array's
boundaries at run-time. Therefore, unhandled array overflows are impossible unless checks are
suppressed. Any discrete type can serve as an array index, and we can specify both the starting
and ending bounds. In addition, Ada offers high-level operations for copying, slicing, and assigning
values to arrays.
Although Ada supports pointers, most situations that would require a pointer in C do not in Ada.
In the vast majority of the cases, indirect memory management can be hidden from the developer
and thus prevent many potential errors. In C, pointers are typically used to pass references to
subprograms, for example. In contrast, Ada parameter modes indicate the flow of information to
the reader, leaving the means of passing that information to the compiler.
When translating pointers from C code to Ada, we need to assess whether they are needed in the
first place. Ada pointers (access types) should only be used with complex structures that cannot
be allocated at run-time. There are many situations that would require a pointer in C, but do not
in Ada. For example, arrays — even when dynamically allocated —, results of functions, passing of
large structures as parameters, access to registers, etc.
Because of the absence of namespaces, global names in C tend to be very long. Also, because of
the absence of overloading, they can even encode type names in their name. In Ada, a package is
221
Ada for the Embedded C Developer, Release 2021-06
a namespace. Also, we can use the private part of a package to declare private types and private
subprograms. In fact, private types are useful for preventing the users of those types from de-
pending on the implementation details. Another use-case is the prevention of package users from
accessing the package state/data arbitrarily.
Ada has a dedicated set of features for interfacing with other languages, so we can easily inter-
face with our existing C code before translating it to Ada. Also, GNAT includes automatic binding
generators. Therefore, instead of re-writing the entire C code upfront, which isn't practical or cost-
effective, we can selectively translate modules from C to Ada.
When it comes to implementing concurrency and real time, Ada offers several options. Ada pro-
vides high level constructs such as tasks and protected objects to express concurrency and syn-
chronization, which can be used when running on top of an operating system such as Linux. On
more constrained systems, such as bare metal or some real-time operating systems, a subset of
the Ada tasking capabilities — known as the Ravenscar and Jorvik profiles — is available. Though
restricted, this subset also has nice properties, in particular the absence of deadlock,the absence
of priority inversion, schedulability and very small footprint. On bare metal systems, this also es-
sentially means that Ada comes with its own real-time kernel. The advantage of using the full Ada
tasking model or the restricted profiles is to enhance portability.
Ada includes many features typically used for embedded programming:
• Built-in support for handling interrupts, so we can process interrupts by attaching a handler
— as a protected procedure — to it.
• Built-in support for handling both volatile and atomic data.
• Support for register overlays, which we can use to create a structure that facilitates manipu-
lating bits from registers.
• Support for creating data streams for serialization of arbitrary information and transmission
over a communication channel, such as a serial port.
• Built-in support for fixed-point arithmetic, which is an option when our target device doesn't
have a floating-point unit or the result of calculations needs to be bit-exact.
Also, Ada compilers such as GNAT have built-in support for directly mixing Ada and Assembly code.
Ada also supports contracts, which can be associated with types and variables to refine values and
define valid and invalid values. The most common kind of contract is a range constraint — using
the range reserved word. Ada also supports contract-based programming in the form of precon-
ditions and postconditions. One typical benefit of contract-based programming is the removal of
defensive code in subprogram implementations.
It is common to see embedded software being used in a variety of configurations that require
small changes to the code for each instance. In C, variability is usually achieved through macros
and function pointers, the former being tied to static variability and the latter to dynamic variability.
Ada offers many alternatives for both techniques, which aim at structuring possible variations of
the software. Examples of static variability in Ada are: genericity, simple derivation, configuration
pragma files, and configuration packages. Examples of dynamic variability in Ada are: records
with discriminants, variant records — which may include the use of unions —, object orientation,
pointers to subprograms, and design by components using dynamic libraries.
There shouldn't be significant performance differences between code written in Ada and code writ-
ten in C — provided that they are semantically equivalent. One reason is that the two languages are
fairly similar in the way they implement imperative semantics, in particular with regards to memory
management or control flow. Therefore, they should be equivalent on average. However, when a
piece of code in Ada is significantly slower than its counterpart in C, this usually comes from the fact
that, while the two pieces of code appear to be semantically equivalent, they happen to be actually
quite different. Fortunately, there are strategies that we can use to improve the performance and
make it equivalent to the C version. These are some examples:
• Clever use of compilation switches, which might optimize the performance of an application
significantly.
223
Ada for the Embedded C Developer, Release 2021-06
ELEVEN
The goal of this appendix is to present a hands-on view on how to translate a system from C to Ada
and improve it with object-oriented programming.
Let's start with an overview of a simple system that we'll implement and use below. The main
system is called AB and it combines two systems A and B. System AB is not supposed to do anything
useful. However, it can serve as a good model for the hands-on we're about to start.
This is a list of requirements for the individual systems A and B, and the combined system AB:
• System A:
– The system can be activated and deactivated.
225
Ada for the Embedded C Developer, Release 2021-06
* This check consists in calculating the absolute difference D between the current val-
ues of systems A and B and checking whether D is below a threshold of 0.1.
The source-code in the following section contains an implementation of these requirements.
In this section, we look into implementations (in both C and Ada) of system AB that don't make use
of object-oriented programming.
Listing 1: system_a.h
1 typedef struct {
2 float val[2];
3 int active;
4 } A;
5
Listing 2: system_a.c
1 #include "system_a.h"
2
Listing 3: system_b.h
1 typedef struct {
2 float val;
3 int active;
4 } B;
5
Listing 4: system_b.c
1 #include "system_b.h"
2
Listing 5: system_ab.h
1 #include "system_a.h"
2 #include "system_b.h"
3
4 typedef struct {
5 A a;
6 B b;
7 } AB;
8
Listing 6: system_ab.c
1 #include <math.h>
2 #include "system_ab.h"
3
Listing 7: main.c
1 #include <stdio.h>
2 #include "system_ab.h"
3
20 int main()
21 {
(continues on next page)
27 display_active (&s);
28 display_check (&s);
29
33 display_active (&s);
34 }
Runtime output
Here, each system is implemented in a separate set of header and source-code files. For example,
the API of system AB is in system_ab.h and its implementation in system_ab.c.
In the main application, we instantiate system AB and activate it. Then, we proceed to display the
activation state and the result of the system's health check. Finally, we deactivate the system and
display the activation state again.
Listing 8: system_a.ads
1 package System_A is
2
5 type A is record
6 Val : Val_Array (1 .. 2);
7 Active : Boolean;
8 end record;
9
18 end System_A;
Listing 9: system_a.adb
1 package body System_A is
2
24 end System_A;
3 type B is record
4 Val : Float;
5 Active : Boolean;
6 end record;
7
16 end System_B;
24 end System_B;
4 package System_AB is
5
6 type AB is record
7 SA : A;
8 SB : B;
9 end record;
10
21 end System_AB;
31 end System_AB;
5 procedure Main is
6
25 S : AB;
26 begin
27 Put_Line ("Activating system AB...");
28 AB_Activate (S);
29
30 Display_Active (S);
31 Display_Check (S);
32
36 Display_Active (S);
37 end Main;
Build output
Compile
[Ada] main.adb
[Ada] system_ab.adb
[Ada] system_a.adb
[Ada] system_b.adb
Bind
[gprbind] main.bexch
[Ada] main.ali
Link
[link] main.adb
Runtime output
As you can see, this is a direct translation that doesn't change much of the structure of the original
C code. Here, the goal was to simply translate the system from one language to another and make
sure that the behavior remains the same.
3 type A is private;
4
13 private
14
17 type A is record
18 Val : Val_Array (1 .. 2);
19 Active : Boolean;
20 end record;
21
22 end Simple.System_A;
22 end Simple.System_A;
3 type B is private;
4
13 private
14
15 type B is record
16 Val : Float;
17 Active : Boolean;
18 end record;
19
20 end Simple.System_B;
22 end Simple.System_B;
4 package Simple.System_AB is
5
6 type AB is private;
7
18 private
19
20 type AB is record
21 SA : A;
22 SB : B;
23 end record;
24
25 end Simple.System_AB;
27 end Simple.System_AB;
5 procedure Main is
6
25 S : AB;
26 begin
27 Put_Line ("Activating system AB...");
28 Activate (S);
29
30 Display_Active (S);
31 Display_Check (S);
32
36 Display_Active (S);
37 end Main;
Build output
Compile
[Ada] main.adb
[Ada] simple.ads
[Ada] simple-system_ab.adb
[Ada] simple-system_a.adb
[Ada] simple-system_b.adb
Bind
[gprbind] main.bexch
[Ada] main.ali
Link
[link] main.adb
Runtime output
Until now, we haven't used any of the object-oriented programming features of the Ada language.
So we can start by analyzing the API of systems A and B and deciding how to best abstract some of
its elements using object-oriented programming.
11.3.1 Interfaces
The first thing we may notice is that we actually have two distinct sets of APIs there:
• one API for activating and deactivating the system.
• one API for retrieving the value of the system.
We can use this distinction to declare two interface types:
• Activation_IF for the Activate and Deactivate procedures and the Is_Active func-
tion;
• Value_Retrieval_IF for the Value function.
This is how the declaration could look like:
Note that, because we are declaring interface types, all operations on those types must be abstract
or, in the case of procedures, they can also be declared null. For example, we could change the
declaration of the procedures above to this:
When an operation is declared abstract, we must override it for the type that derives from the in-
terface. When a procedure is declared null, it acts as a do-nothing default. In this case, overriding
the operation is optional for the type that derives from this interface.
Since the original system needs both interfaces we've just described, we have to declare another
type that combines those interfaces. We can do this by declaring the interface type Sys_Base,
which serves as the base type for systems A and B. This is the declaration:
Since the system activation functionality is common for both systems A and B, we could implement
it as part of Sys_Base. That would require changing the declaration from a simple interface to an
abstract record:
Now, we can add the Boolean component to the record (as a private component) and override the
subprograms of the Activation_IF interface. This is the adapted declaration:
private
In the declaration of the Sys_Base type we've just seen, we're not overriding the Value function —
from the Value_Retrieval_IF interface — for the Sys_Base type, so it remains an abstract func-
tion for Sys_Base. Therefore, the Sys_Base type itself remains abstract and needs be explicitly
declared as such.
We use this strategy to ensure that all types derived from Sys_Base need to implement their own
version of the Value function. For example:
Here, the A type is derived from the Sys_Base and it includes its own version of the Value function
by overriding it. Therefore, A is not an abstract type anymore and can be used to declare objects:
procedure Main is
Obj : A;
V : Float;
(continues on next page)
Important
Note that the use of the overriding keyword in the subprogram declaration is not strictly nec-
essary. In fact, we could leave this keyword out, and the code would still compile. However, if
provided, the compiler will check whether the information is correct.
Using the overriding keyword can help to avoid bad surprises — when you may think that you're
overriding a subprogram, but you're actually not. Similarly, you can also write not overriding
to be explicit about subprograms that are new primitives of a derived type. For example:
We also need to declare the values that are used internally in systems A and B. For system A, this
is the declaration:
private
In the previous implementation, we've seen that the A_Activate and B_Activate procedures
perform the following steps:
• initialize internal values;
• indicate that the system is active (by setting the Active flag to True).
In the implementation of the Activate procedure for the Sys_Base type, however, we're only
dealing with the second step. Therefore, we need to override the Activate procedure and make
sure that we initialize internal values as well. First, we need to declare this procedure for type A:
In the implementation of Activate, we should call the Activate procedure from the parent
(Sys_Base) to ensure that whatever was performed for the parent will be performed in the de-
rived type as well. For example:
Here, by writing Sys_Base (E), we're performing a view conversion. Basically, we're telling the
compiler to view E not as an object of type A, but of type Sys_Base. When we do this, any operation
performed on this object will be done as if it was an object of Sys_Base type, which includes calling
the Activate procedure of the Sys_Base type.
Important
If we write T (Obj).Proc, we're telling the compiler to call the Proc procedure of type T and apply
it on Obj.
If we write T'Class (Obj).Proc, however, we're telling the compiler to dispatch the call. For
example, if Obj is of derived type T2 and there's an overridden Proc procedure for type T2, then
this procedure will be called instead of the Proc procedure for type T.
11.3.5 Type AB
While the implementation of systems A and B is almost straightforward, it gets more interesting in
the case of system AB. Here, we have a similar API, but we don't need the activation mechanism
implemented in the abstract type Sys_Base. Therefore, deriving from Sys_Base is not the best
option. Instead, when declaring the AB type, we can simply use the same interfaces as we did for
Sys_Base, but keep it independent from Sys_Base. For example:
private
Naturally, we still need to override all the subprograms that are part of the Activation_IF and
Value_Retrieval_IF interfaces. Also, we need to implement the additional Check function that
was originally only available on system AB. Therefore, we declare these subprograms:
20 private
21
27 end Simple;
16 end Simple;
9 private
10
17 end Simple.System_A;
15 end Simple.System_A;
9 private
10
15 end Simple.System_B;
12 end Simple.System_B;
4 package Simple.System_AB is
5
16 private
17
23 end Simple.System_AB;
27 end Simple.System_AB;
5 procedure Main is
6
25 S : AB;
26 begin
27 Put_Line ("Activating system AB...");
28 Activate (S);
29
30 Display_Active (S);
31 Display_Check (S);
32
36 Display_Active (S);
37 end Main;
Build output
Compile
[Ada] main.adb
[Ada] simple.adb
[Ada] simple-system_ab.adb
[Ada] simple-system_a.adb
[Ada] simple-system_b.adb
Bind
[gprbind] main.bexch
[Ada] main.ali
Link
[link] main.adb
Runtime output
When analyzing the complete source-code, we see that there are at least two areas that we could
still improve.
The first issue concerns the implementation of the Activate procedure for types derived from
Sys_Base. For those derived types, we're expecting that the Activate procedure of the parent
must be called in the implementation of the overriding Activate procedure. For example:
package body Simple.System_A is
If a developer forgets to call that specific Activate procedure, however, the system won't work as
expected. A better strategy could be the following:
• Declare a new Activation_Reset procedure for Sys_Base type.
• Make a dispatching call to the Activation_Reset procedure in the body of the Activate
procedure (of the Sys_Base type).
• Let the derived types implement their own version of the Activation_Reset procedure.
This is a simplified view of the implementation using the points described above:
package Simple is
end Simple;
E.Active := True;
end Activate;
end Simple;
package Simple.System_A is
private
end Simple.System_A;
end Simple.System_A;
The next area that we could improve is in the declaration of the system AB. In the previous imple-
mentation, we were explicitly describing the two components of that system, namely a component
of type A and a component of type B:
Of course, this declaration matches the system requirements that we presented in the beginning.
However, we could use strategies that make it easier to incorporate requirement changes later on.
For example, we could hide this information about systems A and B by simply declaring an array
of components of type access Sys_Base'Class and allocate them dynamically in the body of
the package. Naturally, this approach might not be suitable for certain platforms. However, the
advantage would be that, if we wanted to replace the component of type B by a new component of
type C, for example, we wouldn't need to change the interface. This is how the updated declaration
could look like:
Important
Note that we're now using the limited keyword in the declaration of type AB. That is necessary
because we want to prevent objects of type AB being copied by assignment, which would lead to
two objects having the same (dynamically allocated) subsystems A and B internally. This change
requires that both Activation_IF and Value_Retrieval_IF are declared limited as well.
Another approach that we could use to implement the dynamic allocation of systems A and B is to
declare AB as a limited controlled type — based on the Limited_Controlled type of the Ada.
Finalization package.
The Limited_Controlled type includes the following operations:
• Initialize, which is called when objects of a type derived from the Limited_Controlled
type are being created — by declaring an object of the derived type, for example —, and
• Finalize, which is called when objects are being destroyed — for example, when an object
gets out of scope at the end of a subprogram where it was created.
In this case, we must override those procedures, so we can use them for dynamic memory alloca-
tion. This is a simplified view of the update implementation:
package Simple.System_AB is
end Simple.System_AB;
end Simple.System_AB;
22 private
23
29 end Simple;
9 E.Active := True;
10 end Activate;
11
20 end Simple;
7 private
8
17 end Simple.System_A;
14 end Simple.System_A;
7 private
8
15 end Simple.System_B;
11 end Simple.System_B;
3 package Simple.System_AB is
4
16 private
17
29 end Simple.System_AB;
48 end Simple.System_AB;
5 procedure Main is
6
25 S : AB;
26 begin
27 Put_Line ("Activating system AB...");
28 Activate (S);
29
30 Display_Active (S);
31 Display_Check (S);
32
36 Display_Active (S);
37 end Main;
Build output
Compile
[Ada] main.adb
[Ada] simple.adb
[Ada] simple-system_ab.adb
[Ada] simple-system_a.adb
[Ada] simple-system_b.adb
Bind
[gprbind] main.bexch
[Ada] main.ali
Link
[link] main.adb
Runtime output
Naturally, this is by no means the best possible implementation of system AB. By applying other
software design strategies that we haven't covered here, we could most probably think of different
ways to use object-oriented programming to improve this implementation. Also, in comparison to
the original implementation (page 229), we recognize that the amount of source-code has grown.
On the other hand, we now have a system that is factored nicely, and also more extensible.
[Jorvik] A New Ravenscar-Based Profile by P. Rogers, J. Ruiz, T. Gingold and P. Bernardi, in Reliable
Software Technologies — Ada Europe 2017, Springer-Verlag Lecture Notes in Computer
Science, Number 10300.
253