This page uses CSS, and renders properly in the latest versions of Microsoft Internet Explorer and Mozilla.
This is Frontier: The Definitive Guide by Matt Neuburg, unabridged and unaltered from the January 1998 printing.
It deals with the freeware Frontier 4.2.3; for commercial Frontier 8, look here, and for the inexpensive Radio UserLand, look here. See my Web site for information.
Those wishing an offline or printed copy may purchase the book, which is still in print.
Copyright 1998 O'Reilly & Associates, Inc. ALL RIGHTS RESERVED. Reprinted with permission.
TOP UP: Reference NEXT: Operators

43

XCMDs and UCMDs

This chapter provides some technical detail supplementing Chapter 23, Extending the Language. It is intended for those thinking of writing a UCMD or XCMD.

Both XCMDs and UCMDs can be written in any development environment you're comfortable with; Metrowerks's CodeWarrior is a likely candidate. For definitive information about XCMDs and how to write them, see the HyperCard documentation. For UCMDs, it is de rigueur to download the Frontier Software Developer's Kit (SDK) from ftp://ftp.scripting.com/userland/. It contains documentation and examples, and some model projects that you can copy and use as shells; also, it provides the "IAC Toolkit", which greatly simplifies the programming interface for dealing with Apple events. There is also a splendid online tutorial on writing UCMDs, by Brent Simmons, at http://www.ranchero.com/frontier/ucmds.

You may be in doubt as to which to write, an XCMD or a UCMD. UCMDs possess several advantages over XCMDs, of which the most salient are:

XCMDs

Communication with an XCMD depends upon a pointer, called an XCmdPtr , to a data structure called an XCmdBlock. The XCmdPtr is the argument handed to the XCMD when it is called.

Example 43-1 XCmdPtr and XCmdBlock
XCmdPtr = ^XCmdBlock;
XCmdBlock = RECORD
    paramCount:     INTEGER;
    params:         ARRAY[1..16] OF Handle;
    returnValue:    Handle; 
    passFlag:       BOOLEAN;
    entryPoint:     ProcPtr; { to call back to HyperCard }
    request:        INTEGER;
    result:         INTEGER;
    inArgs:         ARRAY[1..8] OF LongInt;
    outArgs:        ARRAY[1..4] OF LongInt;
END;

The XCMD can get the actual parameter values by way of params; they are all 0-terminated strings. The XCMD ultimately places into result a handle to a 0-terminated string, if desired, and returns.

Here is an outline of the C code of a typical XCMD. It is from the Dartmouth XCMDs, a HyperCard stack by Kevin Calhoun and Roger Brown which includes many XCMDs and their source code.2 It sorts the lines (fields separated by return) of its first parameter according to the item (fields separated by comma) specified in its second parameter; but that's not important here. In fact, I have omitted most of the actual code, reproducing just enough to illustrate typical tasks that an XCMD performs. ValidStrToNum() shows how you have to worry about converting string parameters to the desired type. ResultIs() and HandleDoSort() show ways to construct a result and attach it to the XCmdPtr. Main() shows typical global and memory management concerns. Comments in italic are mine.

Example 43-2 A typical XCMD (continued)
/* SortFieldByItem1.0.c */
/* © 1991 Trustees of Dartmouth College */
/* written in THINK CTM  © 1991 Symantec Corporation */
/* by Roger Brown 3/2/88  Courseware Development Group 11/19/88 */
 
#include <Memory.h>
#include <Packages.h>
#include "HyperXCmd.h"
#include <stdlib.h>
#include <string.h>
#include "SetUpA4.h"
 
/* various definitions and globals omitted */
 
/* change a string to all upper case */
ucase(s)
char *s;
{ /* omitted */ }
 
char ValidStrToNum(s,n)
char *s;
long *n; 
/* check string for valid ASCII and convert if ok. Return validity.
    Input string is changed to Pascal format */
{
    int c,len;
    
    /* length must be < 32768 */
    len = strlen(s);
    if ((len<1)||(len>32768)) return FALSE;
    /* all characters must be 0..9,-,+ */
    for (c=0;c<len;c++) {
        if ((s[c]<48)||(s[c]>57))
            if ((s[c]!=45)&&(s[c]!=43)) return FALSE;
    }
    CtoPstr(s);
    StringToNum(s,n);
    return TRUE;
}
 
GetHCItem(inStr,i,outStr)
char *inStr,*outStr;
int i;
{ /* omitted */ }
 
/* build a return result structure from a string */
ResultIs(paramPtr,theResult)
XCmdPtr    paramPtr;
char       *theResult;
{
    long    len;
    Handle  resultHandle;
    len = 1+strlen(theResult);
    resultHandle = NewHandle(len);
    BlockMove(theResult,*resultHandle,len);
    paramPtr->returnValue = resultHandle;
}
 
