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.
formRelativePosition
. MOSL also supports data
comparison for most revelant data types.
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.
The MoreOSL folder of the sample contains the following items:
MOSLToken
data type. This is
the simple token structure used by MOSL. You may want to add or
modify this structure for your application. See
Token Format for more details.
The remaining folders contain components of the DTS sample code library MoreIsBetter that are needed for TestMoreOSL.
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: |
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.
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.
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".
To find out more about how to use the MoreOSL library in your own code, see Adding Scriptability using MoreOSL.
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".
Even with MOSL, adding scriptability to your application is still not easy. I suggest you take the following steps.
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.
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.
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.
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.
kEventTable
in
"TestMoreOSL.c".
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.
MOSLGeneralAccessByUniqueID
in place
of your own primitive.
MOSLGeneralAccessByName
in place of your own
primitive.
formRange
support.
As you implement the various MOSL callbacks for your classes, you should keep the following in mind.
MOSLGeneralCount
,
MOSLGeneralExists
, MOSLGeneralGetData
,
MOSLGeneralSetData
) and object primitives
(MOSLGeneralAccessByUniqueID
,
MOSLGeneralAccessByName
) where possible.
AEGetParamDesc
with
typeWildCard
and then call
MOSLCoerceObjDesc
or
MOSLCoerceObjDescToPtr
to coerce the data to the
appropriate type. This ensures that commands whose arguments are
properties or objects (like the one shown below) work
correctly.
tell application "TestMoreOSL" set name of window 1 to name of window 2 end tell |
tokType
is
typeProperty
), your getter must return the value
of the property itself. If the property is a reference to
another object, the value must be the token for that object.
tokType
matches
the class for the "getter" object primitive), your getter must
return an object specifier for that token. To create the
container object specifier, your getter should construct a
token for the container and call the container class's getter.
See CNodeGetter
in "TestMoreOSL.c" for an example.
MOSLSetObjectProperties
routine. For an example of how to do this, look at the
CDocumentMake
routine in "TestMoreOSL.c".
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".
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).
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.
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:
RecursiveResolve
is always either a
single object or a flat list of objects. This ensures that
every node of every node window
produces a flat list,
rather than a nested list, which is in line with scripter
expectations.
Whenever AEResolve
is called, the Object Support
Libraries invokes MOSL's various callbacks.
MOSLCompareProc
-- This is a standard callback
that OSL calls to compare descriptors, both during whose clause
resolution and in response to the kAEEquals
Apple
event. It is described in detail in
Descriptor Comparison.
MOSLCountProc
-- This is a standard callback that
OSL calls to count objects within a container. MOSL implements
this callback in terms of the "counter" object primitive, which it
finds in the class table. The actual
implementation is quite straightforward.
ClassOSLAccessorProc
-- MOSL registers this
callback once for every class in the class table, with the refCon
set to the class' index in the class table. The callback is
responsible for accessing objects and properties within the
classes in the class table. See Class
OSL Accessors for more details.
PseudoClassOSLAccessorProc
-- MOSL registers this
callback once for each pseudo-class, with a refCon that uniquely
identifies the pseudo-class. A pseudo-class is a
class that OSL needs to access objects within, but which isn't
actually a class in the class table (the pseudo-class is not one
of the application's classes). Specifically, the pseudo-classes
accessors currently registered by MOSL are
typeWildCard
from typeAEList
,
typeWildCard
from typeProperty
, and
cFile
/typeAlias
from
typeNull
. See
Pseudo-Class OSL Accessors
for details.
Note: |
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.
|
|
|
|
|
|
|
|
|
|
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.
The MOSL routine PseudoClassOSLAccessorProc
is
registered as an OSL object accessor three times, which it
distinguishes between by consulting its refCon.
cFile
/typeAlias
from
typeNull
(implemented by
PseudoCFileAccessor
)
typeWildCard
from typeProperty
(implemented by PseudoCPropertyAccessor
)
typeWildCard
from typeList
(implemented by PseudoCListAccessor
)
The first case is covered in the cFile Issues section. This section describes the other two cases.
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.
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
.
AECallObjectAccessor
fails, the
special value missing value
is placed in the result.
For example, the result of the following script is a list whose
second element is missing value, because node 2 of node window 1
does not have any children.
tell application "TestMoreOSL" node 1 of every node of node window 1 end tell |
PseudoClassOSLAccessorProc
is for
formRange
and the desiredClass
is
cObject
, PseudoClassOSLAccessorProc
does
not call PseudoCListAccessor
; instead it just returns
the N'th descriptor in the list. This weird special case is
required to make AppleScript's repeat with
construct
work properly [2445795].
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.
cDocument
class, you must also add a
cFile
class to your suite. The cFile
class can be empty, but it has to be there to make AppleScript
compile properly. See the comments in "TestMoreOSLTerminology.r"
for more details. [2444517]
cFile
class, the construct
every document whose file is alias "Macintosh
HD:Test"
still doesn't work properly. See the comments in
"TestMoreOSLTerminology.r" for more details. [2444528]
save document 1 in
file "Macintosh HD:New File Name"
. This actually generates
a cFile
object specifier that confused MOSL because
cFile
is not in the class table. I resolved this
problem by adding cFile
to the set of pseudo-classes
handled by PseudoClassOSLAccessorProc
(which passes
the actual work to PseudoCFileAccessor
). When
PseudoCFileAccessor
is called to return a
cFile
object object within a container, it simply
reassembles the form, selection data, and container information
into an object specificer and then calls AECoerceDesc
to coerce that object specifier to an FSSpec
; the
coercion is handled by a built-in coercion handler.
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.
CompareDataDescriptors
)
MOSLCompareProc
)
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:
The comments inside MOSLCompareProc
("MoreOSL.c")
explain this in greater detail.
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.
kMOSLLogOSLMask
-- When set, this bit causes MOSL
to log all of its interactions with OSL. Specifically, all of
MOSL's OSL callback routines record that they have been called,
their parameters, and their results.
kMOSLLogCallbacksMask
-- When set, this bit
causes MOSL to log whenever it calls a client callback (class
event handler or object primitive). This output is primarily aimed
at folks who use MOSL and want to figure out what their callbacks
are doing wrong.
kMOSLLogGeneralMask
-- When set, this bit causes
MOSL's general class event handlers to log their actions.
kMOSLLogDispatchMask
-- When set, this bit causes
MOSL's core Apple event dispatcher to log its actions.
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.
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.
OSADoEvent
). Sleasy, but functionaly.
After this much pain, I thought it was important to file an enhancement request [2444555] asking for a better solution.
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