The C Developer's Perspective on Ada

What we mean by Embedded Software

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 GNAT Toolchain

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 Pro or at no charge with the GNAT Community edition. 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 Toolchain course.

The GNAT Toolchain for Embedded Targets

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 Platforms

GNAT supports two types of cross platforms:

  • cross targets, where the target device has an embedded operating system.

    • ARM-Linux, which is commonly found in a Raspberry-Pi, is a prominent example.

  • bareboard targets, where the run-times do not depend on an operating system.

    • In this case, the application has direct access to the system hardware.

For each platform, a set of run-time libraries is available. Run-time libraries implement a subset of the Ada language for different use cases, and they're different for each target platform. They may be selected via an attribute in the project's GPR project file or as a command-line switch to GPRbuild. Although the run-time libraries may vary from target to target, the user interface stays the same, providing portability for the application.

Run-time libraries consists of:

  1. Files that are dependent on the target board.

    • These files are responsible for configuring and interacting with the hardware.

    • They are known as a Board Support Package — commonly referred to by their abbrevation BSP.

  2. Code that is target-independent.

    • This code implements language-defined functionality.

The bareboard run-time libraries are provided as customized run-times that are configured to target a very specific micro-controller or processor. Therefore, for different micro-controllers and processors, the run-time libraries need to be ported to the specific target. These are some examples of what needs to be ported:

  • startup code / scripts;

  • clock frequency initializations;

  • memory mapping / allocation;

  • interrupts and interrupt priorities;

  • register descriptions.

For more details on the topic, please refer to the following chapters of the GNAT User’s Guide Supplement for Cross Platforms:

Hello World in Ada

The first piece of code to translate from C to Ada is the usual Hello World program:

[C]

    
    
    
        
#include <stdio.h> int main(int argc, const char * argv[]) { printf("Hello World\n"); return 0; }

[Ada]

    
    
    
        
with Ada.Text_IO; procedure Hello_World is begin Ada.Text_IO.Put_Line ("Hello World"); end 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.

The Ada Syntax

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 syntax 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 coding, but this helps detect problems and vulnerabilities at compile time — early on in the development 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:

abort

else

null

select

abs

elsif

of

separate

abstract

end

or

some

accept

entry

others

subtype

access

exception

out

synchronized

aliased

exit

overriding

tagged

all

for

package

task

and

function

pragma

terminate

array

generic

private

then

at

goto

procedure

type

begin

if

protected

until

body

in

raise

use

case

interface

range

when

constant

is

record

while

declare

limited

rem

with

delay

loop

renames

xor

delta

mod

requeue

digits

new

return

do

not

reverse

Compilation Unit Structure

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 extension for implementations.

One main difference between the C and Ada compilation structure is that Ada compilation units are structured into something called packages.

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;

The package implementation, or body, has the structure:

--  my_package.adb
package body My_Package is

   --  implementation

end My_Package;

Declaration Protection

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;

package body Package_Name is
   -- implementation
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 package state/data arbitrarily. The private reserved word splits the package spec into public and private parts. For example:

[Ada]

    
    
    
        
package Types is type Type_1 is private; type Type_2 is private; type Type_3 is private; procedure P (X : Type_1); -- ... private procedure Q (Y : Type_1); type Type_1 is new Integer range 1 .. 1000; type Type_2 is array (Integer range 1 .. 1000) of Integer; type Type_3 is record A, B : Integer; end record; 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.

Hierarchical Packages

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

package body Root.Child is
   --  package body goes here
end Root.Child;

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.

Using Entities from Packages

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]

    
    
    
        
-- pck.ads package Pck is My_Glob : Integer; end Pck;
-- main.adb with Pck; procedure Main is begin Pck.My_Glob := 0; end Main;

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 Packages.

Statements and Declarations

The following code samples are all equivalent, and illustrate the use of comments and working with integer variables:

[C]

    
    
    
        
#include <stdio.h> int main(int argc, const char * argv[]) { // variable declarations int a = 0, b = 0, c = 100, d; // c shorthand for increment a++; // regular addition d = a + b + c; // printing the result printf("d = %d\n", d); return 0; }

[Ada]

    
    
    
        
with Ada.Text_IO; procedure Main is -- variable declaration A, B : Integer := 0; C : Integer := 100; D : Integer; begin -- Ada does not have a shortcut format for increment like in C A := A + 1; -- regular addition D := A + B + C; -- printing the result Ada.Text_IO.Put_Line ("D =" & D'Img); end Main;

You'll notice that, in both languages, statements are terminated with a semicolon. This means that you can have multi-line statements.

The shortcuts of incrementing and decrementing

You may have noticed that Ada does not have something similar to the a++ or a-- operators. Instead you must use the full assignment A := A + 1 or A := A - 1.

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]

    
    
    
        