GetLineStarts(source,ucSource)
char *source,*ucSource;
{ /* omitted */ }
 
static int compare(size_t a,size_t b)
{ /* omitted */ }
 
static void swap(size_t a,size_t b)
{ /* omitted */ }
 
Handle DoSort()
{ /* mostly omitted, but note this stuff near the end, where
     to build the sorted result each line in the new order
     is grabbed as a C string and concatenated to the result string */
        
        tempField = theUCASEField;
        next = **tempField = 0;
        for (i=0;i<numLines;i++) {
                sLen = 1+strlen(*theField+*(lineStarts+order[i]));
                for (j=0;j<sLen;j++,next++) {
                        c = *(*theField+*(lineStarts+order[i])+j);
                        if (c==0) c = '\15'; /* change 0s back to CRs */
                        *(*(tempField)+next) = c;
                }
        }
        next--;
        *(*(tempField)+next) = 0; /* put a 0 terminator on the result */
/* the rest is omitted */
}
 
pascal void main(paramPtr)
XCmdPtr    paramPtr;
{
        Str255 paramStr;
 
        /* Prepare to use globals */
        RememberA0();
        SetUpA4();
        
        /* get the input container copy */
        MoveHHi(paramPtr->params[0]);
        HLock(paramPtr->params[0]);
        theField = (Handle)paramPtr->params[0];
       
        HLock(paramPtr->params[1]);
        strcpy((char*)paramStr,*(paramPtr->params[1]));
        
        if (ValidStrToNum((char*)paramStr,&sortItem)==TRUE) {
            /* do the sort */
            paramPtr->returnValue = DoSort();
        }
        else {
            strcpy((char*)paramStr,"Error in item number");
            ResultIs(paramPtr,(char*)paramStr);
        }
        
        /* clean up */
        HUnlock (paramPtr->params[0]);
        HUnlock (paramPtr->params[1]);
        RestoreA4(); 
        return;
}

XCMDs feature a callback mechanism whereby the XCMD can ask the calling application to perform certain utility actions or provide information, using a fixed repertoire of functions. This involves setting up the XCmdBlock in a particular way and passing it back to the calling application, but the programmer is usually shielded by the API from having to attend to these details. Frontier provides HyperCard-like services in response to some callbacks, but there are many callbacks that are so HyperCard-specific that they just aren't supported (they won't crash Frontier, but they won't be executed either). Table 43-1 shows the supported callbacks.

Table 43-1 XCMD Callbacks Supported by Frontier (continued)

Callback Name

Frontier Behavior

EvalExpr

The "expr" must be a UserTalk expression; it is evaluated, and the result is returned.

SendCardMessage, SendHCMessage

The "message" must be a UserTalk expression; it is evaluated, and no result is returned (presumably the message would be a verb call).

SendHCEvent

Should be sent if your XCMD calls WaitNextEvent, so that Frontier can handle unhandled events.

GetGlobal, SetGlobal

The "global" can be any variable in the scope of the calling script. If SetGlobal names a nonexistent variable, an entry is created in the scratchpad table.

ZeroBytes, ScanToReturn, ScanToZero, StringEqual, StringLength, StringMatch, ZeroTermHandle, BoolToStr, ExtToStr, LongToStr, NumToHex, NumToStr, PasToZero, PointToStr, RectToStr, ReturnToPas, StrToBool, StrToExt, StrToLong, StrToNum, StrToPoint, StrToRect, ZeroToPas

Treated normally.

For how to import an XCMD into the database and write glue for it, refer to Chapter 23.

UCMDs

Communication with a UCMD is by way of Apple events. This adds a great deal of overhead to the UCMD, but the Frontier SDK's API shields the programmer from having to attend to the details. The SDK provides a shell script whose main() calls UCMDmain() , which the programmer must write. This routine obtains the parameters, and returns a result, entirely by the use of IAC verbs from the IAC Toolkit which is included in the SDK. Declarations for these verbs may be found in the file iac.h.

