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.
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.
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.
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:
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.
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.
Figure 1. Contents of a whose descriptor
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; }
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.
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.
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.
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.
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.
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.
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.
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.
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.
only 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.
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.
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.