Classic Computer Magazine Archive START VOL. 2 NO. 1 / SUMMER 1987

PLUMBING GEM'S MYSTERIES

WRITING TO THE DESK ACCESSORY PIPELINE

by Tom Hudson


Pipes--the final frontier. Contributing Editor Tom Hudson shows how you can get at GEM's internal communications plumbing without using a monkey wrench or calling Roto-Rooter. Tom's example program, a desk accessory for DEGAS Elite, is on your START disk.

File DECOMM.ARC on your START disk

Until recently, I've never had a lot of use for desk accessories. Like most people, I saw them as little programs that allowed you to change the default screen colors, install your printer, set the system clock or do other simple tasks. I usually didn't bother using accessories much, because they took up RAM that I needed for program development. Accessories such as Thunder!, from Batteries Included, show that accessories can do much more than just sit there using memory. They can really perform a useful function in conjunction with the primary application.

While developing DEGAS Elite in the summer of 1986, I was perusing the GEM AES (Application Environment Services) manual and happened to notice several interesting-looking AES functions that I had never used before.

The first function, appl_find(), allows a GEM application (either the main application or a desk accessory) to locate another application or desk accessory and obtain its ap_id, or "application identifier," a unique number that is assigned to each accessory or application currently in memory. "That's nice," I said skeptically, "but what good is that?" Then I read it: The application must know theap_id before it can establish communications with another application!What's this? You mean programs can talk to each other?

My question was answered almost immediately, as I moved on to the other two communication functions, appl_read() and appl_write(). These two functions have proven to be a virtual goldmine, and in this article I will demonstrate how to use them to expand your program functions without having to change the main program.


THE PIPELINE

At the core of the GEM program functions is a simple message system known as the message pipe. If you've ever written a GEM application, you'll be familiar with this system. By using the evnt_ mesag() or evnt_multi() functions, your program waits until GEM sends it a message telling it that the user has taken some action the program must respond to. GEM has a simple set of predefined messages for such functions as menu bar item selection, setting window control points, moving or sizing windows, and so on. Each of these messages is set to a standard length of 16 bytes.

The interesting thing is, your programs can easily send messages to another application in the system, or even to itself, impersonating the GEM event manager! The standard message is 16 bytes in length, but user-defined messages can be larger, if desired. This process is actually quite simple but there are a few guidelines that should be followed, and a few problems you should watch out for. If properly done, communication pipe inter-application communications is a reliable way for programs to exchange information.


LET'S GET TALKING!

The standard GEM message is a 16-byte chunk of data, organized into eight words, as follows:

WORD 0: Message number
WORD 1: The ap_id of sending application
WORD 2: Additional bytes in message (if more than 16 bytes)
WORD 3: Message-specific
WORD 4: Message-specific
WORD 5: Message-specific
WORD 6: Message-specific
WORD 7: Message-specific

Word number zero, the message number, must be a unique value ranging from $0000 to $FFFF. Messages 0-41 are used by GEM, so if you're going to define your own messages, be sure to use a number other than those reserved for GEM, or you might confuse the receiving application!

Word 1 is the ID of the application that's sending the message. When sending messages, it is important to place the proper ap_id in this location. Without a proper ap_id here, the application that receives the message won't know where it came from, and if it tries to respond by sending a message back, GEM may get confused. The moral of the story: Always know your application's ap_id. Fortunately, you can always find your program's ap_id in the global variable gl_apid. In C, you can define this variable as follows:

extern int gl_apid;

When sending a message, just move gl_apid to word 1 of the message.

Word 2 in the message is the extra number of bytes in the message. For a standard 16-byte message, this value is set to 0. For a 20-byte message, the value here would be 4 (20-16). The minimum message size is 16 bytes.

The remaining five words in the standard message are available for data. When designing messages for your programs' use, you can fit up to five pieces of word-sized data in a standard message. If you need to pass more data, you can do so by specifying the extra number of bytes in word 2, and sending the extra bytes through the pipe with the appl_write() function. The receiving application can get the extra bytes from the pipe by using the appl_read() function. For extremely large blocks of data, pointers to the data may be passed in the standard message.

Now that we know what the message format looks like, let's see what is necessary to send it.


ADDRESS UNKNOWN?

In order to send a message to another application or accessory, it is vital that you know the ap_id of that program. Without the proper ap_id of the receiving application, it's like dropping a letter into a mailbox without the name or address of the person you're sending it to. It just won't get there.

The AES function appl_find() will allow you to locate the application you would like to communicate with, and will give you the proper ap_id. The format of the function is:

