Read Me About MoreOSL

1.0b1

MoreOSL, or MOSL for short, is a source code library for implementing AppleScript support within your application. It has the following key features.

The MoreOSL library is targetted to run on a PowerPC with Mac OS 8.5 and above. It compiles and works best under Carbon, but it will compile and work with InterfaceLib.

Packing List

The MoreOSL folder of the sample contains the following items:

The remaining folders contain components of the DTS sample code library MoreIsBetter that are needed for TestMoreOSL.

Using the Sample

Getting Started

You probably want to start by launching the TestMoreOSL application. I've included a Carbon version in the sample package. You should install CarbonLib 1.0.2 or higher before running this application.

WARNING:
The debug version of TestMoreOSL logs extensively to BBEdit. When you run it, it will create a new window in BBEdit and spew lots of logging information to the window. You probably want to close any important documents you might have open in BBEdit before launch the application.

To run the test application, simply launch it in the Finder. The application will bring up a new, untitled document window. You can't manipulate the window using the mouse, so don't even try. TestMoreOSL only supports an AppleScript interface, it does not support the user interface.

Once TestMoreOSL is running, you can start scripting it. You might want to drop the TestMoreOSL application on Script Editor to look at its dictionary. Alternatively, you can open the "Test Script" script (in the TestMoreOSL folder) and run it. This script displays a list of test choices to run. You can run one of many different tests and demos by selecting them and clicking Run.

Seeing the Log Output

If BBEdit is running while you execute your tests, you will see a massive amount of logging information spewing out to an untitled document in BBEdit. This is a log of how TestMoreOSL is processing the incoming Apple events. You might want to stop the logging, either because you want to use BBEdit or because it's too slow, or for some other reason. There are a number of ways to do this.

  1. If you build the sample with debugging turned off, no log output will be generated (in fact, all the logging code is compiled out).
  2. If you launch TestMoreOSL and then close all the windows in BBEdit, the logging will be generated but won't be displayed.
  3. The following AppleScript command will turn off all logging in the debug version.
    tell application "TestMoreOSL"
        set debug to 0
    end tell

In order to save time, "Test Script" uses this last feature to disable all logging if you are running all tests. To find out what the various bits in the debug property mean, look for their definitions in "MoreOSL.h".

Use MoreOSL in Your Code

To find out more about how to use the MoreOSL library in your own code, see Adding Scriptability using MoreOSL.

Building the Sample

The sample was built using the standard MoreIsBetter build environment (CodeWarrior Pro 2 compiler with Universal Interfaces 3.3.1). You should be able to build the project in CodeWarrior Pro 5.3 without difficulty. To build it, up the project, select either the "Dbg-Carbon" or the "Dbg-PPC" target, and choose Make from the Project menu. Either target will build a TestMoreOSL application.

Both targets build to the same application name because that makes it easier to maintain the "Test Script".

Adding Scriptability using MoreOSL

Even with MOSL, adding scriptability to your application is still not easy. I suggest you take the following steps.

Design Your Dictionary

It's important that you design your AppleScript dictionary before you start coding. Think in terms of what your target user needs to do, not in terms of how your application works internally. A well designed dictionary actually makes the job of implementing scripting support easier, because you know a priori the list of classes, the elements within each class, and the properties of each class.

I'm not an AppleScript dictionary expert, so you can't take TestMoreOSL as a paragon of good dictionary design. Instead, you should probably start by looking at other, well designed and easily scripted applications. Also, you might want to look at some of the resources on the AppleScript for Developers web site. I find Cal Simone's classic develop Articles to be especially helpful.

Token Format

Once you know what classes you have to support, the next step is to think about tokens. When I started MOSL, understanding what would be a good token format was the hardest initial design obstacle. Fortunately, MOSL makes decision much easier for you.

MOSL uses a very simple design for its tokens, as described in "MOSLTokens.h". The basic format is a fix-sized record. The fields are defined and used by MOSL, except for the tokData field, which is specificially intended to store a pointer to the native object that the token represents. If you're using C++ (why aren't you using the PowerPlant or MacApp scripting support!?!), you can use this to store a C++ object pointer. If you're using C, you typically store a pointer to your 'object' equivalent, such as a DialogRef or WindowRef, or a pointer to the data structure that holds the object's state.

