![](/file/12652/www.mactech.com.tar/www.mactech.com/sites/all/themes/custom_front/images/you_are_here_red.gif)
![](/file/12652/www.mactech.com.tar/www.mactech.com/sites/default/files/beta-site.gif)
Speeding Up whose Clause Resolution In Your Scriptable Application
Greg Anderson
![](/file/12652/www.mactech.com.tar/www.mactech.com/articles/develop/issue_24/anderson2.gif)
The Object Support Library provides convenient mechanisms for scriptable applications to support complex expressions that may return multiple results (such as every item of container "b" whose name contains "a"). However, the performance of applications that rely on the default behavior is nowhere near what it could be if the application took on some of the work itself. This article shows you how to gain ten- to a hundred-fold increases in the performance of whose clause resolution in your scriptable application. If your application is not yet scriptable, you'll find that the foundation classes presented in this article do most of the work required to support scripting.
One of the greatest strengths of AppleScript is its built-in ability to do complex operations on groups of objects in a single line of script. For example, suppose you have a set of shapes in a scriptable drawing program, and you'd like to change the color of all the red shapes to green. In conventional programming languages, you'd need to write a loop that iterates over each object in the set, tests to see if its color is red, and then does a "set color to green" command for each red object that was found. Using AppleScript, you can do the same operation with the single statement set color of every shape whose color is red to green. In that statement, every shape whose color is red is called a whose clause, and it's the inclusion of whose clauses that makes AppleScript the powerful language it is.
You may at first doubt that using a whose clause is much better than writing the equivalent script with a loop. After all, the direction of modern processor design has been toward simplicity of the instruction set; RISC chips are able to gain incredible performance improvements by doing optimizations that aren't possible in CISC chips. Also, when all is said and done, the whose clause must finally execute the same loop-and-compare algorithm that you'd be forced to use if you wrote the script with the basic flow-of-control script commands, such as do-while and if-then.
Using a whose clause is, however, much more efficient than the alternative. AppleScript is based on the client/server paradigm: typically your script, the client, will be running in one application (usually the Script Editor or a script saved as a miniapplication), with the application being scripted acting as a server. In this situation, each script command that's directed at the scriptable application needs to be transferred between the two applications. A whose clause is a single script command, but with the loop approach many commands would need to be sent. Furthermore, AppleScript allows the scriptable application to reside on a different machine than the application running the script; if your script is running on a machine in Cupertino, California, and the server is on, say, Mars, reducing the number of round-trip messages would have a profound impact on the performance of the script. Remember, you can currently get only about 30 round-trip Apple events per second, so even if you aren't sending data to Mars, you'll still do a lot better with fewer events than with many.
There's another, similar reason that using whose clauses is superior to the equivalent loop-based script: AppleScript compiles scripts into byte codes that are interpreted during execution, whereas the individual script commands (once interpreted) are processed by a scriptable application typically written in a language that's compiled into machine code (be it 680x0 or PowerPC(TM)). The loop-and-compare script will execute several lines of script for every item that's compared, whereas the whose clause is but a single line of script that triggers processing in a compiled application. It should be quite clear which will take less time to execute.
The Object Support Library (OSL) -- the library that provides the API you use to make your application scriptable -- enables your application to support whose clauses without requiring you to write a lot of additional code. You only need to provide an object-counting function and an object comparison function, and the OSL can resolve whose clauses for you. Since supporting whose clauses allows script writers to write more efficient scripts, you should always do at least this much. However, there are two other features of the OSL that can vastly increase the performance of scriptable applications but are often ignored by application writers: whose clause resolution (a way for your application to find the objects that match a whose test without using the OSL) and marking (a mechanism for efficiently handling collections of objects, such as those satisfying a whose clause). Using whose clause resolution, with the help of marking, will enable you to get the most out of your scriptable application. Resolving whose clauses can be a bit tricky, but with a little help from this article, you'll be on your way in no time.
If your application is not yet scriptable, you'll find the sample code included with this article (and on this issue's CD) to be invaluable in getting you up and running -- particularly since it contains a lot of reusable code.
AN OVERVIEW OF THE OSL
Good descriptions of the OSL can be found in the develop articles "Apple Event Objects and You" in Issue 10 and "Better Apple Event Coding Through Objects" in Issue 12. If you need a quick review of the OSL and you don't feel like putting down this issue of develop to dig through your back issues, read on. If you can already generate tokens and resolve object specifiers in your sleep, by all means skip ahead to the next section.When AppleScript is processing a script command such as delete paragraph 2 of document "sample", it converts the command into an Apple event which it sends to the scriptable application that's referenced by the script. The Apple event's event class and message ID together specify the verb of the operation being performed -- in this case delete. The object being operated on is passed in the keyDirectObject parameter of the Apple event, which is called, naturally enough, the direct parameter of the event.
The direct parameter is almost always an object specifier -- a descriptor of type typeObjectSpecifier -- although in some cases it may be something else. For example, in addition to object specifiers, the Scriptable Finder accepts alias records and file specifications in the direct parameter of events sent to it. If the direct parameter of an event is not of type typeObjectSpecifier, you're on your own to convert it into some format that's understood by your event handler. For descriptors that are of this type, though, all you need to do is call the function AEResolve, and the OSL will step in and help your application resolve the object specifier -- that is, locate the Apple event objects it describes.
Object specifiers are resolved through object accessor callbacks that your application installs to allow the OSL to communicate with your application during object resolution. The accessor callbacks must take the description of the object requested by the OSL (for example, document "sample") and return a token that describes the object in terms that the application can understand (for example, a pointer to a TDocument object). Tokens are passed back to the OSL in an AEDesc, a structure that contains a 32-bit descriptor type and a handle. Your application has complete control over what it stores in the token, as long as the AEDesc is valid (that is, it was created with AECreateDesc).
When the OSL calls your application's object accessor callbacks, it always passes either a token that represents the containing object (which it got from an earlier call to one of your object accessors) or a representation of the default container of the application, which is also called the null container of the application. So, to resolve the object specifier paragraph 2 of document "sample", the OSL first asks for document "sample" from the null container. Then it asks the application to provide a token for paragraph 2 from the token the application provided in response to the request for document "sample". The token that the application provides for paragraph 2 is returned as the result of the AEResolve call; the application will presumably use this token to process the Delete event.
-
Resolving object specifiers is explained in Chapter 6 of Inside Macintosh:
Interapplication Communication. A figure illustrating the process of resolving
object specifiers is on page 6-6.*
MARKING
Inside Macintosh: Interapplication Communication describes marking as a mechanism whereby items to be operated on are marked with some flag during resolution (that is, from the callbacks made by the AEResolve function); then, during execution, each marked item is processed and the mark is cleared. As described, marking doesn't sound very interesting and appears to be useful only in fringe cases.Marking is actually very well suited for use as a general-purpose collection mechanism whenever the OSL needs to group tokens together to process an object resolution. For example, if the OSL is resolving the whose clause every shape whose color is red and there are multiple red shapes, the result of the call to AEResolve must be a collection of all the tokens that represent red objects. If your application supports marking, the OSL asks your application to create a special mark token to represent this collection. After your application provides the OSL with a mark token, the OSL will ask your application to add the tokens it provided for the red shapes to the mark token's collection. When AEResolve completes, the mark token is returned as the result of the resolution.
If your application doesn't support marking, the OSL will create collections of tokens for you by copying the data from your tokens into a descriptor list (an AEDescList). It calls the standard Apple Event Manager routines for creating descriptor lists, which copy the data out of the data handle of the AEDesc and then store the token data somewhere inside the data handle of the descriptor list; the descriptor type of the AEDesc is similarly encapsulated.
Dealing with descriptor lists of tokens can be inconvenient, particularly if your application already supports collections of objects in some other way. The OSL marking mechanism gives you the flexibility to handle collections in any way that's convenient for your application.
To support marking, you must pass the flag kAEIDoMarking to AEResolve and implement the three marking callbacks that are passed to AESetObjectCallbacks: the create-mark-token callback (called just a "mark-token callback" in Inside Macintosh), the object-marking callback, and the mark-adjusting callback. The create-mark-token callback doesn't need to do anything more than create an empty mark token. The OSL will dispose of this token as usual by calling your token disposal callback when the token is no longer needed. Listing 1 shows an example implementation of a create-mark-token callback.
Listing 1. Create-mark-token callback
pascal OSErr CreateMark(AEDesc containerToken, DescType desiredClass, AEDesc* markTokenDesc) { TMarkToken* markToken; markToken = new TMarkToken; markToken->IMarkToken(); markTokenDesc->descriptorType = typeTokenObject; markTokenDesc->dataHandle = markToken; return noErr; }The object-marking callback is passed a mark token created from the create-mark-token callback and some other token created by one of your application's object accessor callbacks. Your object-marking callback should add a copy of the other token into the mark token (or apply a reference count to the token being added), because the OSL will dispose of the token added to your collection shortly after calling your object-marking callback. Listing 2 shows one implementation of an object-marking callback.
Listing 2. Object-marking callback
pascal OSErr TAccessor::AddToMark(AEDesc tokenToAdd, AEDesc markTokenDesc, long markCount) { AEDesc copyOfToken; TMarkToken* markToken; // We know that the OSL will only give us mark tokens created with // our create-mark-token callback, but real code would do a test // before typecasting. markToken = (TMarkToken*) markTokenDesc.TokenObject(); // Add a copy of the token to the collection, because the OSL will // dispose of tokenToAdd after passing it to you. A reference- // counting scheme is good here. copyOfToken = CloneToken(tokenToAdd); markToken->AddToCollection(copyOfToken); return noErr; }The mark-adjusting callback is called to remove ("unmark") tokens from the collection. Oddly enough, its parameters specify which tokens in the range to keep; all tokens outside the specified range should be discarded.
Implementing the marking callbacks is trivial. The only real work involved in supporting marking is handling collections of tokens when they're ultimately received by one of your event handlers (handling Move events, for example). The amount of code required to handle the marking callbacks and maintain your own collections is minimal; in fact, the time you'll save by not having to hassle with descriptor lists of tokens will more than make up for the implementation cost. You'll find more information on handling collections of tokens later in this article. Don't put off marking as an optimization to be done later; incorporate it into the design of your application from the very beginning.
-
For more details on the marking callbacks, see Inside Macintosh:
Interapplication Communication, pages 6-53 to 6-54.*
WHOSE CLAUSE RESOLUTION
The only thing that a scriptable application needs to do to support whose clauses is provide an object-counting function and an object comparison function -- the OSL will do the rest of the work. When the OSL does a whose clause resolution, however, it has no choice but to iterate over every element in the search set, repeatedly calling your application's object accessor, object comparison, and token disposal callbacks. Huge performance gains can be realized if you resolve whose clauses yourself, because you'll avoid the overhead the OSL requires to make these callbacks.Passing the flag kAEIDoWhose to AEResolve tells the OSL that you'll resolve the whose clause yourself. The OSL calls your object accessor with the key form formWhose (see Listing 3). The key data is a whose descriptor -- that is, an AERecord that describes the comparison to be performed in the search. Your application should interpret the whose descriptor and test every element of the container token to see if it matches the specified criteria. If the whose descriptor is too complex for your application, you can return the error code errAEEventNotHandled from your object accessor, and the OSL will do the resolution for you with the default techniques. This is very useful, as it allows you to maximize the performance of the most common whose clauses, yet still support complex whose descriptors that are likely to be encountered only rarely.
Listing 3. Handling formWhose in the object accessor
pascal OSErr MyObjectAccessor(DescType desiredClass, AEDesc container, DescType /*containerClass*/, DescType keyForm, AEDesc keyData, AEDesc* resultToken, long /*hRefCon*/) { switch (keyForm) { // case formAbsolutePosition, and so on ... case formWhose: // TWhoseDescriptor is a class that knows how to interpret // a whose descriptor and test tokens for membership in the // search set defined by the desired class and the whose // descriptor. TWhoseDescriptor whoseDesc(desiredClass, keyData); // TTokenIterator is a class that knows how to iterate // over the elements of a token. TTokenIterator iter(container); for (iter.Reset(); iter.More(); iter.Next()) { AEDesc token = iter.Current(); if (whoseDesc.Compare(token) == kTokenIsInSearchSet) { // Add token to the collection stored in resultToken. AddTokenToResult(token, resultToken); } } break; } return noErr; }The astute reader will notice that the scheme presented in Listing 3 is very similar to the process that the OSL goes through to resolve whose clauses. There are still optimizations that could be made to speed up the resolution further, but we'll get to those later. To resolve whose clauses as shown in Listing 3, your application must be able to do the following:
- Iterate over the elements of any token.
- Determine class membership of any token.
- Compare properties of the elements of any token.
- Convert a whose clause into some internal representation usable by your application.
THE CONTENTS OF A WHOSE DESCRIPTOR
Earlier I claimed that a whose descriptor was an AERecord, but I lied. A whose descriptor is actually a descriptor of type typeWhoseDescriptor. Internally, a whose descriptor is stored just like an AERecord, but you can't extract its parameters unless you first coerce it to type typeAERecord. In Apple events parlance, this type of descriptor is called a coerced record; its basic type is typeAERecord, and its coerced type is typeWhoseDescriptor.The advantage of coerced records is that they allow clients of the Apple Event Manager (for example, the OSL) to define new descriptor types for AERecords that define the context in which the record will be used and specify (by convention) what parameters the client can expect to find inside it. The disadvantage is that it requires an extra memory allocation to coerce the descriptor back to typeAERecord before the parameters of the coerced record can be accessed. This is unfortunate, as one of the primary goals of performance optimization is to remove extraneous memory allocations; coercing the descriptor back to typeAERecord is part of the current design of the Apple Event Manager, though, so there's nothing we can do about it.
There are two parameters inside a descriptor of type typeWhoseDescriptor: keyAEIndex and keyAETest.
- The keyAEIndex parameter usually contains an enumeration whose value is kAEAll; this corresponds to the word every in the whose descriptor every item whose name contains "e". The other possible values are kAEFirst, kAELast, kAEMiddle, and kAEAny for whose clauses that request the first, last, middle, or any (random) item. The keyAEIndex parameter might also be of type typeLongInteger or typeWhoseRange, to indicate a single item or a range of items, respectively.
- The keyAETest parameter contains another coerced AERecord whose type can be either typeCompDescriptor or typeLogicalDescriptor. In either case, you must coerce the descriptor to type typeAERecord to access the parameters inside it.
Fortunately, logical descriptors are much simpler than comparison descriptors. A logical descriptor contains two parameters: keyLogicalOperator and keyLogicalTerms. The logical operator indicates the Boolean logic to apply on the contents of the logical terms: and, or, or not. The logical terms descriptor is, as you may have guessed, a list of descriptors whose type is either typeCompDescriptor or typeLogicalDescriptor. Figure 1 shows the contents of a whose descriptor that corresponds to the script every item whose name contains "e" and size is 0.
-
The contents of whose descriptors are described in Inside Macintosh:
Interapplication Communication, pages 6-42 to 6-45.*
Figure 1. Contents of a whose descriptor
PARSING WHOSE DESCRIPTORS
It may look like there can be a lot of different cases to handle in a whose descriptor, but it actually doesn't take too much code to convert a whose descriptor into a format that your application can understand. The next few listings show how this might be done. The code presented is somewhat simplified; it doesn't look at the keyAEIndex parameter of the whose descriptor (kAEAll is assumed), and it recognizes only very specific formats of comparison descriptors. Even this much of an effort is very useful, because it will cover about 90% of the whose clauses that your application is likely to encounter, and it's still possible to return errAEEventNotHandled and allow the OSL to take over for the rest. If you're expecting me to fall back on every develop author's favorite phrase, "This impossible task is left as an exercise for the reader," you're in for a surprise. The sample code on the CD will parse any valid whose descriptor passed to it and never falls back on the default handling provided in the OSL.The top-level routine, ParseWhoseDescriptor, simply extracts the keyAETest parameter and passes it to ParseWhoseTest, returning the resulting search specification. These two routines are shown in Listing 4. (A search specification is an application-defined object that knows how to test tokens for membership in the search set defined by the whose descriptor; see the sample code on the CD for the implementation of the search specifications used in these listings.) ParseWhoseTest examines the type of the descriptor (either logical or comparison) and then extracts the appropriate parameters and passes them to either ParseLogicalDescriptor or ParseComparisonOperator, whichever is appropriate.
Listing 4. Interpreting the contents of a whose descriptor
TAbstractSearchSpec* ParseWhoseDescriptor(TDescriptor whoseDescriptor) { TAbstractSearchSpec* searchSpec = nil; TDescriptor testDescriptor; whoseDescriptor.CoerceInPlace(typeAERecord); // Real code would call whoseDescriptor.GetDescriptor(keyAEIndex) // and at the very least check to see that its value is kAEAll, // and fail with errAEEventNotHandled if it isn't. testDescriptor = whoseDescriptor.GetDescriptor(keyAETest); searchSpec = ParseWhoseTest(testDescriptor); testDescriptor.Dispose(); return searchSpec; } TAbstractSearchSpec* ParseWhoseTest(TDescriptor whoseDesc) { TAbstractSearchSpec* searchSpec = nil; switch (whoseDesc.DescriptorType()) { case typeLogicalDescriptor: TDescriptor logicalOpDesc, logicalTerms; DescType logicalOp; whoseDesc.CoerceInPlace(typeAERecord); logicalOpDesc = whoseDesc.GetDescriptor(keyAELogicalOperator); logicalOp = logicalOpDesc.GetEnumeration(); logicalTerms = whoseDesc.GetDescriptor(keyAELogicalTerms); searchSpec = this->ParseLogicalDescriptor(logicalOp, logicalTerms); logicalOpDesc.Dispose(); logicalTerms.Dispose(); break; case typeCompDescriptor: TDescriptor compOperatorDesc, obj1, obj2; DescType compOp; whoseDesc.CoerceInPlace(typeAERecord); compOperatorDesc = whoseDesc.GetDescriptor(keyAECompOperator); compOp = compOperatorDesc.GetEnumeration(); obj1 = whoseDesc.GetDescriptor(keyAEObject1); obj2 = whoseDesc.GetDescriptor(keyAEObject2); searchSpec = this->ParseComparisonOperator(compOp, obj1, obj2); compOperatorDesc.Dispose(); obj1.Dispose(); obj2.Dispose(); break; } return searchSpec; }Since logical descriptor records can contain one or more terms, each of which is either a comparison or a logical descriptor record, ParseLogicalDescriptor calls back to ParseWhoseTest for each term in the record, creating a search specification for each (see Listing 5). If there's more than one term, ParseLogicalDescriptor compiles the resulting search specifications into a list and returns that; otherwise, it returns a single search specification for the single term.
Listing 5. Resolving logical descriptors
TAbstractSearchSpec* ParseLogicalDescriptor(DescType logicalOperator, TDescriptor logicalTerms) { TAbstractSearchSpec* searchSpec = nil, oneSpecification = nil; TDescriptor oneTerm; TSearchSpecList* specificationList = nil; FOREACHDESCRIPTOR(&logicalTerms, oneTerm) { oneSpecification = ParseWhoseTest(oneTerm); if (specificationList == nil) { if ((searchSpec == nil) && (logicalOperator != kAENot)) searchSpec = oneSpecification; else { specificationList = new TSearchSpecList; if (searchSpec) specificationList->Add(searchSpec); specificationList->Add(oneSpecification); searchSpec = nil; } } else { if (oneSpecification != nil) specificationList->Add(oneSpecification); } } if (specificationList != nil) searchSpec = new TLogicalSpec(logicalOperator, specificationList); if (searchSpec == nil) FailErr(errAEEventNotHandled); return searchSpec; }ParseComparisonOperator (Listing 6) first tests to make sure that the comparison operator is of the correct format. (Again, the code in this listing recognizes only a specific flavor of comparison operator; see the code on the CD for a more complete example.) If the operator passes that test, a new search specification representing the comparison is created and returned.
Listing 6. Parsing comparison descriptors
TAbstractSearchSpec* ParseComparisonOperator(DescType comparisonOperator, TDescriptor& object1, TDescriptor& object2) { TAbstractSearchSpec* searchSpec = nil; TDescriptor desiredClassDesc, containerDesc, keyFormDesc, keyData; if ((object1.DescriptorType() != typeObjectSpecifier) || (object2.DescriptorType() == typeObjectSpecifier)) FailErr(errAEEventNotHandled); object1.CoerceInPlace(typeAERecord); desiredClassDesc = object1.GetDescriptor(keyAEDesiredClass); containerDesc = object1.GetDescriptor(keyAEContainer); keyFormDesc = object1.GetDescriptor(keyAEKeyForm); keyData = object1.GetDescriptor(keyAEKeyData); if (containerDesc.DescriptorType() != typeObjectBeingExamined) FailErr(errAEEventNotHandled); if (keyFormDesc.GetEnumeration() == formPropertyID) searchSpec = new TGenericSearchSpec(keyData.GetDescType(), comparisonOperator, object2); desiredClassDesc.Dispose(); containerDesc.Dispose(); keyFormDesc.Dispose(); keyData.Dispose(); return searchSpec; }
ABOUT THE SAMPLE APPLICATION
The code presented up to this point is the easy part: implementing the marking and whose callbacks, parsing whose descriptors, and creating search specifications can all be done with a small amount of isolated code. Doing a search on a set of elements or performing a complex operation on a collection of tokens is a bit more involved, though, and requires a well-integrated framework that supports these concepts uniformly. You're in luck -- the sample application included on this issue's CD has such a framework.The sample application is called Scriptable Database. As its name implies, it's a database that's fully scriptable; in fact, it's usable only through AppleScript -- it has no user interface whatsoever. It's no coincidence that the model the database uses follows AppleScript's element containment model very closely. The Scriptable Database has documents that can be created, saved, and opened. Documents contain elements; elements have properties and data and may contain more elements. The database itself is completely generic; it doesn't care what the classes of the elements are or what properties they contain. To use it for a specific application, you'll have to edit Scriptable Database's dictionary, also called its AppleScript terminology extension ('aete' resource), to add the terms you'll need for your database.
-
AppleScript terminology extensions are described in Inside Macintosh:
Interapplication Communication, pages 7-15 to 7-20 and Chapter 8.*
- The Database subproject contains a standalone C++ object database. The code in this project is not discussed in this article, but you might find it interesting to peruse.
- The Base subproject contains pure C++ code that has no dependencies on any Mac OS or Toolbox routines, or any code from any other subproject in Scriptable Database.
- The Blue subproject contains C++ wrapper classes for Macintosh managers used by Scriptable Database.
- The Foundation subproject contains the foundation classes that Scriptable Database uses to implement scripting, and as such is the focal point of this article.
- The Scripting subproject contains the object accessors and event handlers needed to respond to the messages sent by AppleScript and the OSL.
- The Application subproject contains all the code that defines the Scriptable Database application; in fact, all the code specific to Scriptable Database is in this subproject. Every other subproject is also used in some other application that I've worked on.
ABOUT THE FOUNDATION CLASSES
The focal point of the foundation classes is the class TAbstractScriptableObject. This class was designed to serve as a base class, but it may also be mixed into an existing class hierarchy with multiple inheritance, as was done in the sample application (see the class TScriptableDocument). Any object derived from TAbstractScriptableObject can be used as a token for the OSL. Memory management of tokens must be done carefully; note that in most instances, tokens passed to the OSL are temporary and must be deleted when the token disposal callback is called. In other instances, though, it may be more convenient to use an existing object that the application has already created -- for example, a document object.Because of this, the token disposal callback must be able to unambiguously determine the difference between the temporary objects and those objects it should not delete, or disaster will result. Designators -- objects that represent some portion of another object -- are used for the temporary objects. The class TAbstractScriptableObject defines the methods CloneDesignator and DisposeDesignator, which do nothing in the abstract case. Designators override these methods to copy and dispose of themselves -- sometimes in conjunction with a reference-counting scheme.
As you might expect, the methods of TAbstractScriptableObject are designed to provide functionality that closely matches the features of the OSL. All objects derived from this class have elements and properties and can be sent events generated from an Apple event that the application receives. There are virtual methods in TAbstractScriptableObject that you can override to provide each of these types of behavior in your objects.
ELEMENTS OF A SCRIPTABLE OBJECT
A scriptable object exports its elements by providing an iterator object that knows how to iterate over the appropriate set of objects. There are two methods that return iterators, ElementIterator and DirectObjectIterator.virtual TAbstractScriptableObject* ParentObject(); virtual TAbstractObjectIterator* ElementIterator(); virtual TAbstractObjectIterator* DirectObjectIterator();The ParentObject method returns the object that this object is an element of. The element iterator iterates over the elements of the object, as was previously mentioned; the direct object iterator usually returns an iterator that knows about a single object -- the TAbstractScriptableObject that created it. If the object is actually a collection, however, its direct object iterator will iterate over every element in the collection. Once your application provides an iterator for the elements of its objects, the code in the foundation classes can handle most of the standard access methods for you. The access methods supported include formAbsolutePosition and formName, the default ordinals (all, first, last, and so on), and ranges of items (for example, items 1 through 10).
Your application's scriptable classes can support more specialized access methods by overriding the appropriate method:
virtual TAbstractScriptableObject* Access(DescType desiredClass, DescType keyForm, TDescriptor keyData); virtual TAbstractScriptableObject* AccessByUniqueID(DescType desiredClass, TDescriptor uniqueID); virtual TAbstractScriptableObject* AccessByOrdinal(DescType desiredClass, DescType ordinal);The first method, Access, is the general object-accessor dispatch method that calls the more specific access method appropriate for the keyForm parameter. You can override this method to define custom access forms -- for example, the Scriptable Finder defined the forms formCreator (to access an application by its creator type) and formAlias (to access a file or folder through an alias record). The method AccessByUniqueID provides a mapping from a unique ID to an object; override this method if your objects have unique IDs that scripts can use to access them. The method AccessByOrdinal handles ordinal access. All ordinals defined in the Apple Event Registry are supported by the implementation in the base class, so your application will probably never need to override AccessByOrdinal.
PROPERTIES OF A SCRIPTABLE OBJECT
Every scriptable object has at least a few properties that it must support. Almost all classes will have these five properties:- pName, since most objects have names
- pClass, pBestType, and
pDefaultType, since the Apple Event Registry requires that all objects support
these properties
- pContents, since the foundation classes handle Get Data and Set Data events by using this property
virtual DescType BestType(DescType propertyName); virtual DescType DefaultType(DescType propertyName); virtual Boolean CanReturnDataOfType(DescType propertyName, DescType desiredType);However, your application doesn't have to override these methods to provide information about every property of an object, since it's also possible (and more convenient) to describe the properties of an object in a property description table. For example, the properties defined in TAbstractScriptableObject are shown in the following property description table:
TPropertyDescription TAbstractScriptableObject::fPropertiesOfClass[] = { { pName, kReserved, typeChar, typeChar }, { pClass, kReserved, typeType, typeType }, { pDefaultType, kReserved, typeType, typeType }, { pBestType, kReserved, typeType, typeType }, { pID, kReserved, typeLongInteger, typeLongInteger }, { pIndex, kReserved, typeLongInteger, typeLongInteger } };Each entry in this table consists of four long words: the property identifier, a long word reserved for use by the class that defines the property, the property's best type, and the property's default type. The property description table is referenced through the class data table, so properties defined in one class are automatically inherited by any class that derives from it. The methods BestType and DefaultType return information from the property description table if an entry for the requested property can be found, and the method CanReturnDataOfType returns true if the desired type is the best type or the default type for a property.
-
See the files Object.cp and Object.h in the sample code for information on the
class data tables. The macros DeclareMinClassData and ImplementMinClassData are
used for classes that have no class properties; classes that do have class
properties use the macros DeclareClassData and ImplementClassData.*
virtual TDescriptor GetProperty(DescType propertyName, DescType desiredType, unsigned long additionalInfo); virtual void SetProperty(TTransaction* transaction, DescType propertyName, TDescriptor& data, unsigned long additionalInfo);The reserved long word can have nearly any value, but should not be greater than or equal to the constant kReservedRangeForPropertyInfo (see AbstractScriptableObject.h).
In addition to making the application's properties easier to implement, the property description table is key in supporting the "properties" property (which returns the current value of all the properties of an object, as specified by the property description table). It's also very useful for accessing properties of collections of tokens, as described later.
The transaction parameter in the SetProperty method must be provided by the caller but is not used by the foundation classes. It's provided as a mechanism whereby transaction-based applications (such as Scriptable Database) can make all changes under the auspices of a transaction object. Once all changes are made successfully, the transaction changes are committed back into the database. If anything goes wrong, the transaction is aborted and all changes are backed out. To the foundation class, TTransaction is just a named object that has no methods. The event handlers in the Scripting subproject use code from the Database subproject to create a transaction to pass to SetProperty (and other methods that can change the contents of the database), and commit or back out of the changes as appropriate after the event completes successfully or fails.
In some rare cases, it may be undesirable to include a property in the property description table, or it may be inconvenient to implement all of the functionality of a property strictly through the GetProperty and SetProperty methods. For example, Scriptable Finder has a trash property that returns a reference to the Trash object on the desktop. In such cases, your application should override the method AccessByProperty to return an appropriate scriptable object that represents the property:
virtual TAbstractScriptableObject* AccessByProperty(DescType propertyIdentifier);The object returned by AccessByProperty can be any sort of scriptable object; unlike properties described solely by the property description table, it can have properties above and beyond the minimum (for example, pClass, pBestType, and pDefaultType), and it can receive events (such as Empty Trash). Properties that are returned through AccessByProperty can also appear in the property description table, but if they do, the reserved long word should contain the magic constant kNeverCreateGenericProperty.
SENDING EVENTS TO A SCRIPTABLE OBJECT
Most scriptable applications use one of two dispatch techniques for handling Apple events: event-first or object-first dispatching. In event-first dispatching, an event is first dispatched to an event handler, which resolves the direct parameter and passes it a message appropriate to the Apple event being received. The advantage of event-first dispatching is that the parameters of the event are well known and can be extracted and passed to the object from the event handler, reducing the amount of duplicate code scattered through the various object event handlers. The disadvantage is that event-first dispatching requires a large number of very similar event handlers, and the message-passing API is often large (one method per event).Object-first dispatching attempts to solve this problem by providing a single event handler that blindly resolves the direct parameter of the received Apple event and passes the event to the resulting object. This technique is much simpler than event-first dispatching, requires a smaller API, and usually does exactly the right thing. But object-first dispatching doesn't always do exactly the right thing. For example, an Apple event that copies a set of objects to some destination container would send a different Copy event to every item in the source; what you might prefer is to have a single Copy event sent to the destination object, with the list of items to copy included as a parameter to the event. You'd never get the latter with object-first dispatching.
The Scriptable Database application uses a combination of event-first and object-first dispatching. Most Apple events are processed by a common event handler that resolves the direct parameter and passes the message along, in object-first dispatching style. Certain special events, however, such as Move, Copy, and Create Element, are processed in their own event handler, which can send a message to some object other than the direct parameter of the Apple event. The two primary methods that events are sent to are AECommand and CreateNewElement.
AECommand is defined as follows:
virtual TDescriptor AECommand(TTransaction* transaction, TAEvent ae, TAEvent reply, long aeCommandID, TAbstractScriptableObject* auxObjects = nil, long auxInfo = 0);Both the Apple event message and the reply are passed to the event handler, just in case they need to be accessed directly. The AECommand method should not put the command result into the reply directly, though, as it might not be the only object that's receiving this message. Instead, it should return the result as the return value of the method, and allow the event handler to collect all the results into a descriptor list and package them in the reply.
The meaning of the parameters auxObjects and auxInfo depends on the event handler that's processing the message; the aeCommandID parameter implicitly defines what the AECommand method should expect to find in these parameters. For example, in the Move and Copy events, the auxObjects parameter contains the set of objects that should be moved or copied. Providing a single method with general-purpose, multiple-definition parameters allows different scriptable applications that use the same foundation classes to define new events that have custom parameters without requiring them to change or expand the API of the foundation classes. This is one of the advantages of object-first dispatching that we definitely want to keep in our design.
The Create Element event is special enough to warrant giving it its own dispatch message:
virtual TAbstractScriptableObject* CreateNewElement(TTransaction* transaction, TAEvent ae, TAEvent reply, DescType newObjectClass, TDescriptor initialData, TDescriptor initialProperties, Boolean& usedInitialData, Boolean& usedInitialProperties);In most cases, classes that override CreateNewElement only need to look at the newObjectClass parameter, create a new object of that class, and return a reference to the newly created object. The event handler calls the SetData method of the new object by using the with data parameter from the Create event, and then calls the SetProperty method of the new object with each of the properties specified in the with properties parameter from the Create event. The initial data and initial properties for the new element are also provided as parameters to CreateNewElement in case they're needed at create time. If the usedInitialData or usedInitialProperties parameter is set to true, the event handler is inhibited from calling SetData or SetProperty, respectively, on the new object.
TOKEN COLLECTIONS
As previously mentioned, objects derived from TAbstractScriptableObject can be grouped into collections of tokens that can be passed around as a single object. The class that implements most of this functionality is TProxyToken, which is publicly derived from TAbstractDesignator. (A collection object is a temporary object created only to manage the collection of tokens and must be disposed of when the collection is no longer needed; therefore, a proxy must be a designator.) There are a number of different types of collections, each derived from the class TProxyToken.The classes of proxies provided in the foundation classes include the following:- TEveryItemProxy -- every element of an object
- TEntireContents -- every item in the entire deep hierarchy
- TMarkToken -- a collection of tokens accumulated from the marking callbacks or from resolving a whose clause
Sending a message to a proxy token usually does nothing more than pass the message on to each of its delegates; for example, the open selection script would pass an Open event to every selected item. In other cases, however, the proxy token handles the event itself. For instance, set selection to item 1 doesn't send a Set Data event to the selected items; instead, it deselects the currently selected items and selects the items in the direct parameter (such as item 1 in the previous example). The exact behavior of the proxy is determined by the concrete class (for example, TEveryItemProxy) that derives from the abstract class TProxyToken, but the proxy token does provide some mechanisms that can be used by its descendants to control the meaning of certain messages.
Properties in particular are handled in a special way by proxies. Some properties will apply to the proxy object itself, whereas other properties will refer to the delegates of the proxy token. For example, the script default type of selection should return the default data type for the selection object (which would be of type typeAEList), whereas default type of every item whose name contains "e" should return a list of default types, one for each item that matches the query every item whose name contains "e". There is no heuristic that can be used to determine which properties should apply to the proxy and which should apply to the proxy's delegates; the only solution is to list all the properties that should be sent to the proxy object in some way. In the foundation classes, this is done with the method PropertyAppliesToProxy:
Boolean PropertyAppliesToProxy(DescType propertyName);Each class that derives from TProxyToken should override PropertyAppliesToProxy and return true for those properties that should be processed by the proxy object and false for those that should be sent to the proxy's delegates.
MORE ON SEARCH SPECIFICATIONS
Previous sections of this article described how a whose clause was received by the object accessors of an application, converted into a search specification, and then resolved with a simple element iterator. Now that you're familiar with the capabilities of the foundation classes, we can go into the workings of the search specifications in a little more detail.As you may recall, there are two types of search specification: logical and comparative. The primary operation of a search specification is to take a token and return whether or not that item is a member of the set specified by the comparator. A logical specification contains a list of other specifications; it does nothing more than call the comparator method of each, and either logically AND or logically OR the results together. A comparative search specification needs to perform some test on a property of an object that was passed to it; it does so by calling the CompareProperty method of the object being tested.
virtual Boolean CompareProperty(DescType propertyIdentifier, DescType comparisonOperator, TDescriptor compareWith);The property identifier, the comparison operator, and the literal data to compare with were all extracted from the whose descriptor, as described previously. The default implementation of CompareProperty calls the object's GetProperty method and compares the result with the literal data by using the specified comparison operator. (You'll find a routine that compares two descriptors in the file MoreAEM in the Blue subproject of the sample application.) Note, however, that calling GetProperty involves a memory allocation to create a descriptor for holding the property data. Memory allocations are something best avoided in the inner loop of an operation that's supposed to progress quickly, so the performance of a whose clause resolution can be improved if you override CompareProperty and do common property comparisons without a memory allocation.
FURTHER OPTIMIZATIONS
Using the techniques described up to this point, your application can resolve whose clauses, and do so much faster than the OSL would. However, there are other optimizations that you can make to further improve performance. The techniques described so far perform better than the OSL for two primary reasons:- They limit the number of memory allocations needed, as much as possible.
- They reduce the number of callbacks that need to be made between the OSL and your application. This is particularly important if your application is PowerPC native but uses the emulated 680x0 OSL.
However, even for all of the performance gains that these techniques provide, whose clauses are still resolved according to the same basic algorithm used by the OSL. As anyone who has dabbled in computer information-science theory knows, it's often more advantageous to switch algorithms completely and put off fine-tuning until after the correct algorithm has been found.
Unfortunately, it's not possible to do any better than what we've already done in the general case (a direct linear search of the search space, comparing every item to the search specification in order). Doing a binary search isn't possible unless your search space happens to be sorted by your search key -- not very likely, and in any event it's impossible to know whether it is or not unless you have specific knowledge about the search space. Searching the entire contents of a deep hierarchy -- such as all the folders on a disk -- is one type of search space that can often be optimized.
In cases where the search space is well known, it's often possible to abandon the idea of direct iteration and use some other algorithm to search. For example, if you're writing code to search the entire contents of a disk, you would be much better off calling PBCatSearch, which walks through the entries in the catalog record in the order they happen to appear on the disk, ignoring the disk's hierarchy. This technique is so much faster than doing a deep traversal of the disk's catalog that doing a deep search of some subfolder on a disk is usually much better accomplished by searching the entire disk and weeding out the matches that aren't somewhere inside the search's root container. In cases where you have access to a search engine with characteristics similar to PBCatSearch, you should go out of your way to try to use it. Of course, this may well require yet another conversion of the search specification, but the performance gains will outweigh the initial cost. The foundation classes presented in this article have hooks that allow the incorporation of existing search engines to be incorporated into the process of resolving whose clauses.
When a whose clause is being resolved, the task of doing the search is delegated to the iterator object returned by the root of the search. Putting the method in the iterator rather than in the object allows different types of iterators to provide different search algorithms, each optimized to its own search space. The iterator returned by the TEntireContents proxy has a special implementation of AccessBySearchSpec; instead of using the implementation it inherits from TAbstractIterator, it uses a method called SearchDeep in the element iterator of the root object. The default implementation of SearchDeep does nothing more than compare every item in the deep hierarchy below each of its elements, and add those that match to the collection. This is really no different from what would happen if TEntireContents::AccessBySearchSpec just called through to Inherited::AccessBySearchSpec, but it does provide a hook enabling special iterators to insert their own search engines if they have a technique that will do deep searches faster than a straightforward deep iteration.
Listing 7 shows the default implementation of SearchDeep; note that it does a deep search on each of the elements of the iterator rather than simply a single deep search. The reason for this is that iterators aren't required to have a single root object that one could conceivably search deep from; once you have an iterator, the only knowledge at your disposal is the set of objects that the iterator "contains." The information as to where the iterator came from isn't available to every iterator, although some (such as TDeepIterator) do save a reference to it.
Listing 7. Doing a deep search
TAbstractScriptableObject* TDeepIterator::AccessBySearchSpec( DescType desiredClass, TAbstractSearchSpec* searchSpec) { TObjectCollector collector; TAbstractObjectIterator* iter = fRootItem->ElementIterator(); iter->SearchDeep(&collector, desiredClass, searchSpec); iter->Release(); collector.CollectorRequest(kWaitForAsyncSearchesToComplete); return collector.CollectionResult(); } void TAbstractObjectIterator::SearchDeep(TAbstractCollector* collector, DescType desiredClass, TAbstractSearchSpec* searchSpec) { TDeepIterator deepIter(nil); for (this->Reset(); this->More(); this->Next()) { TAbstractScriptableObject* elementToDeepSearch = this->Current(); deepIter.FocusOnNewRoot(elementToDeepSearch); for (deepIter.Reset(); deepIter.More(); deepIter.Next()) { TAbstractScriptableObject* token = deepIter.Current(); if (token->DerivedFromOSLClass(desiredClass) && searchSpec->Compare(token)) collector->AddToCollection(token); else token->DisposeDesignator(); token = nil; } if (elementToDeepSearch->DerivedFromOSLClass(desiredClass) && searchSpec->Compare(elementToDeepSearch)) collector->AddToCollection(elementToDeepSearch); else elementToDeepSearch->DisposeDesignator(); } }In Listing 7, rather than having the deep search iterator create and return a collection of tokens, a collector object is passed in and given the responsibility of making a collection from the results of the search, which it's passed one item at a time. This is done so that other parts of your scriptable application can call SearchDeep to do deep searches if they need to, and providing a collector object allows this code the flexibility to process the search results one item at a time, as they are found, rather than waiting for the entire search to complete.
Note the following line in Listing 7:
collector.CollectorRequest(kWaitForAsyncSearchesToComplete);A search engine that's hooked into this code path might, in a multithreaded application, execute asynchronously under its own thread. In these instances, the search engine needs a way to tell the collector that it's still running, and might call collector->AddToCollection with more search results at any time. The search engine does this by attaching a dynamic behavior object to the collection that understands the kWaitForAsyncSearchesToComplete message (see "What Is a Dynamic Behavior?"). When this message is received, the search engine's collector behavior must block the current thread of execution until the search engine completes its search.
WHAT IS A DYNAMIC BEHAVIOR?
A dynamic behavior is an object that can be attached to some other object to change its behavior dynamically at run time. Only objects that are specially written to accept behaviors can have behaviors attached to them, andonly certain methods of that object can be dynamically changed by the behavior object.
Methods that support dynamic behaviors contain additional code that first dispatches to any behavior attached to the object and then does the default action for that method. But the actual flow of control is somewhat different from that.
Suppose you have an abstract class TObject that supports behaviors, and an abstract class TBehavior that provides an interface for an object that can dynamically change the behavior of any TObject-derived object. If TObject has a method called Command that the behavior could modify, the implementation of TObject::Command would look like this:
TObject::Command() { TBehavior* behavior; behavior = this->FirstBehavior(); if (behavior) behavior->CommandDynamicBehavior(); else this->CommandDefaultBehavior(); } TBehavior::CommandDynamicBehavior() { TBehavior* behavior; behavior = this->NextBehavior(); if (behavior) behavior->CommandDynamicBehavior(); else this->Owner()->CommandDefaultBehavior(); }Given this definition for the Command method, some class derived from TBehavior could override the virtual method TBehavior::CommandDynamicBehavior, and call Inherited to execute the default action of the method it's overriding. This allows behaviors to do both pre- and post-processing. The cost to supporting behaviors is additional dispatch time, but the advantage is the powerful, dynamic extensibility of your objects. The use of a collector object and a dynamic behavior object allows the searching code to be flexible, optimized independently of other search engines, and reusable, even to other code that might not have exactly the same needs as the scripting code.
Also note the implementation of the functions TEveryItem::SearchDeep and TMarkToken::SearchDeep. Both of these call the function RecursiveSearchDeep, which calls SearchDeep on each of the elements of the iterator in turn. Without this special code path, a script such as (entire contents of every disk) whose name contains "mac" would end up using the slow deep-iteration search, and miss out on the optimized SearchDeep method of each disk. Calling the SearchDeep method of each disk independently enables different types of disks to have different types of search engines; for example, searches of remote disks might be optimized differently than searches of local disks, and not every type of volume supports PBCatSearch. In a framework that has provisions for optimizations, flexibility of design is extremely important.
WHAT WAS THIS ARTICLE ABOUT, ANYWAY?
It doesn't take too much work to vastly improve the performance of your scriptable application, and the techniques presented in this article will help you do just that. Resolving whose clauses yourself can speed up the execution of your event processing by a factor of ten to a hundred; a chance to gain that level of improvement is hard to ignore.AppleScript is one of the most compelling technologies that Apple offers -- the ability to record scripts, modify them, and play them back later puts powerful automation into the hands of programming novices. However, AppleScript is only as cool as the scriptable applications available in the marketplace. If you've written a scriptable application, thank you. If you haven't yet taken the OSL plunge, by all means read some of the material referred to in this article and dive in. (You might also want to take a look at the "According to Script" column that follows this article.) In either case, you should find the sample code on this issue's CD to be a very useful aid in implementing fast and complete scripting support in your Macintosh application.
- Inside Macintosh: Interapplication Communication (Addison-Wesley, 1993).
- "Apple Event Objects and You" by Richard Clark, develop Issue 10, and "Better Apple Event Coding Through Objects" by Eric M. Berdahl, develop Issue 12. These articles provide good descriptions of the OSL.
RELATED READING
Greg Anderson is enjoying the hot days of late summer as he writes this, but by the time this issue is in your hands, he should be back on the ski slopes earning his nickname, "Air Bear." Greg spends most of his skiing time looking for some protrusion to jump or fall off of while wearing his favorite polar bear hat. He sometimes works, too; he recently moved to Japan to work on international software for Apple Technologies in Tokyo.
Thanks to our technical reviewers Dan Clifford, Eric House, Arnoldo Miranda, and Jon Pugh.
![](/file/12652/www.mactech.com.tar/www.mactech.com/sites/all/themes/custom_front/img/search_text.gif)
- SPREAD THE WORD:
- Slashdot
- Digg
- Del.icio.us
- Newsvine