/* The C89 version */ #include <stdio.h> int average(int* list, int length) { int i; int sum = 0; for(i = 0; i < length; ++i) { sum += list[i]; } return (sum / length); } int main(int argc, const char * argv[]) { int vals[] = { 2, 2, 4, 4 }; printf("Average: %d\n", average(vals, 4)); return 0; }

[C]

    
    
    
        
// The modern C way #include <stdio.h> int average(int* list, int length) { int sum = 0; for(int i = 0; i < length; ++i) { sum += list[i]; } return (sum / length); } int main(int argc, const char * argv[]) { int vals[] = { 2, 2, 4, 4 }; printf("Average: %d\n", average(vals, 4)); return 0; }

For the fun of it, let's also see the Ada way to do this:

[Ada]

    
    
    
        
with Ada.Text_IO; procedure Main is type Int_Array is array (Natural range <>) of Integer; function Average (List : Int_Array) return Integer is Sum : Integer := 0; begin for I in List'Range loop Sum := Sum + List (I); end loop; return (Sum / List'Length); end Average; Vals : constant Int_Array (1 .. 4) := (2, 2, 4, 4); begin Ada.Text_IO.Put_Line ("Average: " & Integer'Image (Average (Vals))); end Main;

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!

Declaration Flippy Floppy

Something peculiar that you may have noticed about declarations in Ada is that they are backwards from the way C does declarations. The C language expects the type followed by the variable name. Ada expects the variable name followed by a colon and then the type.

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]

    
    
    
        
with Ada.Text_IO; procedure Main is -- variable declaration A, B : Integer := 0; C : Integer := 100; D : Integer; begin -- Ada does not have a shortcut format for increment like in C A := A + 1; -- regular addition D := A + B + C; -- printing the result Ada.Text_IO.Put_Line ("D =" & D'Img); declare E : constant Integer := D * 100; begin -- printing the result Ada.Text_IO.Put_Line ("E =" & E'Img); end; end Main;

Notice that we declared a new variable E whose scope only exists in our newly defined block. The equivalent C code is:

[C]

    
    
    
        
#include <stdio.h> int main(int argc, const char * argv[]) { // variable declarations int a = 0, b = 0, c = 100, d; // c shorthand for increment a++; // regular addition d = a + b + c; // printing the result printf("d = %d\n", d); { const int e = d * 100; printf("e = %d\n", e); } return 0; }

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]

    
    
    
        
#include <stdio.h> int main(int argc, const char * argv[]) { int a = 0; if (a = 10) printf("True\n"); else printf("False\n"); return 0; }

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]

    
    
    
        
with Ada.Text_IO; use Ada.Text_IO; procedure Main is A : Integer := 0; begin if A := 10 then Put_Line ("True"); else Put_Line ("False"); end if; end Main;

The above code will not compile. This is because Ada does no allow assignment as an expression.

The "use" clause

You'll notice in the above code example, after with Ada.Text_IO; there is a new statement we haven't seen before — use Ada.Text_IO;. You may also notice that we are not using the Ada.Text_IO prefix before the Put_Line statements. When we add the use clause it tells the compiler that we won't be using the prefix in the call to subprograms of that package. The use clause is something to use with caution. For example: if we use the Ada.Text_IO package and we also have a Put_Line subprogram in our current compilation unit with the same signature, we have a (potential) collision!

Conditions

The syntax of an if statement:

[C]

    
    
    
        
#include <stdio.h> int main(int argc, const char * argv[]) { // try changing the initial value to change the // output of the program int v = 0; if (v > 0) { printf("Positive\n"); } else if (v < 0) { printf("Negative\n"); } else { printf("Zero\n"); } return 0; }

[Ada]

    
    
    
        
with Ada.Text_IO; use Ada.Text_IO; procedure Main is -- try changing the initial value to change the -- output of the program V : constant Integer := 0; begin if V > 0 then Put_Line ("Positive"); elsif V < 0 then Put_Line ("Negative"); else Put_Line ("Zero"); end if; end Main;

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

The syntax of a switch/case statement:

[C]

    
    
    
        
#include <stdio.h> int main(int argc, const char * argv[]) { // try changing the initial value to change the // output of the program int v = 0; switch(v) { case 0: printf("Zero\n"); break; case 1: case 2: case 3: case 4: case 5: case 6: case 7: case 8: case 9: printf("Positive\n"); break; case 10: case 12: case 14: case 16: case 18: printf("Even number between 10 and 18\n"); break; default: printf("Something else\n"); break; } return 0; }

[Ada]

    
    
    
        