The meaning of tokData is class-specific, so you don't have to store the same data in the field for each class. For example, TestMoreOSL stores a DialogRef in tokData for all window-like classes (cDocument, cWindow, cNodeWindow, cAboutWindow) and stores a pointer to the underlying Node structure for nodes (cNode).

Remember, your token should never be the actual data itself, it should represent the data (that is, be a pointer to the data). This is critical to understand because the token is passed to both your getter and setter object primitives (see below).

Finally, it is possible for you to safely extend the MOSLToken structure to add extract fields. All of the routines that MOSL uses to operate on tokens are in the "MOSLTokens.c" file, so your changes should be isolated to that file.

First Code

The first code you should write is to call the InitMoreOSL routine. You must pass it the event table, the class table, the default event handler, and a error-to-string callback. The event table and class table are later sections. The other parameters are discussed here.

The error-to-string callback is necessary because, if an Apple event fails, MOSL automatically handles putting an error message into the reply. However, MOSL does not have access to localised resources, so there's no way it can put the correct localised error message into the reply. To get the localised error message, it calls the error-to-string callback. Your initial prototype need not include an error to string callback (TestMOSL doesn't) but a final product must.

Event Table

The next step is to construct your event table. This is an array of MOSLEventEntry structures which describe the Apple events to which your application responds. The exact structure is given in "MoreOSL.h", but there are some important things you need to know.

Class Table

The class table is an array of MOSLClassEntry structures that describes the classes that your application supports. Each class table entry contains the following information.

The property table is an array of MOSLPropEntry structures that describes the properties for the class. Each entry indicates the property name (actually, a four character code which the dictionary translate to a user-visible name) and whether the property is read-only. Both pieces of information are readily available from your dictionary. Your class's property table should be terminated by either a property of name kMOSLPropNameLast, or a property of name pInherits if this class inherits properties from another class. See "MoreOSL.h" for exact details on how this is set up.

The class event handlers table is an array of function pointers that MOSL calls when it receives an Apple event whose direct object is in your class. The class event handlers table is indexed by the same indices as used in the event table. Every class must have an entry for every event, although if the event makes no sense for a class you can set the entry to nil. Finally, MOSL provides a number of general class event handlers for handling the "count", "exists", "get data", and "set data" events in terms of your class's object primitives. In most cases, you can use these general event handlers for these events and just implement the object primitives.

The object primitives for a class are individual callbacks that MOSL calls under specific circumstances. Unlike the class event handlers, each object primitives has a different prototype. Like the class event handlers, MOSL provides a number of general object primitive routines that you can use to implement one primitive in terms of the others.

Implementing MOSL Callbacks

As you implement the various MOSL callbacks for your classes, you should keep the following in mind.

Default Event Handler

When you initialise MOSL, you pass it a class table that includes all of the classes of AppleScript objects in your application. When an Apple event arrives, MOSL first resolves the direct object and then calls the corresponding class event handler. This raises the question, what happens if the direct object isn't in the class table. Naively, you might expect that the event just fails. However, it is important to process certain events whose direct objects are never in the class table. For example, it is necessary for your application to see the "open documents" event, even though the direct object is a list of aliases and you don't have an alias class in your class table.

The default event handler is the solution to this problem. If MOSL processes an event and can't find its direct object in the class table, it passes the event to the default event handler. For an example of how that handler should respond, see the DefaultAppleEventHandler routine in "TestMoreOSL.c".

Caveats

MOSL does not implement formRelativePosition.

MOSL does not support properties, operators, and elements for built-in types. For example, if you ask for length of name of window 1, MOSL will fail. Notably,this also fails in the Finder. I consider the requirement that all applications support all properties, operators, and elements of built-in types to be a bug in AppleScript [2444537]. The accepted workaround is that the script developer must get the relevant property and then apply the operation inside AppleScript. MOSL should generate a better error number when it fails, however.

In a similar vein, MOSL does not provide any support for properties or elements whose values are records or lists. In the MOSL philosophy, if an object has a property whose value is a record, you should make it a reference to another object, and if an object has a property whose value is a list, you should make it an element of the object. I've received conflicting advice as to whether this is the right approach. Let me know what you think.