Each Apple event datatype (to which UserTalk's datatypes very closely correspond) has a repertory of verbs for manipulating it. IACget xxxparam() obtains a parameter of type xxx passed by the caller; the OSType passed to it names the parameter (more about this in a moment). IACreturn xxx() causes a value of type xxx to be the returned result. IACget xxxitem() and IACpush xxxitem() are for obtaining or setting, respectively, the specified item of a list (lists are of type AEDescList). For example, in the case of a string:

Boolean IACgetstringparam (OSType, StringPtr);
Boolean IACreturnstring (StringPtr);
Boolean IACgetstringitem (AEDescList *, long, StringPtr);
Boolean IACpushstringitem (AEDescList *, StringPtr, long);

To understand the workings of a typical UCMD we must start with how the UCMD will be called. A UserTalk "glue" verb will construct an Apple event (see Chapter 32, Driving Other Applications) and send it to the UCMD. For example, consider the glue (simplified here) for Brent Simmons's pbs.deleteListItem() :

on deleteListItem (x, index)
    return (appleEvent (@pbs.code, 'pbsu', 'dlis', \
        '----', list (x), 'indx', number(index)))

The first parameter for appleEvent() is the address of the UCMD living as a binary in the database. The second parameter is the identification code unique to the UCMD as a whole; it isn't actually important what this is, since the UCMD won't check it (we're just satisfying the rules for Apple events). The third parameter identifies the command you want the UCMD to obey; thanks to this, a UCMD can contain more than one functionality. Then comes the usual series of name-value pairs, of which the first is conventionally the "direct object," whose name is '----'.

Now let's look at the source for pbs.code, omitting the code for all of its functionality except what is needed for deleteListItem() (you can examine the full source at pbs.source). Comments in italic are mine.

Example 43-3 A typical UCMD (continued)
/*
    Brent's Commands 1.0.1
    by Brent Simmons
    <bsimmons@wrldpwr.com>
    © 1996 World Wide Power & Light
    <http://www.wrldpwr.com/>
    Portions copyright © 1996 UserLand Software
    <http://www.scripting.com/>
*/
 
/*Includes*/
#include <ucmd.h>
#include <appletdefs.h>        /* for some string and Handle routines*/
#include <appletstrings.h>
#include <appletmemory.h>
#include <iac.h>
 
/*Defines*/
/* mostly omitted, but this one is relevant... */
#define deletelistitemtoken 'dlis'
 
/*Prototypes*/
/* omitted */
 
/*Functions*/
/* mostly omitted */
 
void deletelistitemverb (void) {
 
    long i = 1, ct = 1;
    long index;
    Handle hlistitem;
    AEDescList list, returnedlist;
 
    IACnewlist (&returnedlist);
    if (!IACgetlistparam ((OSType) keyDirectObject, &list))
        return;
    if (!IACgetlongparam ((OSType) 'indx', &index))
        return;
    while (true) {
        if (!IACgettextitem (&list, i, &hlistitem))
            break;
        if (i != index) {
            IACpushtextitem (&returnedlist, hlistitem, ct);
            ct++;
            } /*if*/
        else
            disposehandle (hlistitem);
        i++;
        } /*while*/
    AEDisposeDesc (&list);
    IACreturnlist (&returnedlist);
    } /*deletelistitemverb*/
 
void UCMDmain (void) {
    switch (IACgetverbtoken ()) {
        case deletelistitemtoken:
            deletelistitemverb ();
            break;
        case getlinkstoken:
            getlinksverb ();
            break;
        case listinstringtoken:
            listinstringverb ();
            break;
        case listtostringtoken:
            listtostringverb ();
            break;
        case stringtolisttoken:
            stringtolistverb ();
            break;
        case striphtmltoken:
            striphtmlverb ();
            break;
        default:
            IACnothandlederror ();
            break;
        } /*switch*/
    } /*UCMDmain*/

The main routine uses IACgetverbtoken() to learn what the third parameter to appleEvent() was, and then just switches on that, performing the desired command (and calling IACnothandlederror() if it doesn't recognize the command). deletelistitemverb() uses IACgetlistparam() and IACgetlongparam() to obtain the parameters by name, IACgettextitem() to run through the input list and IACpushtextitem() to create the output list, and IACreturnlist() to package the output list as the value to be returned.

Like XCMDs, UCMDs can call back to Frontier while they are executing, to ask it to evaluate any valid UserTalk expression, by means of a supplied runscript() function. Use of this function is illustrated by a couple of the examples included in the SDK.

Like XCMDs, a UCMD compiled as a resource file can simply be dropped onto Frontier's icon; it will then be imported into the database. It is up to the programmer to write glue.


1. However, people have written utility shells that make writing XCMDs much easier; these are available on the Internet. Particularly recommended is Mark Hanrek's shell, available from ftp://iw.cts.com/public/InfoWorkshop/Shareware/. Also, because of the long history of XCMDs, there are commercial utilities that make writing them much easier than is shown here; with these utilities, you can write your XCMD in BASIC or pseudo-HyperTalk and have all the gory details taken care of for you.

2. Thanks to Roger Brown, Moonrise Software, internews@valley.net, http://www.dartmouth.edu/~moonrise, for permission to quote from his code.


TOP UP: Reference NEXT: Operators

This is Frontier: The Definitive Guide by Matt Neuburg, unabridged and unaltered from the January 1998 printing.
It deals with the freeware Frontier 4.2.3; for commercial Frontier 8, look here, and for the inexpensive Radio UserLand, look here. See my Web site for information.
Those wishing an offline or printed copy may purchase the book, which is still in print.
Copyright 1998 O'Reilly & Associates, Inc. ALL RIGHTS RESERVED. Reprinted with permission.