with Ada.Text_IO; use Ada.Text_IO; procedure Main is -- try changing the initial value to change the -- output of the program V : constant Integer := 0; begin case V is when 0 => Put_Line ("Zero"); when 1 .. 9 => Put_Line ("Positive"); when 10 | 12 | 14 | 16 | 18 => Put_Line ("Even number between 10 and 18"); when others => Put_Line ("Something else"); end case; end Main;

Switch or Case?

A switch statement in C is the same as a case statement in Ada. This may be a little strange because 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]

    
    
    
        
#include <stdio.h> int main(int argc, const char * argv[]) { int v = 0; switch(v) { case 0: printf("Zero\n"); case 1: printf("One\n"); default: printf("Other\n"); } return 0; }

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.

Loops

Let's start with some syntax:

[C]

    
    
    
        
#include <stdio.h> int main(int argc, const char * argv[]) { int v; // this is a while loop v = 1; while(v < 100) { v *= 2; } printf("v = %d\n", v); // this is a do while loop v = 1; do { v *= 2; } while(v < 200); printf("v = %d\n", v); // this is a for loop v = 0; for(int i = 0; i < 5; ++i) { v += (i * i); } printf("v = %d\n", v); // this is a forever loop with a conditional exit v = 0; while(1) { // do stuff here v += 1; if(v == 10) break; } printf("v = %d\n", v); // this is a loop over an array { #define ARR_SIZE (10) const int arr[ARR_SIZE] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; int sum = 0; for(int i = 0; i < ARR_SIZE; ++i) { sum += arr[i]; } printf("sum = %d\n", sum); } return 0; }

[Ada]

    
    
    
        
with Ada.Text_IO; procedure Main is V : Integer; begin -- this is a while loop V := 1; while V < 100 loop V := V * 2; end loop; Ada.Text_IO.Put_Line ("V = " & Integer'Image (V)); -- Ada doesn't have an explicit do while loop -- instead you can use the loop and exit keywords V := 1; loop V := V * 2; exit when V >= 200; end loop; Ada.Text_IO.Put_Line ("V = " & Integer'Image (V)); -- this is a for loop V := 0; for I in 0 .. 4 loop V := V + (I * I); end loop; Ada.Text_IO.Put_Line ("V = " & Integer'Image (V)); -- this is a forever loop with a conditional exit V := 0; loop -- do stuff here V := V + 1; exit when V = 10; end loop; Ada.Text_IO.Put_Line ("V = " & Integer'Image (V)); -- this is a loop over an array declare type Int_Array is array (Natural range 1 .. 10) of Integer; Arr : constant Int_Array := (1, 2, 3, 4, 5, 6, 7, 8, 9, 10); Sum : Integer := 0; begin for I in Arr'Range loop Sum := Sum + Arr (I); end loop; Ada.Text_IO.Put_Line ("Sum = " & Integer'Image (Sum)); end; end Main;

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]

for (initialization expression; loop predicate; update expression) {
    // some statements
}

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]

    
    
    
        
#include <stdio.h> #define MY_RANGE (10) int main(int argc, const char * argv[]) { for (int i = MY_RANGE; i >= 0; --i) { printf("%d\n", i); } return 0; }

[Ada]

    
    
    
        
with Ada.Text_IO; use Ada.Text_IO; procedure Main is My_Range : constant := 10; begin for I in reverse 0 .. My_Range loop Put_Line (I'Img); end loop; end Main;

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.

In the above example, we are traversing over the range in reverse order. In Ada, we use the reverse 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]

    
    
    
        
#include <stdio.h> #define LIST_LENGTH (100) int main(int argc, const char * argv[]) { int list[LIST_LENGTH]; for(int i = LIST_LENGTH; i > 0; --i) { list[i] = LIST_LENGTH - i; } for (int i = 0; i < LIST_LENGTH; ++i) { printf("%d ", list[i]); if (i % 10 == 0) { printf("\n"); } } return 0; }

[Ada]

    
    
    
        
with Ada.Text_IO; use Ada.Text_IO; procedure Main is type Int_Array is array (Natural range 1 .. 100) of Integer; List : Int_Array; begin for I in reverse List'Range loop List (I) := List'Last - I; end loop; for I in List'Range loop Put (List (I)'Img & " "); if I mod 10 = 0 then New_Line; end if; end loop; end Main;

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]

    
    
    
        
with Ada.Text_IO; use Ada.Text_IO; procedure Main is type Int_Array is array (Natural range 1 .. 100) of Integer; List : Int_Array; begin for I in reverse List'Range loop List (I) := List'Last - I; end loop; for I of List loop Put (I'Img & " "); if I mod 10 = 0 then New_Line; end if; end loop; end Main;

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.

Type System

Strong Typing

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]

    
    
    
        