On the non-Carbon built, many of the window property accessors implemented in "MoreOSLHelpers.h" fail on Mac OS 8.5 through 8.6. This is because, on those systems, the Window Manager routine GetWindowAttributes only works for windows that were created using CreateNewWindow. The problem is manifest in TestMoreOSL in that the script below fails with a paramErr. I haven't had time to implement a substitute routine to call on those system versions. It is not a high priority because this routine works properly on those systems if you call it from Carbon, and I expect the majority of MOSL clients to be Carbonated.

tell application "TestMoreOSL"
    get zoomed of window 1
end tell

The string comparison implementation for Carbon relies on AppleScript's ability to coerce internation text (typeIntlText) to Unicode (typeUnicodeText). This feature was added in AppleScript 1.3.2 (Mac OS 8.5). If your application runs under Carbon on Mac OS 8.1, you will have to either install your own coercions handlers or revert to using the non-Carbon string comparison implementation.

TestMoreOSL does not handle the entire 'core' suite. The missing constructs, listed below, are unimplemented mostly because they make no sense for the test application. However, it's hard to guarantee that MOSL is correct and complete without implementing them all.

MOSL chooses to return the best type for indexed elements. For example, if you ask TestMoreOSL for every window and there is one about window and one node window, you will get back a list {about window id 128 of application "TestMoreOSL", node window id 150 of application "TestMoreOSL"} as opposed to {window id 128 of application "TestMoreOSL", window id 150 of application "TestMoreOSL"}. This seems like the right thing to do and it is in line with other scriptable applications that I tested with. This design decision effects how I handle the index property for windows. Because I required that index of every window return a continuous range of numbers, I require that the index property be the index of the window within the entire window list, not the index of the window within its class of windows. This means that, given the above example, index of node window 1 is not equal to 1. Most folks who I talk to think that this is a reasonable compromise.

The node tree within a node window is read-only and is not actually stored in the document file. I may change this eventually, but only if it's necessary to illustrate some AppleScript concept.

I have no yet run a leak check on MOSL. My coding style acts to prevent leaks, but I won't claim that MOSL is leak free until I've actually checked.

Nested whose clauses (for example, ((nodes whose name contains "2") of nodes whose name contains "1") of node window 1) yield weird errors. I haven't had time to investigate this yet.

MOSL does not yet implement the extra error parameters in an error reply event (such as the "partial results" parameter).

How it Works

Scripting Strategies

There are two basic characteristics of any scripting implementation:

MOSL implements object-first dispatching. For each event in the event table, MOSL registers the same Apple event handler (MOSLAppleEventHandler in file "MoreOSL.c"). This Apple event handler gets the direct object of the event and resolves the object. This results in either a single token or a list of tokens. For a single token, MOSLAppleEventHandler calls DispatchEvent to look up the token's class in the class table and call the corresponding class event handler. For a list of tokens, MOSLAppleEventHandler iterates over the list, extracting each token, calling DispatchEvent for each, and assembling the results into a list.

This approach stands in contrast to event-first dispatching, where each Apple event is processed by a separate handler. In general, event-first dispatching requires more code, whereas object-first dispatching is more table driver. Object-first dispatching also maps well to the native object format for applications that are programmed in an object-oriented style.

MOSL implements deep object resolution. There are two ways to interpret a script like file 1 of every item. In a deep resolution implementation, this produces a list that contains the first file of item 1, followed by the first file of item 2, and so on. If item X is a file, and thus doesn't include child files, the corresponding list item is the special value missing value. In a shallow object resolution implementation, this same object specifier yields a single object which is the first file in the list of items.

Both approaches are internally consistent and various scriptable applications use each approach. For example, AppleScript and the Finder both implement shallow resolution, whereas the AppleScript engineering team recommends deep resolution. MOSL uses deep resolution not because it was easier (in fact, at various stages of the development process, I managed to implement both forms!) but because I consider it more powerful. If you look at the above example, you'll notice that the shallow resolution implementation yields the same results as file 1, whereas the deep resolution implementation yields different, and possibly more useful, results.

Resolution Implementation

MOSL's object resolution is done by the routine RecursiveResolve (in "MoreOSL.c"). This routine's core algorithm is shown below.

