Gavin, a graduate of Cambridge University, is a software engineer who specializes in real-time systems. He can be contacted at [email protected]
When I first used it almost a decade ago, Ada was large, unfamiliar, and difficult to use. About that time, C++ was looming over the horizon and looked like a much easier language to adopt as a step beyond C. What has been happening with the two languages recently, however, has made me think again -- C++ is growing more complex, while Ada has been rejuvenated to form Ada 95. Both languages have taken good ideas from other languages (including each other) and are fairly comparable. (For a detailed comparison of C++, Ada, and other languages, see "Safe Programming with Modula-3," by Sam Harbison, DDJ, October 1992.)
Of course, no matter how good a language is, you still need decent tools to support it. The GNU New York University Ada Translator (GNAT), a high-quality, low-cost Ada 95 (and Ada 83) compiler, is just such a tool. GNAT supports a number of environments (more or less anything for which GNU tools are available), including DOS, Windows, and various flavors of UNIX. GNAT is freely available for download (http://www. gnat.com/), but you will get better support from the Ada Core Technologies team and newer versions of the tools if you pay.
The GNAT variant I use most is for DOS, a port based on DJGPP, which provides a 32-bit C/C++ (and now Ada) toolset to run on any PC with a 386 or above. In this article, I will describe how to use GNAT. All of the code I present is 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.
DOS installation of GNAT is trivial: You unzip the five downloaded files (four, if you already have DJGPP), set the DJGPP environment variable, add the bin directory to your path, and the job is complete. There are, however, a few other things you might want, including the DJGPP binary file compressor (GNU executable files tend to be a bit on the bulky side even with symbols stripped, and DJGPP shrinks them quite a lot), info (GNU documentation), viewer, file utilities, and maybe even emacs (which has a passable Ada mode) -- all of which can be found at the DJGPP download sites. Since my development environment is Windows 3.1, I prefer to have access to emacs, a build shell, and a few test shells. There are a few Ada integrated development environments for DOS and Windows that may be beneficial if you are familiar with the Turbo C IDE; see the GNAT download sites for these.
The GNU tools make use of intermediate files to pass data between compilation phases rather than internal memory storage. It is worth allocating some memory as a RAM disk, 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 simple: If the main body of the program is in a file called, say, "plottest.adb," all you need to do to build it is run the command gnatmake plottest.
The gnatmake utility understands Ada package dependencies and performs the minimum steps necessary to compile all relevant source code and link it together; it includes a sort of implicit "make" dependency. 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 a standard make file is better, as is shown in Listing One: (a)You have to be clever 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; 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 One: (b) 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 One: (c), 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 complex real-time systems, and I am starting to apply Ada to such systems. For this article, I've extracted short but meaningful examples of code from my work. (The complete code is available electronically; see "Availability," page 3. The downloadable files also include some examples of C code equivalent to the Ada that is presented here.)
Drawing On the Screen
The first example I'll present is code to draw sprites on the PC screen (this code is inspired by Michael Abrash's DDJ "Graphics Programming" columns in the early '90s.) For brevity, I have not bothered to make the code very efficient, and I've left out niceties like clipping and masking.
Drawing the sprite on the screen is straightforward: Just iterate over the rows and columns, copying the sprite pixel to the screen location. The hard part is accessing the graphics mode screen. 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. Listing Two 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 in Ada style, including the removal of illegal initial double underscores in some cases. Example 1 shows all that is necessary to perform the INT 10h to change video modes. If you roll your own variants of the DJGPP DPMI access functions (in C or assembler, for example), you could easily use this same code under the Windows 95 port of GNAT.
For simplicity, my sprite definition is merely a 2D array containing the image data, and the code to copy it to a given position on the screen appears in Listing Three. (I warned you that I was not too interested in efficiency here!) The core is a loop that copies each image byte to the screen, using the DJGPP farnspokeb function (poke a byte to an offset from the FS register); see Example 2.
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 expanded inline in C. However, in the Ada example, it is invoked as an out-of-line function, involving the costly overhead of a function call. (I hope no one ever makes an Ada compiler that can handle C macros.) The Ada 95 standard permits assembly code inserts, and Example 3 shows a simplified syntax for such an insert in GNAT, along with the statement that 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.
Reading the Keyboard
To show how to interact with an assembly interrupt routine, I will present a keyboard handler. Ada includes 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 that cannot be addressed in Ada alone. The main one is that all the memory associated 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 Example 4) uses these extra symbols as dummy variables whose only purpose is to provide addresses to pass to the DPMI memory-locking routines. (That is, 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. So, I have to pick some arbitrary type; in this case, Unsigned_Char.) This code fragment also shows how easy it is to indicate failure via exceptions.
The standard DOS keyboard input system is rather pedestrian, and not at all suitable for fast response. My keyboard handler (available electronically; see "Availability," page 3) is basic and merely sets an entry in a 128-element array to nonzero 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. Example 5 shows the Ada interface to the shared scan code vector. It is imported from assembler and labeled as "volatile" because it is updated asynchronously to the main code. For clarity, since the first element of the vector has a separate purpose, I chose to use an Ada rename declaration to give it a more meaningful name.
To illustrate 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 was 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 multitasking support is not very well developed under DOS. One flaw is that independent tasks tend to monopolize the CPU when they start, so the first to start runs to completion. 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 new and interesting tasking facilities, they are not particularly usable 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 to force a reschedule by inserting delay statements into each task's main loop, but it didn't work.
The only solution I have found is to drop back to the task-synchronization mechanism that Ada 83 supported -- the rendezvous. I introduced 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. Then, when the destination task has completed its operation, the scheduler's default round-robin scheme switches to one of the previously inactive tasks.
The new sprite package is available electronically, with this extra task as well as the drawing routine described earlier. 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, calls the Do_Plot procedure, and suspends itself when Do_Plot 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 from exiting cleanly.
The core code for the multiple-sprite example (also available electronically), shows five separate sprites that randomly trundle around on the screen and one extra task (the main routine body) under keyboard control. 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. Without this entry, there is no guarantee when each task will run (relative to the main body); sprites might be plotted 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 intertask communications -- such as protected types -- but these facilities are of minimal value in an environment that has such restricted tasking support. Nevertheless, the availability of even primitive tasking under DOS is welcome in some situations.
Unless you are lucky enough to have more computing power than common sense, you will be used to squeezing as much as you can out of available hardware. This means that you need to write tight code, and tight code carries a high risk of error. Further, unless you are your own boss, you generally need to produce working code quickly, which exacerbates the danger of error. I favor as much automated language support as I can get, and it is definitely true that Ada provides a lot more safety than, say, C. Ada does not prevent you from writing incorrect code -- it just traps a lot of silly mistakes.
For More Information
GNAT home page (Ada Core Technologies): http://www.gnat.com/
DJGPP home page: http://www.delorie.com/djgpp/
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
Home of the Brave Ada Programmers: http://www.adahome.com/
<b>(a) </b>key_test.exe: key_test.o dosmemor.o keyinput.o keyintr.o gnatbind -x key_test.ali gnatlink key_test.ali keyintr.o </p> dosmemor.o: dosmemor.adb dosmemor.ads gcc -c $< keyinput.o: keyinput.adb keyinput.ads dosmemor.ads gcc -c $< key_test.o: key_test.adb keyinput.ads gcc -c $< keyintr.o: keyintr.s gcc -c $< </p> <b>(b)</b> key_test.exe: *.adb *.ads keyintr.o gnatmake key_test.ali -largs keyintr.o keyintr.o: keyintr.s gcc -c $< </p> <b>(c)</b> key_test.exe: key_test.ali keyintr.o gnatbind -x key_test.ali gnatlink key_test.ali keyintr.o key_test.ali: *.adb *.ads gnatmake -c key_test keyintr.o: keyintr.s gcc -c $<
-- Interface to a subset of the DJGPP specific functions for accessing "DOS"-- memory and DPMI functions. See the DJGPP documentation and C header files -- for more information. </p> with System; with Interfaces.C; use Interfaces.C; </p> package Dos_Memory is type Byte_Regs is record Di: Unsigned_Short; Upper_Di: Unsigned_Short; Si: Unsigned_Short; Upper_Si: Unsigned_Short; Bp: Unsigned_Short; Upper_Bp: Unsigned_Short; Cflag: Unsigned_Long; Bl: Unsigned_Char; Bh: Unsigned_Char; Upper_Bx: Unsigned_Short; Dl: Unsigned_Char; Dh: Unsigned_Char; Upper_Dx: Unsigned_Short; Cl: Unsigned_Char; Ch: Unsigned_Char; Upper_Cx: Unsigned_Short; Al: Unsigned_Char; Ah: Unsigned_Char; Upper_Ax: Unsigned_Short; Flags: Unsigned_Short; end record; pragma Convention( C, Byte_Regs ); type Dpmi_Regs is record Di: Unsigned_Short; Upper_Di: Unsigned_Short; Si: Unsigned_Short; Upper_Si: Unsigned_Short; Bp: Unsigned_Short; Upper_Bp: Unsigned_Short; Cflag: Unsigned_Long; Bl: Unsigned_Char; Bh: Unsigned_Char; Upper_Bx: Unsigned_Short; Dl: Unsigned_Char; Dh: Unsigned_Char; Upper_Dx: Unsigned_Short; Cl: Unsigned_Char; Ch: Unsigned_Char; Upper_Cx: Unsigned_Short; Al: Unsigned_Char; Ah: Unsigned_Char; Upper_Ax: Unsigned_Short; Flags: Unsigned_Short; Es: Unsigned_Short; Ds: Unsigned_Short; Fs: Unsigned_Short; Gs: Unsigned_Short; Ip: Unsigned_Short; Cs: Unsigned_Short; Sp: Unsigned_Short; Ss: Unsigned_Short; end record; pragma Convention( C, Dpmi_Regs ); type Dpmi_Mem_Info is record Handle: Unsigned_Long; Size: Unsigned_Long; Address: Unsigned_Long; end record; pragma Convention( C, Dpmi_Mem_Info ); type Dpmi_Paddr is record Offset32: Unsigned_Long; Selector: Unsigned_Short; end record; pragma Convention( C, Dpmi_Paddr ); type Go32_Info_Block is record Size_Of_This_Structure_In_Bytes: Unsigned_Long; Linear_Address_Of_Primary_Screen: Unsigned_Long; Linear_Address_Of_Secondary_Screen: Unsigned_Long; Linear_Address_Of_Transfer_Buffer: Unsigned_Long; Size_Of_Transfer_Buffer: Unsigned_Long; Pid: Unsigned_Long; Master_Interrupt_Controller_Base: Unsigned_Char; Slave_Interrupt_Controller_Base: Unsigned_Char; Selector_For_Linear_Memory: Unsigned_Short; Linear_Address_Of_Stub_Info_Structure: Unsigned_Long; Linear_Address_Of_Original_Psp: Unsigned_Long; Run_Mode: Unsigned_Short; Run_Mode_Info: Unsigned_Short; end record; pragma Convention( C, Go32_Info_Block ); procedure Move_Data( Source_Sel: in Integer;Source_Offset:in System.Address; Dest_Sel: in Integer; Dest_Offset: in Integer; Size: in Integer ); pragma Import( C, Move_Data, "movedata" ); procedure Int86( Ivec: in Unsigned_Long; Regs_In: in Byte_Regs; Regs_Out: out Byte_Regs ); pragma Import( C, Int86, "int86" ); procedure Dpmi_Int( Vector: in Integer; Regs: in out Dpmi_Regs ); pragma Import( C, Dpmi_Int, "__dpmi_int" ); </p> function Go32_Conventional_Mem_Selector return Unsigned_Short; pragma Import(C, Go32_Conventional_Mem_Selector, "_go32_conventional_mem_selector"); procedure Farsetsel( Selector: in Unsigned_Short ); pragma Import( C, Farsetsel, "_farsetsel" ); </p> procedure Farnspokeb( Offset: in Unsigned_Long; Value: Unsigned_Char ); pragma Import( C, Farnspokeb, "_farnspokeb" ); </p> function Dpmi_Lock_Linear_Region(Info: access Dpmi_Mem_Info) return Integer; pragma Import( C, Dpmi_Lock_Linear_Region, "__dpmi_lock_linear_region" ); </p> function Dpmi_Unlock_Linear_Region(Info:access Dpmi_Mem_Info) return Integer; pragma Import( C, Dpmi_Unlock_Linear_Region, "__dpmi_unlock_linear_region" ); </p> function Dpmi_Set_Protected_Mode_Interrupt_Vector( Vector: in Integer; Address: access Dpmi_Paddr ) return Integer; pragma Import( C, Dpmi_Set_Protected_Mode_Interrupt_Vector, "__dpmi_set_protected_mode_interrupt_vector" ); function Dpmi_Get_Protected_Mode_Interrupt_Vector( Vector: in Integer; Address: access Dpmi_Paddr ) return Integer; pragma Import( C, Dpmi_Get_Protected_Mode_Interrupt_Vector, "__dpmi_get_protected_mode_interrupt_vector" ); procedure Dpmi_Get_Segment_Base_Address( Selector: in Unsigned_Short; Address: out Unsigned_Long ); pragma Import( C, Dpmi_Get_Segment_Base_Address, "__dpmi_get_segment_base_address" ); function My_Cs return Unsigned_Short; pragma Import( C, My_Cs, "_my_cs" ); function My_Ds return Unsigned_Short; pragma Import( C, My_Ds, "_my_ds" ); procedure Out_Port_B( Port: in Unsigned_Short; Data: in Unsigned_Char ); pragma Import( C, Out_Port_B, "outportb" ); </p> -- This is a new routine - sets the far selector to the one I most -- often want, the conventional DOS area. procedure Set_Dos_Selector; pragma Inline( Set_Dos_Selector ); end Dos_Memory;
-- Plot a shape using only Ada (and a few imported C functions)with Interfaces.C; use Interfaces.C; with Dos_Memory; use Dos_Memory; </p> package body Sprite is procedure Plot( X, Y: in Coord; Sp: in Sprite_Data ) is Offset, Local_Offset: Unsigned_Long; begin Offset := Unsigned_Long( 16#A0000# + Y * 320 + X ); Set_Dos_Selector; for Row in Sp'Range loop Local_Offset := Offset; for Col in Sp'Range( 2 ) loop Farnspokeb( Local_Offset, Unsigned_Char( Sp( Row, Col ) ) ); Local_Offset := Local_Offset + 1; end loop; Offset := Offset + 320; end loop; end Plot; end Sprite;
Copyright © 1997, Dr. Dobb's Journal