#include <stdio.h> int main(int argc, const char * argv[]) { unsigned char a = 0xFF; char b = 0xFF; printf("Does a == b?\n"); if(a == b) printf("Yes.\n"); else printf("No.\n"); printf("a: 0x%08X, b: 0x%08X\n", a, b); return 0; }

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]

    
    
    
        
with Ada.Text_IO; use Ada.Text_IO; procedure Main is type Char is range 0 .. 255; type Unsigned_Char is mod 256; A : Char := 16#FF#; B : Unsigned_Char := 16#FF#; begin Put_Line ("Does A = B?"); if A = B then Put_Line ("Yes"); else Put_Line ("No"); end if; end Main;

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]

    
    
    
        
with Ada.Text_IO; use Ada.Text_IO; procedure Strong_Typing is Alpha : constant Integer := 1; Beta : constant Integer := 10; Result : Float; begin Result := Float (Alpha) / Float (Beta); Put_Line (Float'Image (Result)); end Strong_Typing;

[C]

    
    
    
        
#include <stdio.h> void weakTyping (void) { const int alpha = 1; const int beta = 10; float result; result = alpha / beta; printf("%f\n", result); } int main(int argc, const char * argv[]) { weakTyping(); return 0; }

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

The complete example would then be:

[Ada]

    
    
    
        
with Ada.Text_IO; use Ada.Text_IO; procedure Strong_Typing is Alpha : constant Integer := 1; Beta : constant Integer := 10; Result : Float; begin Result := Float (Alpha / Beta); Put_Line (Float'Image (Result)); end Strong_Typing;

Floating Point Literals

In Ada, a floating point literal must be written with both an integral and decimal part. 10 is not a valid literal for a floating point value, while 10.0 is.

Language-Defined Types

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 precision.

Application-Defined Types

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]

    
    
    
        
procedure Main is type Distance is new Float; type Area is new Float; D1 : Distance := 2.0; D2 : Distance := 3.0; A : Area; begin D1 := D1 + D2; -- OK D1 := D1 + A; -- NOT OK: incompatible types for "+" A := D1 * D2; -- NOT OK: incompatible types for ":=" A := Area (D1 * D2); -- OK end Main;

Even though the Distance and Area types above are just Float, the compiler does not allow arbitrary 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 multiplying 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 mechanisms. 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 operator 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]

    
    
    
        
procedure Main is type Day is (Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday); D : Day := Monday; begin null; end Main;

[C]

    
    
    
        
enum Day { Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday }; int main(int argc, const char * argv[]) { enum Day d = Monday; return 0; }

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 declaration:

[C]

    
    
    
        
#include <stdio.h> enum Day { Monday = 10, Tuesday = 11, Wednesday = 12, Thursday = 13, Friday = 14, Saturday = 15, Sunday = 16 }; int main(int argc, const char * argv[]) { enum Day d = Monday; printf("d = %d\n", d); return 0; }

But in Ada you must use both a type definition for Day as well as a separate representation clause for it like:

[Ada]

    
    
    
        
with Ada.Text_IO; procedure Main is type Day is (Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday); -- Representation clause for Day type: for Day use (Monday => 10, Tuesday => 11, Wednesday => 12, Thursday => 13, Friday => 14, Saturday => 15, Sunday => 16); D : Day := Monday; V : Integer; begin V := Day'Enum_Rep (D); Ada.Text_IO.Put_Line (Integer'Image (V)); end Main;

Note that however, unlike C, values for enumerations in Ada have to be unique.

Type Ranges

Contracts can be associated with types and variables, to refine values and define what are considered valid values. The most common kind of contract is a range constraint introduced with the range reserved word, for example:

[Ada]

    
    
    
        
procedure Main is type Grade is range 0 .. 100; G1, G2 : Grade; N : Integer; begin -- ... -- Initialization of N G1 := 80; -- OK G1 := N; -- Illegal (type mismatch) G1 := Grade (N); -- Legal, run-time range check G2 := G1 + 10; -- Legal, run-time range check G1 := (G1 + G2) / 2; -- Legal, run-time range check end Main;

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]

    
    
    
        
with Ada.Text_IO; procedure Main is type Grade is range 0 .. 100; G1, G2 : Grade := 99; begin G1 := Grade ((Integer (G1) + Integer (G2)) / 2); Ada.Text_IO.Put_Line (Grade'Image (G1)); end Main;

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]

    
    
    
        
procedure Main is type Day is (Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday); type Business_Day is new Day range Monday .. Friday; type Weekend_Day is new Day range Saturday .. Sunday; begin null; end Main;

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]

    
    
    
        
