OOP Code Resources
Volume Number: 7
Issue Number: 11
Column Tag: Object Workshop
Related Info: Device Manager Control Manager List Manager
OOP & Code Resources
By Peter Blum, Essex, MA
Note: Source code files accompanying article are located on MacTech CD-ROM or
source code disks.
[Peter Blum is the Macintosh Project Leader at TIMESLIPS Corporation where
he develops Timeslips III For the Mac and related products. He is a graduate of the
University of Massachusetts at Amherst (BS CSE).]
Code Resources And Their Limitations
One of the most interesting programming techniques that the Macintosh offers is
the use of code resources to dynamically extend system behavior. The Toolbox itself has
several managers that use code resources. Window Manager (WDEF), Menu Manager
(MDEF), Control Manager (CDEF), and List Manager (LDEF) are the most common.
Additionally, programmers are adding code resource hooks into their products to make
them extendable by outside programmers. HyperCard offers XCMDs, MORE II provides
file format translators in code resources, and After Dark v2.0’s screen savers are code
resources.
There are some basic facts to know about code resources:
• They are stored as a single segment of code
• The caller jumps to the code segment at its first byte address
• They can be coded in high level language compilers like Think and MPW C and
Pascal with passing parameters
• They do not support global variables
• They do not support object oriented code from C or Pascal
CDEF Code Resource Example
A code resource for a CDEF in Think Pascal takes the following form:
{1}
unit CDEFCode;
interface
function Main(varCode : integer;
theControl : ControlHandle;
message : integer; param : LongInt) : LongInt;
implementation
function Main(varCode : integer;
theControl : ControlHandle;
message : integer; param : LongInt) : LongInt;
var
result : LongInt;
{*** various routines here ***}
begin
case message of
drawCntl:
result := MyDrawRoutine;
testCntl:
result := MyTestRoutine;
calcCRgns:
result := MyCalcRoutine;
initCntl:
result := MyInitRoutine;
dispCntl:
result := MyDispRoutine;
posCntl:
result := MyPosRoutine;
thumbCntl:
result := MyThumbRoutine;
dragCntl:
result := MyDragRoutine;
autoTrack:
result := MyTrackRoutine;
end; { case }
Main:= result;
end; { Main }
end. { unit CDEFCode }
The CDEF code resource demonstrates three basic solutions to limitations of code
resources. The parameters passed each offer the code resource a solution:
varCode - allows the code resource to select one of several variations to execute.
theControl - contains information that must be retained between calls to the
resource. This is a substitution for global variables.
message - provides the resource with a way to select a process or specialized
routine. Typically, there are messages to initialize and dispose plus several to handle
the various operations specific to the resource.
Working Around Code Resource Limitations
Specific Toolbox managers like the Control Manager restrict the global variable
structure for their own use; it usually contains configuration information and some
private fields. So, the data structure isn’t setup to hold globals for the code resource.
However, most Toolbox Managers add an extra 4 byte field for the code resource
to store data for globals.
Toolbox Manager Resource type Field Name
Window Manager WDEF dataHandle
Control Manager CDEF contrlData
List Manager LDEF userData
The Menu Manager’s MDEF doesn’t offer this type of field. When our code
resource is initialized, we will allocate memory to hold our globals and store it in the
appropriate field.
As you can see, the CDEF supports variations. This way, you can write one CDEF
that handles several similar cases. In fact, the built in controls: button, checkbox, and
radio are all variations in one CDEF. With a growing appreciation for object oriented
programming which is well suited for programming variations, wouldn’t it be nice to
code a CDEF or any other code resource using objects?
Well, object oriented programming within MPW and Think Pascal and C cannot
be used for code resources! Why? Because they need internal “method tables” which
demand true global variable storage - not the artificial globals provided by
datastructures passed by the caller. Additionally, they tend to require more than one
code segment. But don’t give up hope because Think Pascal can offer a solution.
Using Drivers In Think Pascal
Although Think Pascal cannot compile globals or multiple segments for code
resources, it can for drivers. The driver is a special type of code resource with a
header containing a specialized jump table and other data. The Device Manager
maintains drivers. Think Pascal builds the header and adds intermediate code that
interprets each message from a Device Manager routine and passes it to our Pascal
code. Additionally, Think’s driver code support multiple segments, true global
variables, and objects!
So, we can write the code resource to load and call a driver resource written in
Think Pascal. The driver resource contains most of the code including any objects. The
code resource is simply a shell that manages and calls the driver resource. Here is the
format for a driver routine in Think Pascal.
{2}
unit DriverCode;
interface
function Main(devCtlEnt : DCtlPtr;
paramBlock : ParmBlkPtr;
sel : integer) : integer;
implementation
function Main(devCtlEnt : DCtlPtr;
paramBlock : ParmBlkPtr;
sel : integer) : integer;
begin
case sel of
0: { Open - initialize }
1: { Prime - read/write blocks }
2: { Control - various messages }
3: { Status - inquiries of the device }
4: { Close - dispose }
end; { Main }
end. { unit DriverCode}
The first parameter of Main, devCtlEnt, points to the device control entry record
which follows (IM II-202):
{3}
DCtlHandle = ^DCtlPtr;
DCtlPtr = ^DCtlEntry;
DCtlEntry = record
dCtlDriver: Ptr; { pointer to ROM driver }
{ or handle to RAM driver }
dCtlFlags: integer;
dCtlQHdr: QHdl; {driver I/O queue header}
dCtlPosition: LongInt;
{ byte position used by Read and Write calls}
dCtlStorage: Handle;
{ handle to RAM driver's private storage }
dCtlRefNum: integer;
{ driver's reference Number }
dCtlCurTimes: LongInt; { used internally }
dCtlWindow: WindowPtr;
{ pointer to driver's window }
dCtlDelay: integer;
{ number of ticks between periodic actions }
dCtlEMask: integer;
{ desk accessory event mask }
dCtlMenu: integer;
{ menu ID of menu associated with driver }
end;
The second parameter is the standard ParamBlockRec that is called in low-level
disk routines. See IM IV-116 for details.
Before we get into detailing the code, lets look at some of the complexities that
drivers add and how we will work around them. When you read Inside Macintosh Vol 2,
Device Manager, you learn that the Macintosh only supports up to 32 drivers
(including Desk Accessories) and there are almost 32 installed at any time. They are
given reference numbers from 0 to 31. Most are taken up by Printer, Sound, Disk,
Serial ports, and DAs (See IM II-192). So, adding a Device Manager driver virtually
requires you to replace existing ones. Even if there is one or two free reference
numbers for our drivers to use, most applications use more than one CDEF, LDEF or
other code resource. Additionally, the Device Manager adds its own overhead by linking
our driver into the device queue.
Home Brewed Device Manager
So, lets NOT use the Device Manager. Instead, we’ll call the Think Pascal driver
simulating the Device Manager through a short assembly language routine for which
we’ll use an inline Pascal routine.
{4}
function CallDriver (devCtlEnt: DCtlPtr;
paramBlock: ParmBlkPtr;
theDriverOfs: Ptr): integer;
inline
$2F0A, { MOVEA.L A2,-(A7)
; preserve A2 in the function return }
$246F, $0004, { MOVEA.L 4(A7),A2
; Routine to jump to }
$206F, $0008, { MOVEA.L $8(A7), A0
; parmBlkPtr must go in A0 }
$226F, $000C, { MOVEA.L $C(A7), A1
; dCtlPtr must go in A1 }
$4E92, { JSR (A2)
; call the driver-D0 will contain the result }
$245F, { MOVEA.L (A7)+,A2
; restore A2 before setting return value }
$DEFC, $000C, { ADDA.W #$C, A7
; restore the stack except for function
return value }
$3E80; { MOVEA.L D0, (A7)
; return value on stack }
When called, we pass the address of the routine within our driver that handles
one of the basic routines for a driver: Open, Close, Prime, Control, or Status. To do
this, we need to understand more about the header of the driver. The following figure
will help (see Inside Macintosh Volume 2: Device Manager for more detail) on the next
page.
Figure 1:Device Header Block
The top address is where our driver is loaded in memory. We get this address by
loading our driver, locking it, and dereferencing it once. When we want to call a
routine within the driver, we must load the offset for that routine in drvrOpen,
drvrPrime, drvrCtl, drvrStatus, drvrClose. These are each words which is a data type
not supported by Think Pascal. Instead, we can get a word by using the Hiword and
Loword functions applied to an array of LongInts. The following shows how to get the
final address, driverOfs, and call the driver:
{5}
LongArr = array[0..10] of LongInt;
LongArrPtr = ^LongArr;
LongArrHdl = ^LongArrPtr;
const
{ getting the offsets }
drvrSelOpenOffset = 2;
{ HiWord(LongArrHdl(myDriver)^^[2]) }
drvrSelPrimeOffset = 2;
{ LoWord(LongArrHdl(myDriver)^^[2]) }
drvrSelControlOffset = 3;
{ HiWord(LongArrHdl(myDriver)^^[3]) }
drvrSelStatusOffset = 3;
{ LoWord(LongArrHdl(myDriver)^^[3]) }
drvrSelCloseOffset = 4;
{ HiWord(LongArrHdl(myDriver)^^[4]) }
var
myDriver : Handle;
driverOfs : Ptr;
myLongArr : LongArrHdl;
begin
myDriver := GetResource('DRVR', drvrResID);
MoveHHi(myDriver);
HLock(myDriver);
myLongArr := LongArrHdl(myDriver);
driverOfs := Ptr(ord4(myDriver) +