GNAT's whisker?

Gavin Smyth

This is an article which appeared in Dr Dobb's Journal in December 1997, under the title of 'GNAT: the GNU New York Ada Translator.' You can also find it on the DDJ CD, a very useful programming resource.

The code associated with the article is also available.

In the following March's DDJ, there was a letter from Jerry van Dijk about a free DOS graphics package, vgapck06.zip, which can be found in the ez2load directory on the GNAT distribution mirrors. He also mentioned an SVGA version and a Windows version. (I haven't checked any of these out.)


I first used Ada almost a decade ago and found it to be a very large, unfamiliar and difficult to use language. About that time, C++ was coming over the horizon and looked like a much easier language to adopt as a step beyond C. However, what has been happening with the two languages recently has made me think again: C++ is growing more complex while Ada has been rejuvenated to form Ada 95. Both languages have taken on board good ideas from other languages (including each other) and I feel are now fairly comparable - [1] contains a table comparing a number of languages, including C++ and Ada. For me, Ada has become my general language of choice.

Of course, all this is well and good, but there is not much one can do with any language without access to a decent set of tools to support it, and respectable Ada compilers are expensive, right? Wrong! A major factor which rekindled my interest in Ada was the GNU NYU (New York University) Ada Translator, otherwise known as GNAT. This is a high quality, low cost Ada 95 (and Ada 83, if you want it) compiler with support for a large number of environments - more or less anything for which GNU tools are available, including DOS, Windows, and various flavours of unix. GNAT is freely available for download (see Internet links) but you will get better support from the GNAT team - Ada Core Technologies - and newer versions of the tools if you pay.

The variant of GNAT I use most is the DOS one. This port is based on the excellent DJGPP ([2], [3]) which provides a very good 32-bit C/C++, and now Ada, toolset to run on any PC with a 386 or above. I will describe how to use GNAT, and specifically the DOS port, within this article. All of the code presented here is completely legal Ada (and assembler) for any of the GNAT ports, but some of it does make use of DJGPP specific functions and therefore can only be linked with the DOS toolset.

This article is not intended to be an introduction to Ada in general - see some of the Internet links for tutorials and reference material.

Debunking some criticisms of Ada

Programmers often discount Ada because it is perceived to be a large language, but what is meant by large here? The formal language definition is large (500 or so pages) but that does include areas considered to be standard libraries which are normally taken outside the scope of some other language definitions. Additionally, think of the number of rules of thumb you need as a C++ programmer (such as: any class from which others may be derived ought to have a virtual destructor) - many of these are enforced by the language with Ada. Another view of size is code efficiency: by default GNAT includes a lot of extra error checking at run time, such as array bounds limits, or sub-routine parameter range checking. It is possible to disable all of the checks for speed, and passing options -gnatp -O3 should generate the fastest executable code. Ada can perform more sophisticated compile time analysis than a C/C++ compiler, and may result in faster equivalent code.

Is Ada a difficult language? Well, many of us cut our teeth on C, and C++ looks like it is easier to pick up than Ada. However, coming at either language from scratch may show much less of a difference.

Another belief is that Ada tools are expensive - as I have already mentioned, GNAT can be downloaded for free.

Until recently, Ada lacked quite a lot of features that were essential for modern, efficient, programming. Ada 95 has corrected a lot of these deficiencies - for example, Ada now has object oriented features and much better access to low level features, including access to other languages.

The one area where C++ has an obvious advantage over Ada is the availability of a very large number of external libraries. Ada has far fewer bindings, though this is in part because the language includes some facilities, such as tasking, which appear as libraries under C++. (Maybe that in itself is a disadvantage because the C/C++ libraries can be tuned to the operating environment, whereas the portable Ada scheme will not be able to exploit some specific environments efficiently.)

Ada programming

I will present a number of Ada code fragments to show some of what I consider to be the most significant features of the language. [4] covers object orientation in detail, so I will concentrate instead on tasking and interfacing with other code written in other languages.

But first, how does one use GNAT? DOS installation is trivial: unzip the five (or four, if you already have DJGPP) downloaded files, set the DJGPP environment variable, and add the bin directory to your path, and the job is complete. There are, however, a few other things you might want - the DJGPP binary file compressor (GNU executable files tend to be a bit on the bulky side even with symbols stripped, and DJP shrinks them quite a lot), info (GNU documentation) viewer and GNAT documentation, file utilities, and maybe even emacs (which has a passable Ada mode) - all of these can be found at the DJGPP download sites. My development environment is Windows 3.1, so that I have convenient access to emacs, a build shell and a few test shells. There are a few Ada integrated development environments for DOS and Windows which may be of benefit to people familiar with the Turbo C IDE - see the GNAT download sites for these.