procedure Main is type Day is (Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday); subtype Business_Day is Day range Monday .. Friday; subtype Weekend_Day is Day range Saturday .. Sunday; subtype Dice_Throw is Integer range 1 .. 6; begin null; end Main;

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 hardware numeric type. That's a definition of portability that is preferable to having something compile anywhere but not run correctly, as in C.

Unsigned And Modular Types

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 Natural.

  • declare custom modular types.

The following table presents the main features of each type. We discuss these types right after.

Feature

[C] unsigned int

[Ada] Unsigned range

[Ada] Modular

Excludes negative value

Wraparound

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]

    
    
    
        
with Ada.Text_IO; use Ada.Text_IO; procedure Main is type Unsigned_Int_32 is range 0 .. 2 ** 32 - 1; X : Unsigned_Int_32 := 42; begin Put_Line ("X = " & Unsigned_Int_32'Image (X)); end Main;

In C, when unsigned int has a size of 32 bits, this corresponds to the following declaration:

[C]

    
    
    
        
#include <stdio.h> #include <limits.h> int main(int argc, const char * argv[]) { unsigned int x = 42; printf("x = %u\n", x); return 0; }

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]

    
    
    
        
with Ada.Text_IO; use Ada.Text_IO; procedure Main is type Signed_Int_32 is range -2 ** 31 .. 2 ** 31 - 1; subtype Unsigned_Int_31 is Signed_Int_32 range 0 .. Signed_Int_32'Last; -- Equivalent to: -- subtype Unsigned_Int_31 is Signed_Int_32 range 0 .. 2 ** 31 - 1; X : Unsigned_Int_31 := 42; begin Put_Line ("X = " & Unsigned_Int_31'Image (X)); end Main;

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:

subtype Natural  is Integer range 0..Integer'Last;
subtype Positive is Integer range 1..Integer'Last;

Since they're standard subtypes, you can declare variables of those subtypes directly in your implementation, 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]

    
    
    
        
#include <stdio.h> #include <limits.h> int main(int argc, const char * argv[]) { unsigned int x = UINT_MAX + 1; /* Now: x == 0 */ printf("x = %u\n", x); return 0; }

The corresponding code in Ada raises an exception:

[Ada]

    
    
    
        
with Ada.Text_IO; use Ada.Text_IO; procedure Main is type Unsigned_Int_32 is range 0 .. 2 ** 32 - 1; X : Unsigned_Int_32 := Unsigned_Int_32'Last + 1; -- Overflow: exception is raised! begin Put_Line ("X = " & Unsigned_Int_32'Image (X)); end Main;

While the C uses modulo arithmetic for unsigned integer, Ada doesn't use it for the Unsigned_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]

    
    
    
        
with Ada.Text_IO; use Ada.Text_IO; procedure Main is type Unsigned_32 is mod 2**32; X : Unsigned_32 := Unsigned_32'Last + 1; -- Now: X = 0 begin Put_Line ("X = " & Unsigned_32'Image (X)); end Main;

In this case, the behavior is the same as in the C declaration above.

Modular types, unlike Ada's signed integers, also provide bit-wise operations, a typical application for unsigned integers in C. In Ada, you can use operators such as and, or, xor and not. You can also use typical bit-shifting operations, such as Shift_Left, Shift_Right, Shift_Right_Arithmetic, Rotate_Left and Rotate_Right.

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 example, 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]

    
    
    
        
with Ada.Text_IO; use Ada.Text_IO; procedure Main is A : Integer := 10; begin Put_Line (Integer'Image (A)); A := Integer'Value ("99"); Put_Line (Integer'Image (A)); end Main;

Important

Semantically, attributes are equivalent to subprograms. For example, Integer'Image is defined as follows:

function Integer'Image(Arg : Integer'Base) return String;

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]

    
    
    
        
with Ada.Text_IO; use Ada.Text_IO; procedure Main is C : Character := 'a'; begin Put (C); C := Character'Val (Character'Pos (C) + 1); Put (C); end Main;

A more concise way to get the next value in Ada is to use the 'Succ attribute:

[Ada]

    
    
    
        
with Ada.Text_IO; use Ada.Text_IO; procedure Main is C : Character := 'a'; begin Put (C); C := Character'Succ (C); Put (C); end Main;

You can get the previous value using the 'Pred attribute. Here is the equivalent in C:

[C]

    
    
    
        
#include <stdio.h> int main(int argc, const char * argv[]) { char c = 'a'; printf("%c", c); c++; printf("%c", c); return 0; }

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.

Arrays and Strings

C arrays are pointers with offsets, but the same is not the case for Ada. Arrays in Ada are not interchangeable 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]

    
    
    
        
