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.
-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.)
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 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.
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.
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.
| 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):
|
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.
Unsigned_Char). This short code
fragment also shows how easy it is to indicate failure via exceptions.
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.
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.
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.
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.
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.
[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.
Last modified on 9th April 2000