target_id=appl_find("APPLNAME");

"APPLNAME" is a null-terminated string specifying the filename of the application you want to find and target_id is the variable that holds the target application's ap_id. The program name string is eight characters in length plus the terminating null character (0), and is simply the main portion of the application's filename. If you were looking for a program called RATTFINK.PRG, you would use: appl_find("RATTFINK"). Similarly, if you're looking for CLOCKSET.ACC, you would request: appl_find("CLOCKSET"). If the filename is less than eight characters, such as SHORTPRG, you must pad the remainder with spaces:

target_id = appl_find("SHORT ");

The ap_id of the target application will be returned in target_id. If the specified application name was not found, GEM will return a -1 in target_id. One word of special caution here: If the target application is a program and was the last program that was executed, a bug in the current ST ROMs will return a seemingly valid target_id, while the application is not resident at all! If the program (usually an accessory) tries to communicate with the target application and expects a reply, it will have a long time to wait, because the application isn't there! Well see how to cope with this later.

Once you know the ap_id of the target application, you're ready to start talking to it. Simply build a message in an eight-word array, like so:

message[0] = 0x4000;
message[1] = gl_apid;
message[2] = 0;
message[3] = 1234;
message[4] = 0;
message[5] = 0;
message[6] = 0;
message[7] = 0;

In this example message, we're sending a message number of $4000 (safe because GEM only uses message numbers 0-41). We have placed the ap_id of our application (gl_apid) in message[1]. The message is 16 bytes long, the standard length. Note that message[2] is set to zero, indicating that there are no extra message bytes. The message in this case is a simple numeric value, 1234, in message[3].

Okay, we've built our message buffer, now let's send it. For this function, we'll use the appl_write() function. This function has three parameters, as shown below:

success= appl_write(target_id, msg_size,msg_buff);

The target_id parameter is a word that tells GEM the ap_id of the application we want to send the message to. The msg_size parameter is a word that tells how many bytes there are in the message and msg_buff is a pointer to the message buffer array. The success variable is set to zero if an error occurred, or a positive value if the write was successful. Let's say we want to send the above message to a desk accessory called TESTER.ACC. The following code would get the job done:

target_id = appl_find("TESTER ");
if(target_id>=0)
success = appl_write(target_id,16,message);

Simple enough? Sure. However, we still don't know how to make the TESTER.ACC program look for and acknowledge this message.


THE RECEIVING END

Normally, a typical GEM application or desk accessory will sit around waiting for a message to come into its message pipe. This is accomplished by using the evnt_mesag() or evnt_multi() functions. For simplicity, we'll use the evnt_mesag function, which is the more straightforward of the two.

Somewhere in our TESTER.ACC program, there will be an evnt_mesag() function call, looking something like this:

evnt_mesag(mess_in);

Usually, an inactive accessory will only receive one of two messages from the GEM event manager: 40 (AC_OPEN) or 41 (AC_CLOSE). We can make it recognize our custom message merely by testing the message number of the incoming message for our special code! For example, to have the accessory look for the $4000 message we sent earlier, this is the code we would need:

int mess_in[81;
.
.
.
evnt_mesag(mess_in);
if(mess_in[0] = = 0x4000)
{
(WE GOT THE MESSAGE!)
(PRINT OUT mess_in[3])
}

It's that simple! No muss, no fuss. The 16-byte message buffer (mess_in) will be filled with the 16-byte message that was sent by the other application, and it's ready to be used by the accessory.


TWO-WAY MESSAGES

In many cases, you'll want to have one application send a message to another application, requesting specific information. For example, a desk accessory may need to know the address of a data buffer within an application. This type of message requires two-way communication, which is a little more tricky.

When I design a two-way message, I usually set it up so that message numbers with a low byte of $00-7F are requests, and message numbers with a low byte of $80-FF are replies. For example, a request for information might be message number $4000, and the reply for this message is $4080. The reply is always the message number with $80 added to it. This makes for a uniform message system, and prevents a program from being confused by a different message.

Let's change our above example so that the application is requesting information from the accessory. The application will send a message to the accessory, which will in turn send a reply back to the application program. The application program, after sending the message to the accessory, will wait for a reply from the accessory before proceeding.

The application code might look like:

/* SEND MESSAGE TO ACCESSORY */
target_id = appl_find("TESTER " );
if(target_id>= 0)
{
message[01 =0x4000;
message[1] =gl_apid;
message[2] =0;
message[3] =1;
message[4] =message[5] =message[6]=message[7] =0;
appl_write(target_id,16,message);
/* WAIT FOR REPLY */
do
{
evnt_mesag(message);
}
while message[0] != 0x4080;
}
/* REPLY RECEIVED, CONTINUE PROCESSING */

As you can see, the application writes the message normally, then goes into a loop that waits for an evnt_mesag with a message number of $4080, the reply to message number $4000. As long as the target accessory is in memory, everything should work perfectly. If messages other than number $4080 are received by the application, it ignores them and continues waiting. (Note: It may be necessary, in certain rare instances, to process other messages received by this loop, such as redraw messages. This is usually not the case.)

The accessory code that would process this message is as follows:

int mess_out[8];
.
.
.
/* WAIT FOR MESSAGE FROM THE PIPE */
msg_wait:
evnt_mesag(mess_in);
if(mess_in[0] = = 0x4000)
{
mess_out[0] =0x4080;
mess_out[1] =gl_apid;
mess_out[2] =0;
mess_out[3] =1;
mess_out[4] =2;
mess_out[5] =3;
mess_out[6] =4;
mess_out[7] =5;
appl_wnte(mess_in[1],16,mess_out);
}
goto msg_wait;

When the accessory code receives message number $4000, it just builds a reply message with the proper reply message number ($4080), places its ap_id in mess_out[1], and builds the rest of the message accordingly. It then writes the message to the message pipe using the appl_write() function. Note that it used the value that it received in mess_in[1] as the ap_id of the application that it is sending to. This is extremely important! If the original sender does not supply the proper ap_id in its message, the receiving application/accessory cannot successfully reply. Don't forget to place the gl_apid into the second word of the message buffer!

To summarize this sequence of communications operations:

1. Program #1 gets the ID of the program it wants to communicate with;
2. Program #1 builds and sends the message;
3. Program #1 enters evnt_mesag() wait;
4. Program #2 receives the message;
5. Program #2 composes reply message, sends to original sender;
6. Program #2 returns to message wait state;
7. Program #1 receives reply from program #2, continues processing.

This is a fascinating process, and can be extremely useful. We'll see a real-world application of the technique in a moment. In my latest application of the communication pipe, CAD-3D 2.0, the main program uses desk accessories to perform optional peripheral operations, such as animation scripting, animation recording, and graphics antialiasing (smoothing of jagged lines). Each of these accessories is independent, and in some cases the communication path can become quite complicated, with processing often passing from accessory to the main program to a second accessory and back! To actually watch it happen is amazing.

To me, though the real power lies in being able to build more complex programs out of smaller pieces, or modules. These modules can be designed to be periodically upgraded to newer versions that do a more efficient job, without having to change the main application. Further, an extensive communication protocol can be built into the main program, allowing it to be controlled remotely by a desk accessory. This possibility becomes more practical as newer ST computers, with more memory, are introduced. Theoretically, a desk accessory could be as large or larger than the original program, and they could work together to perform more advanced functions.


SOME WORDS OF CAUTION

The successful use of the communication pipe with customized messages depends on a well-defined communication protocol. If possible, send the entire message in the standard message length of 16 bytes. Use pointers if you're sending large blocks of data. If your application will be sending a large number of messages quickly, one right after another, use the two-way request/reply system shown above to avoid overflowing the pipe. The receiving application may have to do a bit of processing for each message--play it safe.

As I mentioned earlier, it is possible for a desk accessory to request the ap_id of an application and get a valid id, even though the application has terminated. If this happens, the accessory may send a message to the application and sit there forever, awaiting a reply that will never come. You don't want that to happen (take it from someone who knows from experience). Here's a good solution to this dilemma.

Send an information request to the application (one that requests information only and does not command the application to take action) and then call the evnt_multi() function waiting for either a message or a timer event. Set the timer length to 2000 milliseconds. If you get the reply from the application before the timer event occurs, the application is really present and you can proceed. Do this every time the accessory is activated from the desktop DESK dropdown. It is not necessary if the application has called the accessory. This evnt_multi may look something like this:

checlk_it:
event=evnt_muiti(0x0030,-1,-1,-1,0,0,0,0,0,0,0,0,0,0,mgbuf,2000,0,&dum,&dum,&dum,&dum,&dum,&dum);
if(event & 0x0020)
(APPLICATION NOT PRESENT)
else
if(mgbuf[0]]==REPLY)
(APPLICATION PRESENT)
else
goto check_it;

This is a reliable test to be sure the application is really there. If the application was previously run but is not currently running, the timer event of 2000 milliseconds (2 seconds) will expire before a reply message is received.


REAL WORLD EXAMPLES

The file DECOMM.C on your START disk is a real-world example of the communication pipe. This is the source code for a desk accessory that "talks" to DEGAS Elite and gets information on the screen being used, the screen buffer locations, block size and more. If you've seen the TINYLOAD.ACC accessory for DEGAS Elite, you've seen another example of the technique in use. This is a very practical use of the communication pipe. The DEGAS Elite program was getting so large that it barely fit into 512K systems, so I decided to make the program expandable through desk accessories. The accessories can ask Elite where its screens are and which screen is in use, then load data into the screens and change the palette. Let's see how it works.

DEGAS Elite supports five communication pipe commands, numbered $DE00 (DE-Degas Elite-get it?) through $DE04. These commands are listed below.

Screen Buffers.

$DE00-Request screen addresses. This command simply asks DEGAS Elite to tell the accessory where the screen buffers are located. The message is formatted as follows:

message[0]= $DE00
message[1] = Accessory ap_id
message[2] = 0
message[3] = Unused
message[4] = Unused
message[5] = Unused
message[6] = Unused
message[7] = Unused

DEGAS Elite will respond with message $DE80, the reply for $DE00. This message is formatted as follows:

message[0] = $DE80
message[1] = DEGAS Elite ap_id
message[2] = 0
message[3] = Low word of pointer to screen buffer pointer array
message[4] = High word of pointer to screen buffer pointer array
message[5] = Number of pointers in array
message[6] = Unused
message[7] = Unused

This reply message is fairly straightforward. Message words 3 and 4, when put together, form a pointer to an array of pointers that point to the various screen buffers within DEGAS Elite. Message word 5 tells how many pointers are contained in the array. The array currently contains 13 pointers, allocated as follows:

array[0] = Pointer to main menu screen (don't alter!)
array[1] = Pointer to drawing screen #1
array[2] = Pointer to drawing screen #2
array[3] = Pointer to block buffer
array[4] = Pointer to Block mask buffer
array[5] = Pointer to Block work buffer
array[6] = Pointer to Undo buffer
array[7] = Pointer to drawing screen #3 (0 if not present)
array[8] = Pointer to drawing screen #4 (0 if not present)
array[9] = Pointer to drawing screen #5 (0 if not present)
array[10] = Pointer to drawing screen #6 (0 if not present)
array[11] = Pointer to drawing screen #7 (0 if not present)
array[12] = Pointer to drawing screen #8 (0 if not present)

Future versions of DEGAS Elite may support more than 8 drawing screens, so be sure to look at message[5] for the number of pointers. As noted above, if the screen pointer is zero, the screen buffer was not allocated. Do not use these buffers. Two buffers may be used by the accessory for "scratch" buffers. These are array[5] and array[6]. The array [5] buffer is a temporary work area used when drawing with a block image, and the array[6] buffer is the Buffer used to hold the image temporarily for the Undo key. These are discarded when you return to the menu screen, and so are available for use by your accessory. Note that they may not be aligned for proper display use--use them only for data buffers or work areas, not display screens.

If you want to check to see if DEGAS Elite is really there (to circumvent the appl_find GEM bug), $DE00 is a good message to use, as the reply is sent back immediately.

Current Screen.

Since DEGAS Elite has up to eight drawing screens available, your accessory may need to know which one is currently in use by the user. The next command, $DE01, will request the screen number. This message is formatted as follows:

message[0]= $DE01
message[1] = Accessory ap_id
message[2] = 0
message[3] = Unused
message[4] = Unused
message[5] = Unused
message[6] = Unused
message[7] = Unused

DEGAS Elite will respond with message number $DE81, formatted as follows:

message[0] = $DE81
message[1] = DEGAS Elite ap_id
message[2] = 0
message[3] = Screen buffer index
message[4] = Unused
message[5] = Unused
message[6] = Unused
message[7] = Unused

The screen buffer index is an index into the screen buffer array. If the index is 1, drawing screen #1 is in use. If the index is 11, drawing screen 7 is in use. Be sure to request the buffer addresses with the $DE00 command before using the $DE01 command so you'll be sure to have the buffer addresses handy.

Get Block Parameters.

This command allows you to find out where the current "block" image is stored, as well as how large it is. The block parameter request message is for matted as follows:

message[0] = $DE02
message[1] = Accessory ap_id
message[2] = 0
message[3] = Unused
message[4] = Unused
message[5] = Unused
message[6] = Unused
message[7] = Unused

When DEGAS Elite receives this message, it will compose and send a reply message, $DE82, as follows:

message[0] = $DE82
message[1] = DEGAS Elite ap_id
message[2] = 0
message[3] = Index of buffer where block is located
message[4] = X coordinate of upper left comer of block
message[5] = Y coordinate of upper left corner of block
message[6] = Width of block in pixels
message[7] = Height of block in pixels

In the current version of DEGAS Elite, the block is stored in buffer number 3, and always has its upper left corner at coordinates 0,0. This may change in future versions. If you choose to change the block, be sure you align it at the proper position in the correct buffer.

Set Block Parameters.

DEGAS Elite allows accessories to change the current block size. This would be necessary if the accessory loaded a different-sized block image into the block buffer. The command to change the block size is as follows:

message[0] = $DE03
message[1] = Accessory ap_id
message[2] = 0
message[3] = New block width
message[4] = New block height
message[5] = Unused
message[6] = Unused
message[7] = Unused

There is no reply to this message from DEGAS Elite. However, it is important that you set the block size to a setting compatible with the current screen resolution (greater than 0 and less than or equal to the screen width and height).

Changing the Color Palette.

If you are writing an accessory that loads a picture from disk and places it in one of the display buffers, you'll want to be able to set the color palette to the appropriate settings. This is done with the color palette change command, $DE04. This is a special case message, consisting of 48 bytes. It is formatted as follows:

message[0] = $DE04
message[1] = Accessory ap_id
message[2] = 32
message[3] = Image palette set flag
message[4] = Unused
message[5] = Unused
message[6] = Unused
message[7] = Unused
message[8] = Palette color 0
message[9] = Palette color 1
message[10] = Palette color 2
message[11] = Palette color 3
message[12] = Palette color 4
message[13] = Palette color 5
message[14] = Palette color 6
message[15] = Palette color 7
message[16] = Palette color 8
message[17] = Palette color 9
message[18] = Palette color 10
message[19] = Palette color 11
message[20] = Palette color 12
message[21] = Palette color 13
message[22] = Palette color 14
message[23] = Palette color 15

The first 8 words in the message is a standard-length GEM message. Note that word 2 of the message is 32, indicating that there are 32 bytes (16 words) of data following the main message. Word 3 of the message is a flag that tells DEGAS Elite whether or not it is to assign the palette to the currently selected drawing screen. If the word is 0, the palette is loaded but is not assigned to the current drawing screen. If the word is nonzero, the palette is loaded and is assigned to the current drawing screen.

If you are loading a picture with a new palette, you will want to use a nonzero value in the flag, so that the picture will have the proper colors assigned to it. If you're just changing the colors, use a zero in the flag. This will allow you to use the "remap" function to recolor the current picture to the closest match using the new palette.

When you send the color palette set command to DEGAS Elite, be sure to send a total of 48 bytes to Elite, with either a single, large message buffer (one appl_write) or with a normal message buffer and the palette array. Both options are shown below. One large array:

appl_write(DEGAS_id,48,message);

Normal message buffer and palette array:

appl_write(DEGAS_id,16,message);
appl_write(DEGAS_id,32,palette);

Either way, the 48-byte message will be received properly by DEGAS Elite. You must always send the entire 16-word palette array, regardless of the resolution.


CAUTIONS

When a program such as DEGAS Elite uses a communication pipe, you should use only the documented message pipe commands to get information from the program. Using tricks such as disassembling the program and locating undocumented variables or buffers is a big no-no, and can only lead to disaster in the future when a new version of the program is released. Don't do it. 'Nuff said.

When an accessory takes over and must display a dialog, it is best for the accessory to open a full-screen window over DEGAS Elite's control panel. If this is not done, mouse clicks on the panel will be relayed by GEM to DEGAS, possibly messing up the display. Just be sure to close the window when your accessory is done with its work. When control returns to DEGAS (or whatever program you are working with), it will receive a full-screen redraw message, and will redraw its windows, control panels, and so on.


DEMONSTRATIONS

The DECOMM.ACC desk accessory program on your START disk is a demonstration of how to communicate with DEGAS Elite. It shows the use of all communication pipe commands and reception of replies from DEGAS. When activated, if DEGAS Elite is resident, the accessory will print out a report on your printer telling the status of Elite. It will print a list of pointers to the screen buffers, the current screen in use and the block parameters.

You can write all sorts of interesting accessories that will work with DEGAS Elite, and Patrick Bass, START Technical Editor has done just that. Check out his accompanying article for more information and ideas.

With the communication pipe and a well-defined, well-thought-out communication protocol, your programs can be written so that they can be easily expanded in the future. It just takes a little effort when you're coding the main program. I hope you find this application for desk accessories to be as powerful as I have.

Desk accessories. They're not just RAM-wasters any more.