How and Why to Use External Commands and Functions
By Christopher Watson

It is now evident that a new trend in programming has attracted a significant part of the Macintosh community. In growing numbers, novice programmers use HyperCard and its built-in language, HyperTalk, as a springboard toward involvement in higher levels of development.
This makes good sense for several reasons. To begin with, HyperTalk is well suited as a beginner’s language, and learning it provides a quick and easy method for grasping the flow and art of programming. Secondly, HyperCard is an ideal and flexible environment to implement those compiled segments of code known as External Commands and Functions (XCMDs and XFCNs). Using HyperCard as a vehicle, the greenhorn programmer isn’t forced to spend excessive amounts of time learning about interface considerations in order to see her code at work. Finally, externals development itself offers an attractive entry-level path into the world of high-level language Macintosh programming.
It’s a sound path to follow, and I highly recommend it. In all honesty, though, this newfound interest in programming knowledge brings with it a growing number of abuses. One of the most frequent misuses of externals is using them as substitutes for speed. It is difficult to develop and implement externals without a little give and take, and easy to succumb to the “X-Dream.”
Once the avid HyperCard/HyperTalk developer reaches a certain level of expertise, she typically moves to externals development to address the inevitable concerns of speed, power, and transparency. It’s tempting to jump blindly into the fray, but for those who just don’t quite know where to begin, it’s a tough proposition. Not only must a new language be learned, but one must consider several important issues as well.
There are numerous arguments for and against any particular programming environment as the optimal medium for developing XCMDs and XFCNs. I will not bore you with a discourse on the merits of one over another. At some point, the suitable choice for you will become evident. For those who are new to programming, the learning curve can be excruciating. Some people have an aptitude for acquiring programming skills quickly while others agonize over every step of the process. Regardless of your potential, proficiency comes relatively quickly only if you engage yourself wholeheartedly, and refuse to be intimidated.
As you learn the language, the programming bug begins to bite. Suddenly, all your mouseUps want to become EventRecords, and all your fields want to be Handles. The shortcomings of your application are now no more than minor piques. A whole new world opens for you. As never before, you feel surges of power brought on by your ability to program.
What a seduction!
The Developer’s Litmus Test ∆ There are three basic criteria you can use to help you decide whether or not an XCMD would be the best solution for a particular stack project:
• The external should greatly accelerate some time or memory-intensive processing.
• It should satisfactorily extend the capabilities of your stack, and HyperCard as a whole.
• It should sufficiently enhance the fluidity or smoothness of the stack’s interface or its general look
and feel.
It’s fairly easy to justify an external when it accomplishes something impossible within HyperTalk. And if your external lessens the amount of work for the stack end user by providing a more efficient, cleaner interface, then you’ve got it made.
But you also have to decide whether the benefits you get from having the external in your stack justify the extra work you have to do to write the code.
The Eternal Battle For Speed ∆ More often than not, the external in question will not increase speed substantially. Despite the obvious stopwatch comparisons, speed weighs heavily on efficiency of source code. Until you gain more experience in writing tight, efficient code, your externals may actually be slower than their HyperTalk counterparts. Merely duplicating the actions of a HyperTalk script is not a valid purpose for an external. Somewhere along the line, your code should improve on the processing capabilities of its HyperTalk equivalent (if there is one).
A very short, simple function, performing no more than one or two key commands, rarely yields a noticeable increase in speed. For the most part, the longer and more process-intensive the function is, the greater the efficiency ratio between the XCMD and its HyperTalk counterpart.
A good example of the downside of this principle is illustrated by a publicly available product, the “ToggleICON” XCMD. (I wrote this laughable external in my early days, so I’m not exposing myself to any risk by lambasting it.) This XCMD does nothing more than toggle between two ICON attributes for a button. It makes your script look neater, using only one line instead of three or four, but the external itself adds 8K to the size of the stack, and there is no perceptible speed increase because the task is so diminutive. The external makes no sense and is a tremendous waste of effort.
On the upside, my new “FindReplace” XFCN is far more viable. It performs a search and replace on the text in any field. In HyperTalk, performing this task on a field containing any more than a few hundred characters is cause for hair loss. This XFCN makes the replacement cycle much more bearable because it utilizes a very fast ToolBox Utility that you simply don’t have direct access to from HyperTalk. It accelerates the procedure many-fold, and is not a weighty chunk of code. With the addition of a good-looking dialog box for user input of the parameters, a practical external is born that makes HyperCard work better.
While you evaluate the need for an external, analyze objectively the arguments for its development, keeping the litmus test issues in mind. Speed, in the literal sense, means nothing if your users are forced to stumble through awkward or confusing interface components in order to get it.
My group, XNet™, and Ireceive countless requests for externals that are meant to be nothing but CODE resource emulations of some less-than-speedy scripts.
A natural question is: Why is the script so slow that an external, many times larger than the script, seems essential?
Granted, there are possible functions that dictate the use of an external, due simply to the intense computations they perform. Reiterative string manipulations are notorious examples. In most cases, an external that manipulates data at machine-level, as a relocatable block of memory, reduces computational time dramatically. However, one must never overlook the possibility, in the case of HyperTalk lethargy, that a script is written inefficiently.
To demonstrate a viable external by way of example, let’s take a closer look at the “FindReplace” XFCN, to see where, and to what extent, this particular external benefits HyperCard.
Exhibit A ∆ The full listing of the Turbo Pascal source code for my “FindReplace” XFCN follows. To enhance its readability, I’ve omitted comments, and the code includes no information regarding the DITL and DLOG resources used by the XFCN. Commentary for each section of the code follows the listing.
Those of you experienced enough to recognize the processes in this XFCN may see some strange or unorthodox methods. It goes without saying that different people have different ways of doing things. My technique is not necessarily the best, rather, it is one of several possibilities. For the purpose of this article, I omitted some information concerning elements critical to the complete project, and because of this, I cannot guarantee its integrity during execution.
After declaring the program name (which is almost always the name of the external), we insert some compiler directives. They are fairly standard except for the last one. The $T (type) compiler directive is not a necessary addition. In this case, it assigns a file type and creator, “rsrc” and “RSED,” to the resultant compiled file. These are the type and creator signatures for ResEdit documents. Since Turbo Pascal does not allow for direct compilation into the resource fork of a HyperCard stack, this directive makes things easier when it comes time to copy the XFCN into a stack: a simple double-click executes ResEdit and automatically opens up the file containing the compiled code.
In the next line one often finds the first case of crimes against external code size. Every time a new UNIT is used by the code, depending on the unit, it can add an enormous number of mostly unused declarations, functions and procedures. Before the dialog box capabilities were added to this external, the only item used from the “ToolIntf” unit was the “Munger” function. By placing the actual INLINE interface call to the function directly within the XFCN code:
FUNCTION Munger(h: Handle; offset: LongInt; ptr1: Ptr; len1: LongInt; ptr2: Ptr;
len2: LongInt): LongInt; INLINE $A9E0;
one avoids adding over 4K of dead weight. With the inclusion of the dialog box functions, however, a number of other “ToolIntf” functions are now necessary, making this size reduction technique no longer feasible. Let’s face it… who wants to type all those interfaces when they’re already in the units? Nevertheless, when one or two functions are all that are used from a particular unit, it’s best to remove the unit from the USES lineup, and type the interfaces in directly.
After establishing the entry point for the XFCN, we make a few global constant and variable declarations. These declarations are used in the main procedure of the XFCN, or they are shared by various function and procedures. In most cases, pointers to vital blocks of data, like the Dialog Record, are declared at this point. Since the pointer to the data associated with the dialog is used by some of the other procedures, it is declared globally to remain active during the entire course of code execution. Of course, it’s otherwise perfectly acceptable to declare a DialogPtr as a variable specific to the “DoDialog” procedure. In that case, we would have to design a new implementation of the “ClearOut” procedure. More on that later.
We now run through all the Callback or Glue Routines that will be used by the XFCN. Many developers simply use an $I (Include) directive to bring in the entire collection of Glue Routines from a separate file. In many instances, this causes further unnecessary bloating of the external. It’s much better to insert directly only the functions that will be used, as I have done.
Two of these routines are responsible for retrieving and resetting the text contained within a HyperCard field: “GetFieldByNum” and “SetFieldByNum.” These access routes to two very useful hooks, built into the HyperCard application, make the wholesale alteration of field contents extremely easy. We use GetFieldByNum to pull the field data into the external and, after manipulating it, use SetFieldByNum to replace it.
From that point, we continue with the custom functions and procedures used by the XFCN. Most are fairly simple and perform one or two specific tasks. One of those procedures, “CenterDLOG,” is one you’ll probably want to place in your personal library of useful procedures. It centers any dialog window within the card window, and accounts for inconsistent window positioning on large screens. There are numerous published variations of this type of procedure, but this particular version enables one to pass a copy of the DialogPtr as a parameter to the call. This allows you to place the function near the top of the code and center any dialog window you may be working with. Another nice feature of this procedure is that it accesses the Dialog Record to determine the dimensions of the window, freeing you from having to know the dimensions beforehand.
We now get to the heart of the matter. The main procedure, the final block headed by only a BEGIN statement, is executed first when the XFCN is called from HyperTalk. All the other functions and procedures are called from this area.
After determining that only a single parameter is passed to the XFCN, we parse it out to gain a reference to the field. According to the design of the XFCN, that parameter should be either “cd” or “bg,” followed immediately by the number of the field. Therefore, card field number four should be designated in the parameter as a string reading “cd4,” resulting in a BOOLEAN value taken from the first two characters, and an INTEGER from the last character. We pass these results first to the “GetFldName” function so that we may later display the actual name of the field in the dialog box. Then the external executes the function that handles all the pertinent dialog box actions, and returns the number of the enabled item that received the mouseclick. With this number (representing a “hit” in either the “OK” or “Cancel” button), we can determine whether to continue with the procedure, or exit the external entirely.
Before the dialog is disposed of, we retrieve the strings from the two editable text items: the “find” and “replace” strings supplied by the user. Pointers to these strings are then fed to the Munger function. After obtaining a handle to the field data via the “GetFieldByNum” function, we can begin churning out the new text.
We initialize a couple of important variables: “off” as the offset result of the Munger function, and “x” as a value incremented by one after each successful completion of the following repeat loop. This variable ultimately contains the total number of matches found within the field data.
The repeat loop calls the Munger function, which locates successive occurrences of the find string and replaces them with the replace string. Two other actions take place: the “SendHCMessage” Glue Routine sets the HyperCard cursor to the beachball. With each iteration, this call rotates the beachball in one-eighth increments to inform the user that the procedure is executing smoothly. In addition, the counter variable advances in value each time Munger returns a result indicative of a successful find.
There is a very important portion of the implementation of the Munger function in XCMDs and XFCNs that must be brought to the fore. Munger works, in part, by supplying pointers to both the target and replacement strings. While Munger is used in the main procedure of an external, it is not possible to use the “@” operator directly to convert the string variables to pointers. The conversion requires a separate function; hence, the “GetPtr” function. It’s one of many methods available for circumventing this unwarranted restriction.
At the point where Munger returns a negative value (meaning no further matches were found), the external exits the loop and, using the “SetFieldByNum” procedure, replaces the contents of the selected field with the modified data. Finally, all memory occupied by the dialog and handles is released, and the “returnValue” field of the XCmdBlock is filled with the number of total replacements made. This number represents the value returned by the XFCN to HyperTalk.
All of this, not surprisingly, goes by very fast. The nice thing is that it doesn’t really matter how much text is in the field. Munger only acts on the replacements. As many as 100 replacements take as few as two seconds to complete on a Macintosh II, even if the field contains its limit of 30,000 characters.
The Proof is in the Coding ∆ As you can see, by using an XFCN to access the Munger function, we’re able to perform an action that might very well be a jail sentence in HyperTalk. By examining the full code for this example, you should be able to get a better feel for the organization of such an XFCN, as well as why this type of external is one of the more practical methods for attaining needed speed.
Of course, I’m not an attitude developer. I realize the above example could probably be refined to a great extent. If you have ideas concerning the improvement of “FindReplace”, do not hesitate to contact me on the CONNECT/MacNET System at the ID below.
Who Ya Gonna Call? ∆ You may have no interest in learning to write your own XCMDs, which is perfectly understandable. Before I convinced myself to put my nose to the grindstone and learn, I had many externals written for me by Oscar Hills (of “CustomFileName” and “ChooseICON” fame). You could do the same thing, but even though Oscar is a wonderfully helpful person, he’s very busy these days.
But now, another terrific and definitive resource for information, source code examples, and friendly accompaniment in the learning process exists. XNet™, a growing world-wide coalition of experienced HyperTalk and Externals Developers, was founded to answer the demands of both the novice and expert developer. The members of the group regularly release new FreeWare stack and XCMD/XFCN products, which usually include the source code and other informative tidbits. Although the membership is growing, most of the current members can be contacted on major networks and information services such as CONNECT/MacNET, CompuServe, GEnie and AppleLink. Inquire with the System Operator or Forum Manager of the system’s HyperCard areas for information concerning XNet™ members’ electronic addresses.
To obtain information about joining XNet™ (which isn’t that hard to do, folks… we’re easy!), write to me at: XNet™, 503 South School Street, Lodi, CA 95240. I’m also available on the CONNECT Professional Information Network (MacNET) at CWATSON.
Christopher Watson is an independent HyperCard and XCMD/XFCN Developer and is the Founder of XNet™. The source code for the “FindReplace” XFCN used in this article is Copyright 1989 Christopher Watson. All Rights Reserved. Reprinted with permission.
Listing 1
program FindReplace;
{$R-}
{$U-}
{$D PasXFCN}
{$T rsrcRSED}
USES MemTypes, QuickDraw, OSIntf, ToolIntf,
HyperXCmd;
PROCEDURE PasXCMD(paramPtr: XCMDPtr);
CONST
dlogID = 1024;
VAR
fldNum, hit : INTEGER;
targStr, repStr, fldName : Str255;
len1, len2, off, x : LONGINT;
cdFlag : BOOLEAN;
h : Handle;
myDLOG : DialogPtr;
savePort : GrafPtr;
savePen : PenState;
PROCEDURE DoJsr(addr: ProcPtr); INLINE $205F,$4E90;
FUNCTION PasToZero(str: Str255): Handle;
BEGIN
WITH paramPtr^ DO
BEGIN
inArgs[1] := ORD(@str);
request := xreqPasToZero;
DoJsr(entryPoint);
PasToZero := Handle(outArgs[1]);
END;
END;
PROCEDURE ZeroToPas(zeroStr: Ptr; VAR pasStr: Str255);
BEGIN
WITH paramPtr^ DO
BEGIN
inArgs[1] := ORD(zeroStr);
inArgs[2] := ORD(@pasStr);
request := xreqZeroToPas;
DoJsr(entryPoint);
END;
END;
PROCEDURE SendHCMessage(msg: Str255);
BEGIN
WITH paramPtr^ DO
BEGIN
inArgs[1] := ORD(@msg);
request := xreqSendHCMessage;
DoJsr(entryPoint);
END;
END;
FUNCTION EvalExpr(expr: Str255): Handle;
BEGIN
WITH paramPtr^ DO
BEGIN
inArgs[1] := ORD(@expr);
request := xreqEvalExpr;
DoJsr(entryPoint);
EvalExpr := Handle(outArgs[1]);
END;
END;
FUNCTION NumToStr(num: LongInt): Str31;
VAR str: Str31;
BEGIN
WITH paramPtr^ DO
BEGIN
inArgs[1] := num;
inArgs[2] := ORD(@str);
request := xreqNumToStr;
DoJsr(entryPoint);
NumToStr := str;
END;
END;
FUNCTION StrToNum(str: Str31): LongInt;
BEGIN
WITH paramPtr^ DO
BEGIN
inArgs[1] := ORD(@str);
request := xreqStrToNum;
DoJsr(entryPoint);
StrToNum := outArgs[1];
END;
END;
FUNCTION GetFieldByNum(cardFlag: BOOLEAN; fldNum: INTEGER): Handle;
BEGIN
WITH paramPtr^ DO
BEGIN
inArgs[1] := ORD(cardFlag);
inArgs[2] := fldNum;
request := xreqGetFieldByNum;
DoJsr(entryPoint);
GetFieldByNum:=Handle(outArgs[1]);
END;
END;
PROCEDURE SetFieldByNum(cardFlag: BOOLEAN; fldNum: INTEGER; fldVal: Handle);
BEGIN
WITH paramPtr^ DO
BEGIN
inArgs[1] := ORD(cardFlag);
inArgs[2] := fldNum;
inArgs[3] := ORD(fldVal);
request := xreqSetFieldByNum;
DoJsr(entryPoint);
END;
END;
PROCEDURE Fail(str : Str255);
BEGIN
WITH paramPtr^ DO
BEGIN
SysBeep(1);
returnValue := PasToZero(CONCAT(‘Err -
‘,str));
END;
END;
PROCEDURE CenterDLOG(dlog : DialogPtr);
VAR
tempHdl : Handle;
tempStr : Str255;
cdLoc, dLoc : Point;
BEGIN
WITH paramPtr^ DO
BEGIN
tempHdl := EvalExpr(‘item 1 of loc of cd
window’);
ZeroToPas(tempHdl^,tempStr);
cdLoc.h := StrToNum(tempStr);
DisposHandle(tempHdl);
tempHdl := EvalExpr(‘item 2 of loc of cd window’);
ZeroToPas(tempHdl^,tempStr);
cdLoc.v := StrToNum(tempStr);
DisposHandle(tempHdl);
WITH dlog^.portRect DO
BEGIN
dLoc.h := cdLoc.h + (256 - ((right - left)
DIV 2));
dLoc.v := cdLoc.v + (176 - ((bottom - top)
DIV 2));
END;
MoveWindow(dlog,dLoc.h,dLoc.v,TRUE);
END;
END;
FUNCTION GetCdFlag : BOOLEAN;
VAR
tempStr : Str255;
BEGIN
WITH paramPtr^ DO
BEGIN
ZeroToPas(params[1]^,tempStr);
IF tempStr[1] = ‘c’ THEN GetCdFlag := TRUE
ELSE IF tempStr[1] = ‘b’ THEN GetCdFlag
:= FALSE;
END;
END;
FUNCTION GetFldNum : INTEGER;
VAR
tempStr : Str255;
BEGIN
WITH paramPtr^ DO
BEGIN
ZeroToPas(params[1]^,tempStr);
GetFldNum := StrToNum (COPY(tempStr,3,
LENGTH(tempStr)));
END;
END;
FUNCTION GetFldName(flag : BOOLEAN; num : INTEGER)
: Str255;
VAR
fStr, fldName : Str255;
tempHdl : Handle;
BEGIN
IF flag THEN fStr := ‘cd’ ELSE fStr := ‘bg’;
tempHdl := EvalExpr(CONCAT(‘short name of ‘,fStr,’ fld ‘,NumToStr(num)));
ZeroToPas(tempHdl^,fldName);
GetFldName := fldName;
DisposHandle(tempHdl);
END;
FUNCTION DoDialog(flag : BOOLEAN; num : INTEGER;
str : Str255) : INTEGER;
VAR
tempLong : LONGINT;
tempType, itemHit : INTEGER;
tempHdl : Handle;
tempRect : Rect;
dLoc : Point;
tempStr, fStr : Str255;
BEGIN
GetPort(savePort);
GetPenState(savePen);
IF flag THEN fStr := ‘cd’ ELSE fStr := ‘bg’;
tempStr := CONCAT(‘Working on ‘,fStr,’ fld ‘,
NumToStr(num),#44#32#34,str,#34);
ParamText(tempStr,’’,’’,’’);
myDLOG := GetNewDialog(dlogID,NIL,POINTER(-1));
CenterDLOG(myDLOG);
ShowWindow(myDLOG);
SetPort(myDLOG);
GetDItem(myDLOG,1,tempType,tempHdl,tempRect);
InsetRect(tempRect,-4,-4);
PenSize(3,3);
FrameRoundRect(tempRect,16,16);
InitCursor;
REPEAT
ModalDialog(NIL,itemHit);
UNTIL (itemHit = 1) OR (itemHit = 2);
DoDialog := itemHit;
GetDItem(myDLOG,3,tempType,tempHdl,tempRect);
GetIText(tempHdl,targStr);
GetDItem(myDLOG,4,tempType,tempHdl,tempRect);
GetIText(tempHdl,repStr);
END;
FUNCTION GetPtr(str : Str255) : Ptr;
BEGIN
GetPtr := POINTER(ORD(@str) + 1);
END;
PROCEDURE ClearOut;
BEGIN
DisposDialog(myDLOG);
SetPort(savePort);
SetPenState(savePen);
SendHCMessage(‘set cursor to hand’);
END;
BEGIN
WITH paramPtr^ DO
BEGIN
SendHCMessage(‘set cursor to watch’);
IF (paramCount <> 1) THEN
BEGIN
Fail(‘Incorrect parameter number.’);
EXIT;
END;
cdFlag := GetCdFlag;
fldNum := GetFldNum;
fldName := GetFldName(cdflag,fldNum);
hit := DoDialog(cdFlag,fldNum,fldName);
IF (hit = 2) THEN
BEGIN
ClearOut;
EXIT;
END;
h := GetFieldByNum(cdFlag,fldNum);
IF result <> xresSucc THEN
BEGIN
ClearOut;
Fail(‘Could not get field.’);
EXIT;
END;
off := 0;
x := 0;
len1 := LENGTH(targStr);
len2 := LENGTH(repStr);
SendHCMessage(‘set cursor to busy’);
REPEAT
SendHCMessage(‘set cursor to busy’);
off :=
Munger(h,off,GetPtr(targStr),len1,GetPtr(repStr),len2);
IF (off > 0) THEN x := x + 1;
UNTIL (off < 0);
SetFieldByNum(cdFlag,fldNum,h);
IF (result <> xresSucc) THEN
BEGIN
ClearOut;
Fail(‘Could not set field.’);
DisposHandle(h);
EXIT;
END;
DisposHandle(h);
ClearOut;
returnValue := PasToZero(NumToStr(x));
END;
END;
BEGIN
END.