on RecursiveResolve obj
    if obj is a list then
        set result to empty list
        for each element in obj
            set resolvedObj to (RecursiveResolve item)
            if resolvedObj is a list then
                append each element of resolvedObj to result
            else
                append resolvedObj to result
            end-if
        end-for
        return result
    else
        return AEResolve obj
    end-if
end RecursiveResolve

This algorithm has a number of consequences:

Whenever AEResolve is called, the Object Support Libraries invokes MOSL's various callbacks.

Note:
Registering the ClassOSLAccessorProc with a distinct refCon for each class allows OSL to do the mapping from class ID to class table index using its fast built-in hash tables. Doing this lookup inside ClassOSLAccessorProc would either be slower or require me to reimplement my own fast lookup mechanism.

Class OSL Accessors

MOSL registers the ClassOSLAccessorProc object accessor once for each class in the class table, with the refCon set to the class's index in the class table. ClassOSLAccessorProc is a simple dispatcher routine. It establishes the class of interest (by casting its refCon to a MOSLClassTableIndex) and then examines the incoming form parameter and calls one of the following routines.

formUniqueID

ClassAccessorByUniqueID

formName

ClassAccessorByName

formAbsolutePosition

ClassAccessorByAbsolutePos

formPropertyID

ClassAccessorByProperty

formRange

ClassAccessorByRange

Of these, ClassAccessorByUniqueID and ClassAccessorByName are simple wrappers around the class's "accessByUniqueID" and "accessByName" object primitives. ClassAccessorByAbsolutePos is somewhat more complex. It calls the class's "counter" object primitive to determine the number of elements within the object, then processes the supplied index (for example, a positive index refers to elements counting from the beginning, whereas a negative index refers to elements counting from the end) and calls the "accessByIndex" object primitive with a positive index number.

The most complex case for ClassAccessorByAbsolutePos is where the selection data is of typeAbsoluteOrdinal and value kAEAll. In this case, ClassAccessorByAbsolutePos is responsible for returning a token that includes all elements of the object. It does this by creating an AEDescList, repeatedly calling the "accessByIndex" object primitive to get the tokens for each element of the object, placing those tokens in the list and returning the list as the token.

ClassAccessorByProperty is surprisingly simple. The incoming parameter is a token that represents an object. [Attempts to get properties from properties are handled by the pseudo-class accessor for typeProperty.] The result must be a token that represents a property. All the routine needs to do is change the tokType field of the MOSLToken structure to typeProperty and the tokPropName field to be the property name (four character code). It also checks that the class actually includes the property name in its property table. This ensures that object resolution for bogus property names comes to a quick halt with a meaningful error code.

ClassAccessorByRange is surprisingly complex. The basic algorithm is shown below.

on ClassAccessorByRange containerObject, selectionData, desiredType
    get bounds1 from selectionData using keyAERangeStart
    get bounds2 from selectionData using keyAERangeStop
    AEResolve bounds1
    AEResolve bounds2
    coerce bounds1 to its base type
    coerce bounds2 to its base type
    set state to kLookingForFirst
    for each element in containerObject
         coerce theElement to base type
         if state is kLookingForFirst then
             if theElement is bounds1 then
                 set state to kLookingForSecond
             else if theElement is bounds2 then
                 set state to kLookingForSecond
                 set bounds2 to bounds1
             end-if
         end-if
         if state is kLookingForFirst then
             if theElement is compatible with desiredType
                 add theElement to result
             end-if
             if theElement is bounds2 then
                 set state to kDone
             end-if
         end-if
         if state = kDone then
             break
         end-if
    end-for
end ClassAccessorByRange

In words, this algorithm is:

Resolve the two boundary objects and coerce them to their base class. Iterate over each element of the container object. When you find the first boundary object, start adding compatible elements to the result. When you find the secondary boundary object, leave.

This whole process is implemented in terms of the class's "counter", "accessByIndex", and "coerceToken" object primitives.

Pseudo-Class OSL Accessors

The MOSL routine PseudoClassOSLAccessorProc is registered as an OSL object accessor three times, which it distinguishes between by consulting its refCon.

The first case is covered in the cFile Issues section. This section describes the other two cases.

