Scriptability: A Bare-Bones Introduction
Kevin C. Killion, Stone House Systems,
Inc.
by Kevin C. Killion
© Copyright, 1996, Stone House Systems, Inc. All Rights Reserved.
Set aside your IM, AE Registry, and tech notes for the moment. Starting fresh, let's review the bare essentials you need to start making your app scriptable. Simple things should be simple, complex things should be possible, so let's ignore the rampant excesses of the AppleScript spec for now. We will introduce scripting with a simplified explanation of the concepts, and follow this up with implementation of a minimal, but very useful initial set of scripting functions. As an added bonus, we'll consider how to design your implementation for accessibility from BASIC as well as AppleScript.
Download the complete RTF document (stuffed binary)
An unprecedented number of new sets of APIs have been added to MacOS in the last few years. As of this writing, some 35+ different add-on technologies are provided by Apple in the MacOS SDK. Some of these technologies give developers the ability to implement exciting new features with a minimum of new coding effort. The Thread Manager, for example, enables an entirely different approach for handling long processing times without delaying the user, and it does so with only a few simple API calls. QuickTime has been another unqualified hit: simple to implement, and yet an incredible crowd-pleaser with customers.
Other new technologies released by Apple have not been so successful. Take AOCE: Many of the features it made possible were hard to explain, or passed minimal real benefits through to the end user. Nonetheless, documentation was vast and dense. It was hardly a surprise (to many developers at least) when Apple would say (in the January 5, 1996 issue of MacWeek) that it was "confusing to use" and that the entire API would be thrown out.
We might make an attempt to categorize MacOS add-on APIs with a cost/benefit approach. Every developer will have a different view, of course, depending on his or her own interests and target markets. For myself, I'd view the MacOS APIs this way:

