Ada, an internationally standardized object-oriented language, was designed for programming large, long-lived systems, especially those with stringent reliability requirements. This introduction is a survey of Ada's key features, with a focus on those most useful to embedded programmers.
Ada, an internationally standardized object-oriented programming language, was designed in the early '80s and updated in the mid '90s for use in large, long-lived systems, especially those with stringent reliability requirements.
Unlike C, Ada was specifically intended to meet the needs of embedded and real-time applications, which must be reliable as well as efficient. Typical examples are aircraft avionics, command and control, and transportation systems.
Ada can be used as a procedural programming language with built-in support for multitasking and hardware control and as an object-oriented programming language complete with all the trimmings.
A strongly typed language with a Pascal-like syntax, Ada offers structured control statements; procedures and functions (known collectively as subprograms); data type-definition facilities; pointers (called access values in Ada) to dynamically allocated objects, declared objects, and subprograms; block structure; exception handling; and a set of standard libraries. Ada has a traditional run-time data model comprising static storage, stack space for local variables and formal parameters, and a heap for dynamically allocated objects. Ada does not have garbage collection, but does include several mechanisms by which the programmer can prevent memory leaks.
A concurrent program may be regarded as a collection of cooperating active entities (each with a thread of control and a virtual or actual processor) interacting with each other and sharing resources. Ada's main concurrency constructs are the task, which models an active entity, and the protected object, which models a shared data structure that needs to be accessed with mutual exclusion. Tasks can communicate with each other directly (through a synchronous mechanism known as a rendezvous), or indirectly through protected objects or shared data.
Ada also has a variety of low-level features needed for systems programming. For example, the programmer can locate data objects at specific physical memory locations, specify the representation of objects, treat addresses as pointers to typed data objects and vice versa, and define interrupt handlers.
For use in real-time systems, Ada provides access to one or more clocks, allows the programmer to define periodic tasks, specifies the effect of priorities on task scheduling and queue management, offers mechanisms to avoid priority inversions, and supplies constructs that allow tasks to time out on various events.
Ada's secure underpinnings (strong typing, and so on) as well as specialized functionality, such as mechanisms to promote traceability between source and object code, make the language applicable to systems that must be certified against safety standards such as DO-178B. Moreover, with suitable restrictions on the tasking features, concurrent Ada programs can be certified against such standards.
Programming in the large
Several features directly support programming in the large and object-oriented programming. A package facility provides for modularization, allowing the grouping of related entities (for example a type together with its operations). An Ada program typically comprises a main subprogram together with the packages that the subprogram depends upon, either directly or indirectly. Flexible separate compilation facilities support both bottom-up and top-down development. Generic templates promote component reusability (for example, a queue data type that is parameterized by its element type). Ada's multi-level namespace minimizes the likelihood of clashes between the names of different modules.
Ada supports encapsulation in a variety of ways, including the separation of a package into a specification (the interface) and a body (the implementation), and private types. A “client” unit that references an object of a private type has no access to the “state” information reflected in the data fields; it can only access the object through the operations provided for the type.
Ada provides direct support for object-oriented programming: encapsulation (as just noted), objects (entities that have state and operations), classes (abstractions of objects), inheritance, polymorphism, and dynamic binding.
Ada also supports programming by extension, which gives the programmer the ability to extend a type or module without changing the original. Ada's approach to object-oriented programming supports type extension. Ada's child library mechanism supports module extension: you can extend a package's capabilities by defining a child unit that contains the new declarations. Special visibility properties into the parent package make it possible for the programmer to implement the child unit's operations.
Multitasking made easy
The program in Listing 1 demonstrates Ada's built-in support for multitasking. It has two threads of control: an explicit task, Outputter , which increments a counter at intervals of no less than one second, and the main procedure, Tasking_Example , invoked by an implicit environment task, which suspends itself for 20 seconds and then sets a shared variable that causes the other task to terminate.
Listing 1 An Ada multitasking program
1 with Ada.Text_IO;2 procedure Tasking_Example is3 Finished : Boolean := False;4 pragma Atomic(Finished);56 task Outputter; -- task specification78 task body Outputter is -- task body9 Count : Integer := 1;10 begin11 while not Finished loop12 Ada.Text_IO.Put_Line(Integer'Image(Count));13 Count := Count + 1;14 delay 1.0; -- one second15 end loop;16 Ada.Text_IO.Put_Line("Terminating");17 end Outputter;1819 begin20 delay 20.0; -- twenty seconds21 Finished := True;22 end Tasking_Example;
The with Ada.Text_IO clause on Line 1 makes the contents of standard library package Ada.Text_IO available to the Tasking_Example procedure. This package contains the Put_Line procedure for displaying a string.
The variable Finished is used to communicate between the main procedure and the Outputter task. Specifying Atomic via pragma prevents the compiler from caching the variable locally within the Outputter task; such an optimization would defeat the intent of the program. Atomic also checks that the variable is accessed atomically-that no context switching can occur in the middle of reads or writes.
The Outputter task is simply a loop, terminated when Finished is True , that displays the current value of Count , increments Count , and then suspends itself for (at least) one second. When the loop is exited, control transfers to the statement that displays the “Terminating” message, and the task then terminates.
The Outputter task is activated at the begin of the enclosing procedure. The body of this procedure suspends execution for 20 seconds and then sets Finished to True , which will result in Outputter 's termination. The Tasking_Example procedure will not return until Outputter has terminated.
Ada 95 and beyond
The Ada 95 standard contains features for interfacing with native (non-Ada) code and data. An Ada program can import subprograms or global data from other languages, and Ada subprograms and global data can be exported to foreign programs as well.
Ben Brosgol is a senior member of the technical staff at Ada Core Technologies. He has a PhD in applied mathematics from Harvard University. Contact him at .