Property Pseudo-Class Accessor

When OSL asks for a property from an object, the ClassOSLAccessorProc returns a token of typeProperty. When the script wants to get the value of a property, this token becomes the direct object of the "get data" event. However, if the value of the property is itself an object specifier (for example, in TestMoreOSL, every document has a node display property that is a reference to the node window for that document), it is possible for an object specifier to ask for properties or elements of the property.

The object specifier node 1 of node display of document 1 (in the TestMoreOSL application) is an example of this situation. As OSL tries to resolve this object specifier, the first thing it does is request document 1 of the application. The ClassOSLAccessorProc handles this, returning a document token (tokType is cDocument). OSL then requests the "node display" property of this token. Again, the ClassOSLAccessorProc handles this, returning a property token (tokType is typeProperty, tokObjType is cDocument). Finally, OSL attempts to resolve node 1 of this property token. The token type is typeProperty, so it is at this point that the PseudoCPropertyAccessor is called.

PseudoCPropertyAccessor handles requests for elements and properties of properties. The first thing it does is use the token's tokObjType field to work out what class of object the token is a property for. It then calls the class's "getter" object primitive on the token. The specification for this primitive is that, if the token is a property that is a reference to another object, it should return a token for that object. PseudoCPropertyAccessor now has a token for the object referenced by the property. It then calls AECallObjectAccessor on that token to recommence resolution based on the object referenced by the property.

The end result is that MOSL resolves objects and properties in object reference properties with a minimum of fuss for the client application.

List Pseudo-Class Accessor

MOSL regularly uses typeAEList as a token type. For example, if you ask for every document, MOSL will return the list as of documents as a typeAEList, where each element is a document token (tokType is cDocument). This approach is fairly standard (it is recommended in Inside Macintosh: Interapplication Communication) but it does have some consequences on for MOSL's object accessors. Specifically, because MOSL uses typeAEList as a token type, MOSL must provide an object accessor for typeAEList. The PseudoClassOSLAccessorProc routine (which is simply a wrapper for PseudoCListAccessor) is that accessor.

As an example of why this is necessary, consider the object specifier name of every document. The ClassOSLAccessorProc resolves the first part of the object specifier (every document) to a list (typeAEList) of document tokens (cDocument). OSL then attempts to access the "name" property of this list. It is not smart enough to do this itself, so MOSL must register an object accessor for typeAEList.

The basic implementation of the list pseudo-class object accessor is shown below.

on PseudoCListAccessor containerObject, selectionData, desiredType
    set result to empty list
    for each element in containerObject
        set resultItem to (AECallObjectAccessor item,selectionData,desiredType)
        if resultItem is a list then
            append each element of resultItem to result
        else
            append resultItem to result
        end-if
    end-for
    return result
end PseudoCListAccessor

The idea is that, if the container object is a list, PseudoCListAccessor breaks the list down into elements, calls the appropriate object accessor for each element, and puts the results into a list which it then returns. It uses the same list merging technique as RecursiveResolve to ensure that the resulting list is always flat.

This implementation works just fine formAbsolutePosition and formPropertyID. In fact, this is the core of the deep resolution implementation. For example, an object specifier of node 1 of every node window would cause PseudoCListAccessor to be called, and it would redispatch the request for node 1 to each element of the list and reassemble the results into a list. However, this basic implementation does not work well for formRange. The exact problem and solution are too convoluted to explain here, but are covered in the comments entitled "formRange for typeAEList" in the file "MoreOSL.c".

There are two other saliant points in the implementation of PseudoCListAccessor.

cFile Issues

I encountered a number of weird problems related to cFile while working on MOSL. Most of them have workarounds, but the problems are themselves worth noting.

Descriptor Comparison

MOSL registers the routine MOSLCompareProc as its OSL comparison callback. When I started MOSL, I was under the impression that this routine would only be called when OSL needed to compare two of my tokens; because OSL has no idea of my token format, it has to call me to do the job. This is not true. OSL calls the comparison callback whenever it needs to compare anything, be they object tokens, properties, or just data. OSL should, in my opinion, be smarter about this [2444551], but for the moment the MOSL comparison callback has to handle all of these comparisons explicitly.