The GNU tools make quite a lot of use of intermediate files to pass data between compilation phases rather than internal memory storage. It is worth allocating some memory as a RAM disc, even on a rather limited memory system, to speed up GNAT and DJGPP builds.

If a program is written entirely in Ada, building it is simplicity itself - if the main body of the program is in a file called, say, plottest.adb, to build it all you need to do is run the single command: gnatmake plottest.

The gnatmake utility understands Ada package dependencies and performs the minimum of steps necessary to compile all relevant source and link it together - it includes a sort of implicit make depend. However, if you have some program components written in a different language, you need to give some hints as gnatmake will not know how to build the foreign object files to be linked in. You could rely on a BAT file, but better is a standard make file, such as is shown in Listing 1a. It is tricky to determine all the dependencies - in general, if an Ada file "withs" any other, you need to specify the latter's specification file (.ads) as a dependency and if it contains inline functions, you need to specify the body file (.adb) as a dependency too. There is a way to cheat - invoke gnatmake to build everything but the foreign object file - Listing 1b is an attempt at such a make file, but unfortunately it does not work: gnatmake does not treat keyintr.o as a dependency although make does. The solution is the slightly expanded Listing 1c, where the compilation and bind/link stages are explicitly separated with standard make responsible for the executable file dependencies.

Most of my work is with real time systems, and I am starting to apply Ada to such systems. Because these tend to be complex and therefore make it difficult to extract short meaningful examples for this article, I have written a few snippets of code that would not look out of place in a PC action game. (The complete code is available electronically - the printed listings include only pertinent files, and some of the more boring and repetitive material has been omitted. The downloadable files also include some examples of C code equivalent to the Ada presented here. )

Drawing on the screen

The first example is a short piece of code to draw sprites on the PC screen, inspired by the Abrash columns in DDJ in the late 1980s/early 1990s. For brevity, and since I merely want to illustrate the connections with other languages, I have not bothered to make the code very efficient, and have left out niceties like clipping and masking.

Drawing the sprite on the screen is quite straightforward - just iterate over the rows and columns, copying the sprite pixel to the screen location. The tricky bit is accessing the graphics mode screen in the first place. In DOS, an INT 10h invokes video operations, but performing a software interrupt is a little tricky. Fortunately, DJGPP provides a DPMI interface to this sort of thing, and Listing 2 shows my Dos_Memory package specification, which provides an Ada interface to a small subset of the DPMI functionality. The bulk of this file is a set of type definitions and subroutine declarations, with import pragmas attaching the declarations to existing C functions - note how some functions are renamed into "Ada style," including the removal of illegal initial double underscores in some cases. Figure 1 shows all that is now necessary to perform the INT 10h to change video modes. If you rolled your own variants of the DJGPP DPMI access functions (say, in C or assembler), you could easily use this same code under the Windows 95 port of GNAT.

Figure 1 - Generating an INT 10h to change video mode

declare
  Regs: Dpmi_Regs;