with Ada.Text_IO; use Ada.Text_IO; procedure Main is type Arr_Type is array (Integer range <>) of Character; Arr : Arr_Type (1 .. 26); C : Character := 'a'; begin for I in Arr'Range loop Arr (I) := C; C := Character'Succ (C); Put (Arr (I) & " "); if I mod 7 = 0 then New_Line; end if; end loop; end Main;

[C]

    
    
    
        
#include <stdio.h> int main(int argc, const char * argv[]) { char Arr [26]; char C = 'a'; for (int I = 0; I < 26; ++I) { Arr [I] = C++; printf ("%c ", Arr [I]); if ((I + 1) % 7 == 0) { printf ("\n"); } } return 0; }

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, inclusive. 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]

    
    
    
        
with Ada.Text_IO; use Ada.Text_IO; procedure Main is My_String : String (1 .. 19) := "This is an example!"; begin Put_Line (My_String); end Main;

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]

    
    
    
        
with Ada.Text_IO; use Ada.Text_IO; procedure Main is My_String : String := "This is a line" & ASCII.LF; begin Put (My_String); end Main;

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]

    
    
    
        
with Ada.Text_IO; use Ada.Text_IO; procedure Main is type Arr_Type is array (Integer range <>) of Integer; A1 : Arr_Type (1 .. 2); A2 : Arr_Type (1 .. 2); begin A1 (1) := 0; A1 (2) := 1; A2 := A1; for I in A2'Range loop Put_Line (Integer'Image (A2 (I))); end loop; end Main;

[C]

    
    
    
        
#include <stdio.h> #include <string.h> int main(int argc, const char * argv[]) { int A1 [2]; int A2 [2]; A1 [0] = 0; A1 [1] = 1; memcpy (A2, A1, sizeof (int) * 2); for (int i = 0; i < 2; i++) { printf("%d\n", A2[i]); } return 0; }

In all of the examples above, the source and destination arrays must have precisely the same number of elements. Ada allows you to easily specify a portion, or slice, of an array. So you can write the following:

[Ada]

    
    
    
        
with Ada.Text_IO; use Ada.Text_IO; procedure Main is type Arr_Type is array (Integer range <>) of Integer; A1 : Arr_Type (1 .. 10) := (1, 2, 3, 4, 5, 6, 7, 8, 9, 10); A2 : Arr_Type (1 .. 5) := (1, 2, 3, 4, 5); begin A2 (1 .. 3) := A1 (4 .. 6); for I in A2'Range loop Put_Line (Integer'Image (A2 (I))); end loop; end Main;

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]

    
    
    
        
with Ada.Text_IO; use Ada.Text_IO; procedure Main is type Arr_Type is array (Integer range <>) of Integer; A1 : Arr_Type (1 .. 2) := (10, 20); A2 : Arr_Type (1 .. 2) := (10, 20); begin if A1 = A2 then Put_Line ("A1 = A2"); else Put_Line ("A1 /= A2"); end if; end Main;

[C]

    
    
    
        
#include <stdio.h> int main(int argc, const char * argv[]) { int A1 [2] = { 10, 20 }; int A2 [2] = { 10, 20 }; int eq = 1; for (int i = 0; i < 2; ++i) { if (A1 [i] != A2 [i]) { eq = 0; break; } } if (eq) { printf("A1 == A2\n"); } else { printf("A1 != A2\n"); } return 0; }

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]

    
    
    
        
with Ada.Text_IO; use Ada.Text_IO; procedure Main is type Arr_Type is array (Integer range <>) of Integer; A1 : Arr_Type (-2 .. 42) := (others => 0); begin -- use a slice to assign A1 elements 11 .. 19 to 1 A1 (11 .. 19) := (others => 1); Put_Line ("---- A1 ----"); for I in A1'Range loop Put_Line (Integer'Image (I) & " => " & Integer'Image (A1 (I))); end loop; end Main;

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]

    
    
    
        
with Ada.Text_IO; use Ada.Text_IO; procedure Main is type Arr_Type is array (Integer range <>) of Integer; A1 : Arr_Type := (1, 2, 3, 4, 5, 6, 7, 8, 9); begin A1 := (1, 2, 3, others => 10); Put_Line ("---- A1 ----"); for I in A1'Range loop Put_Line (Integer'Image (I) & " => " & Integer'Image (A1 (I))); end loop; end Main;

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.

Heterogeneous Data Structures

The structure corresponding to a C struct is an Ada record. Here are some simple records:

[Ada]

    
    
    
        