MOSLCompareProc has two basic comparison methods.

The implementation of each method is fairly straightforward (comparing data descriptors is longwinded, but simple, except for strings). The tricky part of MOSLCompareProc is deciding which method to use. The basic algorithm is:

  1. If both sides are objects, compare the tokens
  2. If either side is a property, get the value of the property, coerce the other side to the same type, and compare the data.
  3. If either side is data, get the data, coerce the other side to the same type, and compare as data.

The comments inside MOSLCompareProc ("MoreOSL.c") explain this in greater detail.

Debugging Infrastructure

Debugging MOSL was hard. This was partly because of my inexperience with AppleScript and OSL, but also because the combination of OSL and MOSL is a big object-oriented framework, and debugging big object-orient frameworks is inherently hard. For example, when you step over AEResolve, you have no real idea which of your callbacks will be called and when.

My primary solution was logging. The MoreBBLog module allows easy logging to BBEdit, and MOSL makes extensive use of that facility. MOSL also registers coercion handles from all common types to typeText. This allows MoreBBLog to display more meaningful information in the log. Most of those coercion handlers are in "MoreBBLog.c" itself, but MoreOSL also registers a coercion handler (DebugTokenCoerceProc) to coerce tokens to text.

MOSL's logging is controlled by four bits in the gDebugFlags global variable.

By default the debug version of TestMoreOSL has kMOSLLogOSLMask and kMOSLLogDispatchMask set, but it also exposes these flags through its debug property so that a script can control the level of logging. The standard test script uses this technique to enable logging when you run an individual test but disable it when you run all of the tests, on the assumption that individual tests are run while debugging and all tests are run during regression testing.

The standard test script ("Test Script") proved to be an invaluable debugging tool in itself. The best feature of the test script is that it allowed me to make significant changes to the implementation (such as a conversion from Pascal to C -- a long story) with some assurance that I hadn't broken some obsure part of the implementation.

Another very useful debugging feature is the DebugOSLAccessorProc. In the debug version of MOSL, I register this object accessor as being capable of accessing anything from anything. If OSL attempts to access some object from a class I wasn't expecting, DebugOSLAccessorProc runs, logs the parameters, and returns an error. This way I can quickly spot failed object accesses in the log.

MOSL use asserts everywhere. This has helped me find numerous bugs.

MoreOSL String Comparison

Implementing support for AppleScript's string comparison operators is easy to do badly, but very hard to do right. The challenges include:

Some of these difficulties are evident even in the Roman script system. For example, assuming document 1 is called "Þnd" (the first character is the fi ligature (option-shift-5)), the following script snippets might produce different results because the application implements string comparison in terms of IdenticalText or CFStringCompare, which break down the ligature, but AppleScript's built-in string tables doesn't (even if you turn on expansion).

tell application "X"
    name of document 1 = "find"
end tell
 
tell application "X"
    get name of document 1
    result = "find"
end tell

Moreover, while testing this against CFString I discovered a bug [2442526] that causes CFString to break the identity:

(a equals b) implies ((a contains b) and (b contains a))

While this probably won't be a huge problem in practice, it is annoying.

While casting around for a solution (especially for the "contains" operator) I looked at how some other applications did this. That was a depressing exercise. The Finder's implementation of "contains", for example, does not even attempt to be two-byte friendly, nor does it address the possibility that IdenticalText may return true for text of different length (for example, "Þnd" vs "find").

Despite these obstacles, an application must implement string comparison in order to support the simplest AppleScript constructs (such as formName). So I had to come up with a solution. I chose two different paths depending on the target environment.

After this much pain, I thought it was important to file an enhancement request [2444555] asking for a better solution.

Credits and Version History

If you find any problems with this sample, mail <DTS@apple.com> as the first line of your mail and I'll try to fix them up.

1.0b1 (Mar 2000) was the first version released for internal review.

1.0b2 (Apr 2000) was the first release distributed to the general public. Contains a small number of minor fixes. The biggest functional change is that Apple events defined to have no reply (such as 'odoc') no longer show "current application" as their reply in Script Editor's log.

Share and Enjoy.

Apple Developer Technical Support
Networking, Communications, Hardware (slumming in scriptland!)

25 Apr 2000