begin
  Regs.Ah := 0;
  Regs.Al := Unsigned_Char( Mode );
  Dpmi_Int( 16#10#, Regs );
end;

For simplicity, my sprite definition is merely a two dimensional array containing the image data, and the code to copy it to a given position on the screen appears in Listing 3 - I did warn you that I was not too interested in efficiency here! The core of this is a loop copying each image byte to the screen, using the DJGPP farnspokeb function (poke a byte to an offset from the FS register), shown in Figure 2.

Figure 2 - Use of farnspokeb

Farnspokeb( A_Long_Offset, A_Byte );

If you look at the definition of farnspokeb in the farptr.h DJGPP header file, you will see it is a single machine code instruction which is expanded inline in C. However, in the Ada example above, it is invoked as an out of line function (I hope no-one ever makes an Ada compiler that can handle C macros!), involving the costly overhead of a function call. The Ada 95 standard permits assembly code inserts and Figure 3 shows a simplified syntax for such an insert in GNAT, along with the statement which replaces the call to farnspokeb in the middle of the sprite plotting loop. As you can see, the syntax is not particularly pleasant, but inserting assembly statements is possible. If you compile sprite.adb with the -S option, you will see the intermediate assembly code to confirm that the instruction has been expanded as expected. See the compiler info pages for precise details of the operand definition strings.

Figure 3 - Assembler inserts
Syntax for an assembler insert

Asm( Template : String;                    -- Assembly instruction
     Outputs  : Asm_Output_Operand_List;   -- Output operands
     Inputs   : Asm_Input_Operand_List;    -- Input operands
     Clobber  : String  := "";             -- Other touched registers
     Volatile : Boolean := False);

where the operand lists are both lists of associations of expressions to assembler operands, of the form (for input):

Type'Asm_input( Operand: String; Expression: Type )

Inline assembler replacement for farnspokeb

Asm( "movb %b1,%%fs:(%k0)",
     No_Output_Operands,
     ( Unsigned_Long'Asm_Input( "qi", Local_Offset ),
       Pixel'Asm_Input( "r", Sp( Row, Col ) ) ) );

To summarise, this particular example of sprite copying is not at all efficient, but at least I have shown how it is possible to embed assembler directly in Ada source, and along the way how easy it is to invoke C routines from Ada.

Reading the keyboard

To show how to interact with a assembly interrupt routine, I will use a keyboard handler as an example. Now, Ada does include some support for interrupt handling (though it is somewhat underdeveloped in the DOS port of GNAT), but there are a few essentials for DJGPP interrupt routines which cannot be addressed in Ada alone, the main one being that all the memory associate with an interrupt routine must be locked in physical memory to ensure that a page fault cannot occur during the processing of the interrupt. The easiest way to achieve this is to write the interrupt handler in assembler and add a few extra symbols to delimit the handler code and data. The file keyinput.adb (partly shown in Figure 4) uses these extra symbols as dummy variables whose only purpose is to provide addresses to pass to the DPMI memory locking routines (i.e., they are never dereferenced, so their type is irrelevant: in GNU C I can get away with declaring them as type void, but in Ada, I cannot define them as such and so have to just pick some arbitrary type - in this case, Unsigned_Char). This short code fragment also shows how easy it is to indicate failure via exceptions.

Figure 4 - Locking the keyboard interrupt code area

Dpmi_Error: exception;  -- Thrown if there is a system problem

Start_Of_Locked_Code, End_Of_Locked_Code: Unsigned_Char;
pragma Import( Asm, Start_Of_Locked_Code, "start_of_locked_code" );
pragma Import( Asm, End_Of_Locked_Code, "end_of_locked_code" );

Code: aliased Dpmi_Mem_Info;

...

Dpmi_Get_Segment_Base_Address( My_Cs, Code.Address );
Code.Address := Code.Address + To_Long( Start_Of_Locked_Code'Address );
Code.Size := To_Long( End_Of_Locked_Code'Address ) -
             To_Long( Start_Of_Locked_Code'Address );
if( Dpmi_Lock_Linear_Region( Code'access ) /= 0 ) then
  raise Dpmi_Error;
end if;

The standard DOS keyboard input system is rather pedestrian, and not at all suitable for fast response. My keyboard handler (Listing 4) is quite basic (for example, it does not deal with extended keys) and merely sets an entry in a 128 element array to non-zero when the key with that scan code is pressed, returning it to zero when the key is released. Because there is no key with scan code zero, I reuse that location to store the scan code of the last key pressed. Figure 5 shows the Ada interface to the shared scan code vector - imported from assembler, and labelled as volatile because it is updated asynchronously to the main code. For clarity, since the first element of the vector does really have a separate purpose, I chose to use an Ada rename declaration to give it a more meaningful name.

Figure 5 - Interfacing to the keyboard handler common variables

Keys_Pressed: array( Scan_Code ) of Scan_Code;
pragma Import( Asm, Keys_Pressed, "keys" );
pragma Volatile( Keys_Pressed );

Most_Recent_Key: Scan_Code renames Keys_Pressed( 0 );

This example illustrates the ease with which foreign variables, like foreign routines, can be used.

Multiple tasks

As my final example area, multitasking, I am going to place a number of independent sprites on the screen, each having its own controlling task. The body of each task started off being rather straightforward, merely looping on the state of a volatile variable, updating screen coordinates and plotting the sprite. However, this does not work as expected under DOS - the Ada tasking support is not very well developed under this operating system. One flaw is that the independent tasks tend to monopolise the CPU when they start, so the first to start runs to completion, and since completion in this case is to be signalled by another task setting the control variable, the running task never stops.

Although Ada 95 has a lot of new and interesting tasking facilities, they are not particularly useable under DOS, unless you can find some way to force a reschedule occasionally, or unless each task running to completion is acceptable. (Of course, there is no such problem under a more sophisticated operating system, such as Windows 95 or Linux.) I had hoped that inserting delay statements into each task's main loop would force a task reschedule, but unfortunately this is not the case. The only solution I have found is to drop back to the task synchronisation mechanism that Ada 83 supported, the rendezvous. What I had to do was introduce an extra task with which every other task would rendezvous - because all the tasks in this example draw sprites, I chose to insert this extra task into the plotting system. A rendezvous will cause the scheduler to switch from the requesting task to the destination task, and when the latter has completed its operation, the scheduler's default round robin scheme switches to one of the previously inactive tasks.

Listing 5 shows the new sprite package, with the extra task as well as the drawing routine described earlier. As can be seen, the definition of a task is fairly trivial, and the rendezvous mechanism is embodied in the select statement - this indicates that the Plotter task waits until some other task invokes its Plot entry point; then it calls the Do_Plot procedure; finally suspending itself what that completes. The or terminate clause is a simple way to ensure that the task ceases when there is nothing to invoke its entry point - otherwise the task would remain in existence permanently and prevent the program as a whole exiting cleanly.

Listing 6 contains the core code for the multiple sprite example - this shows five separate sprites which randomly trundle round the screen, and one extra task (the main routine body) which is under keyboard control, bringing together all the code from earlier in this article. As you can see, each of the random sprite tasks depends on the volatile Alive variable for loop control. Additionally, each task has a Start_Drawing entry, on which it waits before entering the main loop: this is necessary because without it there is no guarantee when each task will run relative to the main body, possibly even starting to plot sprites before the screen has been switched to a graphics video mode. Finally, as mentioned earlier, every task, including the main body, plots sprites via the plot task's rendezvous to force periodic rescheduling under DOS.

This example does not show off Ada 95's other methods of inter task communications, such as protected types, but these facilities are of minimal value in an environment which has such restricted tasking support. Nevertheless, the availability of even primitive tasking under DOS is quite welcome in some situations.

Conclusion

Unless you are lucky enough to have more compute power than commonsense, you will be used to squeezing as much as you can out of available hardware. This means that you need to write efficient code, and writing efficient code tends to carry a higher risk of accidental error. Further, unless you are your own boss, you generally need to produce working code quickly, which exacerbates the danger of error. I favour as much automated language support as I can get, and it is definitely true that Ada provides a lot more safety than, say, C - both at build time (with stronger typing) and at run time (with range checking), so I feel that it is a "better" language to work in. Before any sort of language war is breaks out, Ada does not prevent you from writing incorrect code - it just traps a lot of silly mistakes; and there are times when only C will do (such as interfacing with subsystems which have no Ada binding).

Many people consider Ada to be a bulky, inefficient language lacking in support for modern programming. In [4], David Moore covers Ada 95's object oriented facilities, and here I hope I have shown some of the other features which help in real time systems. Another common reason for not using Ada is that compilers are expensive - GNAT eliminates that argument. Now there is no excuse for not exploring this rich language.

Internet links

GNAT home page (Ada Core Technologies): http://www.gnat.com. This includes pointers to download sites (which also contain electronic forms of the language reference manual) and information on compiler support.

DJGPP home page: http://www.delorie.com/djgpp. The DJGPP package itself may be downloaded from any Simtel.Net mirror.

Lovelace, an interactive Ada tutorial: http://www.adahome.com/Tutorials/Lovelace/lovelace.html.

GNAT and other Ada compilers feature quite a lot in the newsgroup comp.lang.ada.

Finally, the Home of the Brave Ada Programmers is a comprehensive source of useful information: http://www.adahome.com.

References

[1] "Safe programming with Modula-3" S. Harbison, DDJ, October 1992.

[2] "Go-faster sprites" G. Smyth, EXE 11(3), August 1996.

[3] "C programming" A. Stevens, DDJ, August 1995.

[4] "Object-oriented facilities in Ada 95" D. Moore, DDJ, October 95.


Gavin's home page | BeesKnees home page

Last modified on 9th April 2000