with Ada.Text_IO; use Ada.Text_IO; procedure Main is type R is record A, B : Integer; C : Float; end record; V : R; begin V.A := 0; Put_Line ("V.A = " & Integer'Image (V.A)); end Main;

[C]

    
    
    
        
#include <stdio.h> struct R { int A, B; float C; }; int main(int argc, const char * argv[]) { struct R V; V.A = 0; printf("V.A = %d\n", V.A); return 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]

    
    
    
        
with Ada.Text_IO; use Ada.Text_IO; procedure Main is type R is record A, B : Integer := 0; C : Float := 0.0; end record; procedure Put_R (V : R; Name : String) is begin Put_Line (Name & " = (" & Integer'Image (V.A) & ", " & Integer'Image (V.B) & ", " & Float'Image (V.C) & ")"); end Put_R; V1 : constant R := (1, 2, 1.0); V2 : constant R := (A => 1, B => 2, C => 1.0); V3 : constant R := (C => 1.0, A => 1, B => 2); V4 : constant R := (C => 1.0, others => <>); begin Put_R (V1, "V1"); Put_R (V2, "V2"); Put_R (V3, "V3"); Put_R (V4, "V4"); end Main;

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. However, 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 pointers. 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]

    
    
    
        
with Ada.Text_IO; use Ada.Text_IO; procedure Main is type R is record A, B : Integer; end record; procedure Put_R (V : R; Name : String) is begin Put_Line (Name & " = (" & Integer'Image (V.A) & ", " & Integer'Image (V.B) & ")"); end Put_R; V1, V2 : R; begin V1.A := 0; V2 := V1; V2.A := 1; Put_R (V1, "V1"); Put_R (V2, "V2"); end Main;

[C]

    
    
    
        
#include <stdio.h> struct R { int A, B; }; void print_r(const struct R *v, const char *name) { printf("%s = (%d, %d)\n", name, v->A, v->B); } int main(int argc, const char * argv[]) { struct R V1, V2; V1.A = 0; V2 = V1; V2.A = 1; print_r(&V1, "V1"); print_r(&V2, "V2"); return 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.

Here's now a similar example, but using heap allocation instead:

[Ada]

    
    
    
        
with Ada.Text_IO; use Ada.Text_IO; procedure Main is type R is record A, B : Integer; end record; type R_Access is access R; procedure Put_R (V : R; Name : String) is begin Put_Line (Name & " = (" & Integer'Image (V.A) & ", " & Integer'Image (V.B) & ")"); end Put_R; V1 : R_Access; V2 : R_Access; begin V1 := new R; V1.A := 0; V2 := V1; V2.A := 1; Put_R (V1.all, "V1"); Put_R (V2.all, "V2"); end Main;

[C]

    
    
    
        
#include <stdio.h> #include <stdlib.h> struct R { int A, B; }; void print_r(const struct R *v, const char *name) { printf("%s = (%d, %d)\n", name, v->A, v->B); } int main(int argc, const char * argv[]) { struct R * V1, * V2; V1 = malloc(sizeof(struct R)); V1->A = 0; V2 = V1; V2->A = 1; print_r(V1, "V1"); print_r(V2, "V2"); return 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]

    
    
    
        
procedure Main is type A_Int is access Integer; Var : A_Int := new Integer; begin Var.all := 0; end Main;

[C]

    
    
    
        
#include <stdlib.h> int main(int argc, const char * argv[]) { int * Var = malloc (sizeof(int)); *Var = 0; return 0; }

In Ada, an initializer can be specified with the allocation by appending '(value):

[Ada]

    
    
    
        
procedure Main is type A_Int is access Integer; Var : A_Int := new Integer'(0); begin null; end Main;

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]

    
    
    
        
procedure Main is type A_Int is access all Integer; Var : aliased Integer; Ptr : A_Int := Var'Access; begin null; end Main;

[C]

    
    
    
        
int main(int argc, const char * argv[]) { int Var; int * Ptr = &Var; return 0; }

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]

    
    
    
        
with Ada.Unchecked_Deallocation; procedure Main is type Integer_Access is access all Integer; procedure Free is new Ada.Unchecked_Deallocation (Integer, Integer_Access); My_Pointer : Integer_Access := new Integer; begin Free (My_Pointer); end Main;

[C]

    
    
    
        
#include <stdlib.h> int main(int argc, const char * argv[]) { int * my_pointer = malloc (sizeof(int)); free (my_pointer); return 0; }

We'll discuss generics later in this section.

Functions and Procedures

General Form

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.

Here's a first example:

[Ada]

    
    
    
        