This leads to a kind of technological triage. Some technologies are no-brainers: if an application can make any use at all of QuickTime, it should. Other toolkits are easy to dismiss: the documentation and methods for QuickDraw GX, for example, are so turgid and involved that only a high-end graphics program is likely to benefit.
For many developers, AppleScript belongs firmly in the upper right quadrant: high benefit at a high cost. AppleScript offers tantalizing power and flexibility, and could lead an application into a collaborative world in which the whole is much greater than the sum of the parts. On the other hand, the task of making an app scriptable appears daunting and formidable. Dave Mark says, "Many people (myself included) are intimidated the first time they try to take on the Apple Event Manager. In many ways, this task is as complex as when you first learned to work with the Mac Toolbox."
To start taking AppleScript seriously, we must first reinvigorate ourselves with the potential that AppleScript offers.
Beyond these, AppleScript provides the developer with additional benefits, which may not be as obvious:
It isn't too hard to understand why developers have ignored AppleScript in droves, despite the inherent appeal of what it could deliver.
The growth of AppleScript and scripting has languished over that cost benefit issue: The benefits are irrefutable, but the cost of development is often seen as insurmountable. But if implementation can be made manageable and affordable, the scriptability becomes a much more viable option.
AppleScript divides Mac developers into two camps: the mystical and the mystified. Some developers have bought into scripting completely, and are aglow with fervor for the Object Model, the OSL, the terminology resources and the natural language interface. The rest of this article is not for this enlightened group.
This article is intended for those who remain perplexed by scripting. I intend to provide a "start here" approach for adding scripting to your application. If your perspective of scripting matches the outline presented in the text up to now, keep reading.
A key step is to understand the difference between "scripting" and "AppleScript". When you make your app scriptable, you are doing nothing that has anything inherently to do with AppleScript! AppleScript is merely a language provided to access the scripting facilities of the system and applications.
The features and strengths of AppleScript often bear little relation to the code you write to support scripting. Moreover, there is nothing to prevent other languages from being developed to drive scripting. In fact, provision for such languages is built into the Mac scripting toolkits. Some folks love AppleScript, but not everyone. If you're in the latter group, then consider scripting anyway and hope that alternative scripting languages emerge.
Every application deals with a set of "things". An accounting system has clients, accounts, transactions, reports and many other elements. A model of some industrial process may have inputs, processing stages and outputs. A transportation management system may have parcels, containerized shipments, sources, destinations, warehouses and vehicles. (I'm bored to tears with Apple's fixation on text and pictures, so I like to use other examples.)
Of course, there is nothing novel about the concept of objects and properties. As programmers, we are used to the distinction between methods and instance variables in OOP. Even users have some sense of the distinction; for 12 years now they have dealt with screen "objects" and manipulated their "properties" through dialogs and commands.
(The original Xerox workstations crystallized this philosophy with a specific "Props" button on the keyboard. The user selected a screen object, and pressed Props to access a dialog to change the properties of an object. As time goes on, the brilliance of the Xerox design becomes even more evident. Much of what we admire in OpenDoc already was fundamental on the Xerox workstations of 1981!)
It's important to note that a crucial way of identifying the objects in an application is to tell whether they can be created or removed. A shipment can be removed from our transportation system, but we cannot "remove" its color. The shipment is an object, color is one of that object's properties.
Of the tasks we envision enabling through scripting, an enormous portion involve reading and setting the values of these properties. With just these two actions, the user can create custom reports, transfer selected information to other applications, tweak settings to improve some measure of performance, and do many other things.
Six-year-old on phone: "Do you want to come and play at my house?"
Classmate: "Where's your house?"
Six-year-old (puzzled): "Here!"
User: "Tell me the weight of the shipment."
Computer: "Which shipment?"
User: "This one!"
In an interactive, live program, direct pointing can identify the "thing" to be discussed. But that obviously isn't sufficient for a batch-style, hands-off operation. Moreover, there are plenty of situations in which a script may wish to talk to elements that aren't even part of the visible interface.
Consequently, "this one" is not a sufficient direction to a target. To identify this target, a descriptive address is needed, and it's natural to do that in terms of the context within a larger entity.
Parent of classmate: "Where's your house?"
Parent of six-year-old: "We're the third house on the block, on the block nearest the school, on the east side of the town."
Computer: "Which shipment?"
User: "The one stamped '47', in Bin C, on the upper shelf, of the truck that left at 10 am"
These addresses identify the object of interest with a series of successively larger contexts. Note that each context is an object in itself: house, block, side of town, the town, bins, shelves, trucks ñ these are the objects, the things, of their respective applications. And each step in establishing context describes how one object is referenced with respect to another.
Mac scripting documentation describes this relationship as containment, which is a very useful and descriptive name. However, we must quickly make note of what containment is not:
So, while "containment" is a handy term in our scripting efforts, be aware that, more than anything else, it is merely a way to establish context for identification of objects.
A parcel does not "contain" a weight, a spreadsheet cell does not "contain" a font size, and a client does not "contain" a name. Nonetheless, when we understand that the role of "containment" references in scripting has more to do with context and identification than it does with enclosure, certain similarities do emerge.
"Change the color." "Of what?" "Of the third row."
In this example, reference to the object identifies, in a very real sense, the context of the value to be changed. Understanding this subtlety will make it easier to understand the syntactic similarity of property references and object references in both AppleScript and other scripting languages. Later on, this will also help to explain why the same user routine winds up being useful for identification ("resolution") of both objects and properties.
To get our heads around where scripting is positioned, and why AppleScript sentences look like they do, let's do a thought experiment.
Let's imagine that AppleScript doesn't exist. Then imagine that we were given the task of designing a scripting language from scratch. Further suppose that we buy into the concepts of:
- objects as the entities within an application
- objects have properties to be examined and set
- objects are identified within contexts of other objects
Now, what structures might we place into our new language to implement these concepts?
To illustrate the task, let's suppose we have a simple tabular document, with a number of table rows that can be colored individually.
First, it seems natural to allow language variables to refer to properties of the objects in our application. A first try at devising such a scheme in BASIC, for example, might involve calling some subroutine that retrieves the needed value.mycolor = GetValue("color") ' "color" is a pre-arranged keyword
We immediately see that we can't talk about color without talking about of what, so we also need some way to denote context. Let's next imagine another available subroutine called GetObject that provides a reference to an object, when we ask for a specific entry on a list. We then could expand our GetValue routine to use this reference as a context. We then have:thisrow = GetObject("row", 4) ' get the fourth row ' ("row" is a pre-arranged keyword) mycolor = GetValue(thisrow, "color") ' get its color
We're making progress, but to get that row, we need to reference its context. A row only has meaning within the context of a document. In turn, the document is only one of several documents that may be open, but the context there isn't as clear. We might decide to denote this "top level" or "global" context with "NIL". Then:thisdoc = GetObject(NIL, "doc", 1) ' get the first document thisrow = GetObject(thisdoc, "row", 4) ' from the doc, get the fourth row mycolor = GetValue(thisrow, "color") ' from this row, get its color
In a very simplistic form, note that all of our references constitute a series of pairwise operations:
- within the "global" context, identify a document
- within the document context, identify a row
- within the row context, reference the color property
Now let's play language designer. Rather than clutter our programming with lots and lots of subroutine calls, let's devise some new syntax elements to streamline the coding.
Since everything we've done involves these pairs, we might quickly devise a syntax that combines the two elements of context and a "contained" object or property. A simple dot may serve this purpose:
{ context} .{ contained item}
For convenience, we might allow skipping any top-level "NIL" reference. With these changes, the previous listing could then be simplified as:thisdoc = doc(1) ' get the first document thisrow = thisdoc.row(4) ' from the doc, get the fourth row mycolor = thisrow.color ' from this row, get its color
Again, the pairwise nature of these references is evident. But for the user's convenience, we could allow these statements to be strung together, so that the intermediate variables aren't required:mycolor = doc(1).row(4).color ' get color of 4th row of doc #1
While this new, crisp syntax looks more complex, it's easy to imagine that processing of it still relies on those good ol' pairwise steps. (And, lo and behold, what we have just devised is not an imaginary language! With small variations, this is the syntax used in Microsoft's Visual Basic family of languages.)
Now let's take a look at another language, namely AppleScript:get the color of row 4 of document 1
Again, it's a long statement, but the fundamental notion is that processing this will involve a series of ever-finer context resolutions.
Although this is intended to be a "bare bones" introduction, we will require a few prerequisites before getting into the coding.
First, we will assume that you have already implemented the four "required" AppleEvents, namely, Open Application, Open Documents, Print Documents, and Quit Application. Supporting these events are already well-documented, and are comparatively straightforward. If by some chance you have not implemented the required events, start there. They are very well described, complete with code examples, in "Inside Macintosh: Interapplication Communication", pages 4-11 through 4-20.
As a by-product of implementing the required events, we'll also assume you have some notion of the idea of AppleEvents acting as containers to transport "clumps" of data. Since you needed to "install" your "handlers" for these required events, we'll also assume that you're familiar with those buzzwords.
To initialize the scripting facilities for your program, you must call the AEObjectInit routine in your application's initialization:err := AEObjectInit;
Of course, you should always check the result of this and all other calls that return an error parameter. In this text, we will skip that for clarity. However, error handling is implemented in the listing accompanying this article.
We are now ready to implement the first two crucial "handlers" to enable scripting. The "Set Data" and "Get Data" AppleEvents are the messengers that allow a user to retrieve and change properties of application objects. They are the heart of scripting.
In concept, their function is very simple. Let's start with the Set Data event (see figure, below). After you have created and installed your handler routine for Set Data, your application is prepared to receive these events. Each Set Data event contains two parameters: First, the parameter identified as "keyDirectObject" contains a reference to the property that is to be changed. Second, a parameter labelled "keyAEData" contains the new value for the property.
Figure 1: The Set Data event
Our code for this event involves retrieving these two parameters, figuring out what internal variable in our program corresponds to the property that has been specified, and then changing that variable's value. (For a complete implementation example of code used in this paper, see the listing, "Scripting.p". This sample listing is partially based on Apple's "Quill" example.)
Handling the Get Data event is similar (see figure, below). In this case, the AppleEvent that has been received has only one parameter, a reference to the object and property of interest. This is exactly the same as for the Set Data event. The big difference is that when we access the corresponding variable in our program, we retrieve its existing value, and send it back to the calling application. We do this by packaging up this value as a parameter and attaching it to the reply AppleEvent.
Figure 2: The Get Data event
So far, it hardly sounds difficult at all, does it?
We tell the system that we have Set Data and Get Data handlers by installing them as follows in our application's initialization area:err := AEInstallEventHandler(kAECoreSuite, kAESetData, @HandleSetData, 0, FALSE) err := AEInstallEventHandler(kAECoreSuite, kAEGetData, @HandleGetData, 0, FALSE)
Like all AppleEvent handlers, the calling sequence for our routines consists of an AppleEvent being received, a possible reply event, and a reference constant:FUNCTION HandleSetData (theAppleEvent: AppleEvent; reply: AppleEvent; refCon: LongInt): OSErr;
The reference constant makes it possible to handle several different kinds of events with the same handler. For example, you could conceivably have a single routine that handles both Set Data and Get Data by installing the same routine for both functions, and telling them apart with that constant. However, for code clarity, I'd recommend ignoring the refCon and coding each handler separately.
Within each of these handlers, we retrieve the direct object (the object and property to be accessed) as follows:err := AEGetParamDesc(theAppleEvent, keyDirectObject, typeWildCard, myDirObj);
We now have the reference to the property to be retrieved or set, contained in the variable myDirObj. This variable is declared as type AEDesc, which has mystical trappings to it but which really is nothing more than a handle with a 4-character label (a DescType) on it:TYPE AEDesc = RECORD descriptorType: DescType; dataHandle: Handle; END;
Ah, but what's in that handle? For initial scripting efforts, it is sufficient to state that this handle contains an encoded representation of the entire specification. For our figures, that specification is "font of row 1 of document 1", in a compact binary form. (No, it is not the literal text of this specification.)
This is where the fun starts. It might be possible to parse the contents of this handle ourselves, determining the structure and successive "containments" implied. Mull that over for a while, and think about how you would do that. Besides the effort involved in decoding and parsing the message, we would have to do a great deal of bookkeeping along the way.
Now recall our earlier lengthy discussion about context and containment. We stressed that every step of the resolution involved a pairing of a context and an object or property to be isolated from within that context. What if we could reduce the problem of identifying that direct object into a series of simpler requests of the context/object form?
HARD: decipher "font of row 1 of document 1"
SIMPLER: Do each of the following:
- find document 1
- find row 1 within it
- find the font of that
This process of breaking down a complex specifier into a reference of a specific object or property is called "resolution". The Mac scripting architecture makes resolution much easier by handling the bookkeeping chore for us. To do this, we are given a function called AEResolve. We call AEResolve, and hand it the complex specification. The system will make additional calls back to our application, consisting of a series of simpler resolution questions, like those listed immediately above. When AEResolve returns, we are given a reference (of our own design and in terms of the application's internals) that uniquely identifies the target object or property.
The sheer power of this approach is that AppleScript and the scripting engines within the system contain a great deal of leverage to manipulate complex expressions on their own, requiring our application to deal with only these simple context/object pairing questions.
If you've been paying close attention, you've noticed two leaps of faith in the above discussion. First, we said that the system calls back to the application to ask these pairing questions, but we have not yet said how. Second, we said that AEResolve returns a reference to some internal variable that is to be retrieved or set, but we have not yet said where that comes from. We will cover those omissions in the next sections.
For now, let's continue to operate on faith. We call AEResolve, and we retrieve a spec for the application internal to be accessed. This is another AEDesc, called newDesc in this sample. newDesc's handle contains a structure that is our own device for referring to goodies in the application. We extract this with a simple BlockMove, placing the result in a structured variable we call myToken:err := AEResolve(myDirObj, kAEIDoMinimum, newDesc); BlockMove(newDesc.dataHandle^, @myToken, myTokenSize);
The few statements we've examined so far are identical in both Set Data and Get Data. Now, these two handlers will diverge. Consider what each must do:
Set Data:
1. Retrieve the value specified for the application internals
2. Attach it to the outgoing "reply" AppleEvent
Get Data:
1. Extract the desired new value from the incoming AppleEvent
2. Set the correct application internal to this value
There is an obvious symmetry to setting and getting data elements. Wherever possible, it's desirable to handle such similar functions with the same code, to avoid them getting out of sync and to avoid largely redundant code. For example, many programmers like to have a single I/O routine for both reading and saving documents.
We will do the same here, bottlenecking both our Set Data and Get Data procedures through one routine. In each case, the bottleneck routine is passed our "token" reference, and the relevant AppleEvent (either the incoming or outgoing one).
For our Set Data handler, we give our handler the token and the incoming AppleEvent:VAR direction: (doSet, doGet); { ...} direction := doSet; err := DoTransferProperty(direction, myToken, theAppleEvent);The Get Data handler is passed the token and the outgoing reply AppleEvent:
VAR direction: (doSet, doGet); { ...} direction := doGet; err := DoTransferProperty(direction, myToken, reply);
We are now very close to having our application take its first steps towards scriptability. All we need to do now in our code is fill in the steps we've taken on faith. These missing steps are:
1) Devise a means to uniquely identify variables in our system, using "tokens".
2) Provide the callback routine that AEResolve will use to ask the context/object questions, returning a specific token.
3) Implement the DoTransferProperty routine.
Besides these code changes, there is one more step: we must create an 'aete' resource.
Much of the existing document and sample code for scripting correctly points out that the contents and usage of tokens are totally up to you. Unfortunately, they often then go on to define very convoluted token strategies for sake of illustration.
I have found that to get started with scripting, a very straightforward token design is really quite sufficient.
The tokens passed between the system and your application consist of AEDesc's which, as we've said, are nothing more than handles with four-character labels. What you put in that label, and what you put in that handle are up to you.
It is not inconceivable that a very simple application may be able to define all of its objects and their properties with a coding that uses the label alone, and leaves the handle at NIL. Suppose we have only the app itself, and a single tabular document with no more than 99 rows and columns (identify them as "A", "R", and "C"). Further suppose that no object has more than 9 unique properties, and we will let "0" represent the object itself. Then, we could use codings like this:R021 = the first property of row 2 R020 = row 2 itself A001 = property #1 of the application (some global setting)
That just shows how simple a token strategy may be. A more realistic token design will create a structured record identifying the object or property. In the accompanying listing, this is the token definition used:TYPE MyTokenType = RECORD myTokenCode: DescType; theObject: CObject; subReference: longint; isAProperty: Boolean; propertyCode: DescType; END;
Here is how I use each of these fields in this record:
myTokenCode: this is just a simple code to identify the kind of object being discussed. The code used here is only meaningful to the application itself, of course, so it can be anything.
theObject: this is a handy place to store the handle containing the most relevant object. For example, from TCL, this field could be set to the actual object of concern.
subReference: I defined this field for convenient referral of application elements which are not "objects" in the class hierarchy themselves. For example, I may have an object "client" which possesses an array of values representing billing dates. With theObject referring to Smith Company and subReference 3, I can resolve down to the third billing date for this client. If subReference=0, the token refers to the object itself.
isAProperty: This same token structure is used for both an object and the property it refers to. For an object, isAProperty=FALSE, for a property, it's TRUE.
propertyCode: another four-character code, of my own device, identifying the needed property, if isAProperty=TRUE.
I have found that this simple token design satisfies all of the needs I have encountered for basic scripting capability. Remember -- the specifics of the design are totally up to you.
Move on to part 2...
Return to contents...