Forth DA
Volume Number: 3
Issue Number: 2
Column Tag: Threaded Languages
Building Desk Accessories with Forth
By Jörg Langowski, MacTutor Editorial Board, Grenoble, France
A template desk accessory in Mach2
Any of our readers who has taken a close look at Mach2 code will have noticed that
the output generated by the Forth compiler resembles very much that of 'classical'
compiled languages like Pascal. Disassembly shows that much of the code is
inline-generated machine code, and references to kernel routines are not too frequent.
This is one of the strong points of Mach2; retaining most of the ease of
programming and debugging with a Forth interpreter, you can not only generate
stand-alone applications, but also things that are much more dependent on interacting
directly with the operating system such as VBLtasks that run independently of a
runtime Forth kernel (see my article in V2#6 ); one could also imagine to create INIT
resources, MDEFs or WDEFs and... desk accessories.
The problem with Forth code in general is that a 'stand-alone- application'
generated by any Forth system available for the Macintosh contains - and is dependent
on - at least part of the run time Forth kernel. Whether the kernel contains the
interpreter (as in MacForth) or the task of interpreting is taken over by the CPU
itself in subroutine threaded code such as Mach2 - the standard I/O routines, window
handling, controls, menus etc. all come in a pre-written package that will form part of
the stand-alone application. To write something like a desk accessory in Forth, this
would imply that the runtime package had to be part of the DA. This is (a) not very
practical because space-consuming and (b) not an available option in any Forth for the
Macintosh that I'm aware of.
Nevertheless, Mach2 allows us to create a functioning desk accessory without too
much effort and allows me to simultaneously illustrate some principles of DA
programming to you at the same time.
DA strategy for implementation in Mach2
How would we proceed to build a desk accessory using Mach2? The DA is a DRVR
resource. We would have to create this resource in memory first, then write it out to a
resource file. There are resource manager routines that allow us to do this;
AddResource will add a new resource to the current resource file, given a handle to a
data structure in memory, and UpdateResFile saves these changes to the resource file.
So all we have to do is to create a data structure of the format of a desk accessory, get a
handle to it and call AddResource with the type DRVR to create the new driver, then
update the resource file.
The general format of a desk accessory is known from Inside Macintosh. The first
couple of bytes are a header containing flags that tell the system whether the DA needs
to be called periodically, and what type of calls it can respond to; a delay setting that
determines the time between periodic actions; and event mask that determines what
events the desk accessory will respond to; a menu ID (negative) if the DA has a menu
associated with it; and offsets to the Open, Prime, Control, Status and Close routines.
These latter offsets are measured from the beginning of the desk accessory to the
beginning of each of the routines. The last portion of the header is a string containing
the driver name.
The remaining portion of the desk accessory may be executable code and can be
written in Mach2 Forth if we make sure that references to kernel routines are avoided.
Before we discuss the example (listing 1), however, let me briefly summarize what
happens when a desk accessory is opened.
Fig. 1 Our simple DA, written in Forth!
Opening the desk accessory
When the DA (or any driver) is opened for the first time, a device control entry
is created by the system, a data structure which contains information about the driver;
it is described in Inside Macintosh (II-190). It will, for instance, contain the driver
flags, the delay setting, the event mask and the menu ID from the desk accessory
header. Also a window pointer to a window associated with the driver may be stored
here or a handle to a memory block if the DA needs to store large amounts of data.
When a driver routine (open, control, close, status, prime) is called, a pointer
the driver's device control entry is passed in register A1. The other parameter,
passed in A0, is a pointer to the parameter block of the call. For desk accessories, this
parameter block is important for Control calls since we'll be able to tell from the
parameter block what has happened that the desk accessory has to respond to, such as
menu selections, periodic actions, editing commands, etc.
Glue routines
Since the parameters to the driver routines are passed in registers, we'll have to
write some assembler code to make them 'digestible' for Forth, which is
stack-oriented. Also, we'll save A0-A1 and restore them after the call; as IM
mentions, no other registers have to be saved. One important thing to remember is that
we have to setup a Forth stack before using any actual Forth code; we make A6 point to
a data block sufficiently large (100 bytes in the example, but you may easily change
this to have more stack space).
The glue routines then in turn call the driver routines which have been written
in Forth. The routines referenced in the DA header are the glue routines, of course. The
final part of listing 1 contains the stack setup, the glue routines, and the part that
initializes the desk accessory header and writes the driver code to a resource file.
The code written is contained between the 'markers' startDA and endDA. For
adding the resource to the file, the word make-res is provided, which gets a handle to a
data structure in memory and calls AddResource with the handle, the resource type
(DRVR in our case) and a resource file ID as parameters.
init-DA will initialize the desk accessory's header. This includes setting the
driver flags, the driver name, an event mask, the delay time between periodic actions,
and calculating and storing the offsets between the start of the DA and the beginning of
the 'glue' routines. Also, the ID of the DA's own menu is stored in the header; we'll
come back to that later.
make-DA calls init-DA and then writes the newly created DRVR resource with
ID=12 into the file "Mach2 DA.rsrc". This file can then be used by RMaker to create a
Font-DA Mover compatible file that contains the DA and any resources owned by it.
DA-owned resources
Listing 2 shows the RMaker input file. It will include the DRVR code from
"Mach2 DA.rsrc" and two more resources, a window template and a menu. Both these
resources have an ID of -16000, which later indicates to the Font/DA Mover that they
are 'owned' by the desk accessory whose ID is 12; they will therefore be moved
together with the DA. If the DA's ID is changed during the move, their IDs will be
changed accordingly so that they always correspond to that of the DA.
The format of the ID number of an owned resource is given in IM (I-109); I'll
briefly review it here. The ID number is always negative and bits 15 and 14 are
always 1. Bits 11-13 specify the owning resource type, and are zero for a DRVR. Bits
5-10 contain the ID number of the owning resource, which therefore must be between
0 and 63. Bits 0-4 may contain a number which identifies the individual resource.
Therefore, the allowed number range for owned resources is between -16384 and -1.
If the DRVR resource has an ID=12, the IDs of the owned resources start with
-16000 ( if bits 0-4 are zero) and go up to -15969 ( bits 0-4 = 31). Since
-16000 is a simple number to remember, the DRVR is given an ID of 12 when it is
created by the program. Both the MENU and WIND resources owned by the DRVR in the
example will have IDs of -16000 (which correspond to 'local' IDs of 0).
Two Forth words are provided to easily convert local IDs to owned resource IDs.
getDrvrID will calculate the driver ID from its reference number, which is kept in the
device control entry; and ownResID will calculate the owned resource ID from the
driver ID and the local resource ID.
The desk accessory
We can now take a look at the desk accessory's main code. The Open routine is
called by the DAOpen glue routine. It will do nothing if the desk accessory's window is
already open, which can be checked by looking at the dCtlWindow field in the device
control entry. If the DA has not been opened yet, or has been opened and then closed
again, it will create a new window from the WIND resource with local ID=0 and store
its pointer in the device control entry; furthermore, it stores the driver reference
number in the windowKind field of the window record. By this means, the desk
manager will know that a window belongs to the DA, how to find it and to send the
appropriate messages to the DA when the window is activated, deactivated, the mouse
clicked in its content region or when it is closed.
Open will in addition calculate the ID of the MENU resource (local ID=0) that is
owned by the DA. This number is also stored in the DA header, but you cannot reliably
assume that it is correct. Font/DA Mover will change the DA's ID and the IDs of its own
resources correctly, but it doesn't go into the DA header and sets the correct menu ID.
However, some negative menu ID has to be present in the DA header in order to tell the
desk manager that the DA has to respond to menu selections. The device control entry,
however, has to contain the correct menu ID; otherwise the DA won't respond to its own
menu.
Close will store zero in the dCtlWindow field so that a new Open will re- create
the window; it deletes the DA's menu and disposes of the heap space occupied by window
and menu, then it redraws the menu bar. Close is called automatically by the Desk
Manager when the close box of the DA window is clicked, or Close is selected from the
File menu of an application where the DA was called.
The Prime and DrStatus routines are not needed here, and will do nothing at all.
Sending messages to the DA
The heart of the desk accessory is the Ctl routine. This routine will receive a
message from the desk manager to indicate which action should be taken - a very
simple implementation of object-oriented behavior, in fact. I have written a shell
routine that handles some of the actions of a desk accessory; all other actions simply do
nothing, but since they are included in the case statement, you can very easily add your
own routines.
The message code is contained in the csCode parameter, which is in the parameter
block whose address was passed in A0 when Ctl was called. Ten messages are possible:
-1 : 'good bye kiss' that will be given just before an application exits to the finder;
64 : an event should be handled;
65 : take the periodic action that has to be performed by the DA. This message is sent
every time the number of ticks in the drvrDelay field of the DA header has
expired;
66 : The cursor shape should be checked and changed if appropriate. This message is
sent each time SystemTask is called by the application, as long as the DA is
active;
67 : A menu selection should be handled. csParam contains the menu ID and
csParam+2 the menu item number;
68 : handle Undo command from the Edit menu;
70 : handle Cut command;
71 : handle Copy command;
72 : handle Paste command;
73 : handle Clear command.
The example implements handlers for the first five actions; no Edit menu
selections are handled. The periodic action simply consists of a short beep once every
second. (You might want to change this to save your sanity if you really want to do
something useful with this desk accessory). The goodBye action is also a beep, but 50
ticks long. When the desk accessory is active and you close an application, it will sound
almost as if the system reboots. Don't let yourself be bo thered by this.
The accCursor message will call update-cursor, which checks whether the mouse
is inside the DA window and changes the cursor to the standard NNW arrow, if
necessary.
The menu and event handlers are a little more complicated. First, since both will
output text to the DA window, we have to write some rudimentary output routines; the
Mach2 output routines won't work without the kernel. tp will type a string in the
current grafPort, and crd acts the same way as cr in the Forth kernel. I've also
included some numeric output routines, which you might find convenient to use; they
are not needed for the example, although I used them in debugging.
The event handler(s)
The DA's response to the accEvent message has to be subdivided according to the
event that has happened. Therefore, we check the what field of the event record and set
up another case statement that contains the handlers for each type of event. The
behavior that we'll give to our DA window is that of a document window with zoom box
and size box that responds to mouse down and key down events by displaying
appropriate messages in the window. The DA's own menu should be displayed when the
window is activated and removed when it is deactivated.
The activate handler, therefore, first checks whether the window is activated or
deactivated, gets the menu from the resource file in the first case and attaches it to the
menu bar, or deletes it in the latter case.
The update handler will clear the update region and redraw the grow icon. Key
down events will clear the window and display a message in the first line.
Mouse down events cannot be handled as easily as with application windows. If you
call findWindow when the mouse is clicked in a desk accessory window, the code
returned is always 2 (= in system window), no matter what part of the system window
was clicked. The drag region and close box are handled by the desk manager, so no
problem there; but we have to check ourselves for clicks in the size or zoom box. This
is e specially annoying for the zoom box, because we also have to keep a record of the
current zoom state of the window. With application windows, the window manager
takes care of this task, changing the part code that is returned by FindWindow
depending on whether we have to zoom in or out.
I defined the words ?inGrow and ?inZoom that return true when the mouse click