procedure Proc (Var1 : Integer; Var2 : out Integer; Var3 : in out Integer);
function Func (Var : Integer) return Integer;
with Func; procedure Proc (Var1 : Integer; Var2 : out Integer; Var3 : in out Integer) is begin Var2 := Func (Var1); Var3 := Var3 + 1; end Proc;
function Func (Var : Integer) return Integer is begin return Var + 1; end Func;
with Ada.Text_IO; use Ada.Text_IO; with Proc; procedure Main is V1, V2 : Integer; begin V2 := 2; Proc (5, V1, V2); Put_Line ("V1: " & Integer'Image (V1)); Put_Line ("V2: " & Integer'Image (V2)); end Main;

[C]

    
    
    
        
void Proc (int Var1, int * Var2, int * Var3);
int Func (int Var);
#include "func.h" void Proc (int Var1, int * Var2, int * Var3) { *Var2 = Func (Var1); *Var3 += 1; }
int Func (int Var) { return Var + 1; }
#include <stdio.h> #include "proc.h" int main(int argc, const char * argv[]) { int v1, v2; v2 = 2; Proc (5, &v1, &v2); printf("v1: %d\n", v1); printf("v2: %d\n", v2); return 0; }

The first two declarations for Proc and Func are specifications of the subprograms which are being 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 parameters. If there are no parameters, the parentheses must be omitted entirely from both the declaration 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.

An expression function is static when the Static aspect is specified. For example:

procedure Main is

   X1 : constant := (if True then 37 else 42);

   function If_Then_Else (Flag : Boolean; X, Y : Integer)
     return Integer is
      (if Flag then X else Y) with Static;

   X2 : constant := If_Then_Else (True, 37, 42);

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.

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]

    
    
    
        
package Machine is type Status is (Off, On); type Code is new Integer range 0 .. 3; type Threshold is new Float range 0.0 .. 10.0; function Get (S : Status) return Code; function Get (S : Status) return Threshold; end Machine;
package body Machine is function Get (S : Status) return Code is begin case S is when Off => return 1; when On => return 3; end case; end Get; function Get (S : Status) return Threshold is begin case S is when Off => return 2.0; when On => return 10.0; end case; end Get; end Machine;
with Ada.Text_IO; use Ada.Text_IO; with Machine; use Machine; procedure Main is S : Status; C : Code; T : Threshold; begin S := On; C := Get (S); T := Get (S); Put_Line ("S: " & Status'Image (S)); Put_Line ("C: " & Code'Image (C)); Put_Line ("T: " & Threshold'Image (T)); end Main;

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 operators 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]

    
    
    
        
package Machine_2 is type Status is (Off, Waiting, On); type Input is new Float range 0.0 .. 10.0; function Get (I : Input) return Status; function "=" (Left : Input; Right : Status) return Boolean; end Machine_2;
package body Machine_2 is function Get (I : Input) return Status is begin if I >= 0.0 and I < 3.0 then return Off; elsif I >= 3.0 and I < 6.5 then return Waiting; else return On; end if; end Get; function "=" (Left : Input; Right : Status) return Boolean is begin return Get (Left) = Right; end "="; end Machine_2;
with Ada.Text_IO; use Ada.Text_IO; with Machine_2; use Machine_2; procedure Main is I : Input; begin I := 3.0; if I = Off then Put_Line ("Machine is off."); else Put_Line ("Machine is not off."); end if; end Main;

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>;

function Some_Function is <function_definition>
  with Some_Aspect => <aspect_specification>;

type Some_Type is <type_definition>
  with Some_Aspect => <aspect_specification>;

Obj : Some_Type with Some_Aspect => <aspect_specification>;

For example, you can inline a subprogram by specifying the Inline aspect:

[Ada]

    
    
    
        
package Float_Arrays is type Float_Array is array (Positive range <>) of Float; function Average (Data : Float_Array) return Float with Inline; end Float_Arrays;

We'll discuss inlining later in this course.

Aspect specifications were introduced in Ada 2012. In previous versions of Ada, you had to use a pragma instead. The previous example would be written as follows:

[Ada]

    
    
    
        
package Float_Arrays is type Float_Array is array (Positive range <>) of Float; function Average (Data : Float_Array) return Float; pragma Inline (Average); 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]

    
    
    
        
package My_Device_Types is type UInt10 is mod 2 ** 10 with Size => 10; 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]

    
    
    
        
with Ada.Text_IO; use Ada.Text_IO; with My_Device_Types; use My_Device_Types; procedure Show_Device_Types is UInt10_Obj : constant UInt10 := 0; begin Put_Line ("Size of UInt10 type: " & Positive'Image (UInt10'Size)); Put_Line ("Size of UInt10 object: " & Positive'Image (UInt10_Obj'Size)); end Show_Device_Types;

We'll explain both Size aspect and Size attribute later in this course.