One of the least-implemented powerful capabilities you can add to
your application is attaching and embedding scripts. In this column
I'll give you an idea of how to do this, and clear up some confusion
along the way.
The term attach has been used to refer to both attaching and embedding. Allow me to set
the record straight by offering definitions of the two terms as they apply to scripting.
Attached scripts are useful for two reasons. You, or your users (depending on what's
appropriate for your application), can do the following:
By allowing users to keep a menu of their favorite scripts, you enable them to build a
library of expanded functionality for your application. The Mac OS and Finder
accomplish this with the Automated Tasks submenu in the Apple menu. You can do this
with a Scripts menu that appears as the last (or next to last) of your application's
menus.
Here are the steps for implementing this attachable behavior:
Listing 1. Loading and executing a script from a file
FUNCTION RunAttachedScript(theAlias: AliasHandle): OSAError; VAR fileSpec: FSSpec; scriptRes: Handle; scriptDesc: AEDesc; scriptID, resultID: OSAID; myErr, ignoredErr: OSAError; savedRes, refNum: Integer; specChanged: Boolean; BEGIN (* Get the file specification corresponding to the menu item chosen. *) myErr := ResolveAlias(NIL, theAlias, fileSpec, specChanged); IF myErr <> noErr THEN MyErrorProc(myErr); (* Open the resource fork and grab the script resource. *) savedRes := CurResFile; refNum := FSpOpenResFile(fileSpec, fsRdPerm); IF refNum = -1 THEN MyErrorProc(-1); UseResFile(refNum); scriptRes := Get1Resource(kOSAScriptResourceType, 128); IF ResError <> noErr THEN MyErrorProc(ResError); (* Prepare and run the script. *) myErr := AECreateDesc(typeOSAGenericStorage, scriptRes^, GetHandleSize(scriptRes), scriptDesc); IF myErr <> noErr THEN MyErrorProc(myErr); myErr := OSALoad(gGenericComponent, scriptDesc, kOSAModeNull, scriptID); IF myErr <> noErr THEN MyErrorProc(myErr); myErr := OSAExecute(gGenericComponent, scriptID, kOSANullScript, kOSAModeNull, resultID); ignoredErr := OSADispose(gGenericComponent, scriptID); ignoredErr := AEDisposeDesc(scriptDesc); IF myErr <> noErr THEN MyErrorProc(myErr); (* Finish up. *) ReleaseResource(scriptRes); CloseResFile(refNum); UseResFile(savedRes); (* You might want to do something with the result. *) IF resultID <> kOSANullScript THEN MyDealWithResult(resultID); RunAttachedScript := myErr; END;
Before executing a script, you must establish a connection to a scripting component.
The easiest thing to do is to connect to the generic scripting component with
OpenDefaultComponent. When you're done, disconnect from the component with
CloseComponent. Depending on how you design your application, you can open this
connection and keep it open while your program is running, or you can open and close
the connection each time you load and execute a script. For more information on
choosing and connecting scripting components, see Inside Macintosh: Interapplication
Communication, Chapter 10.
Embedded scripts can be used in two ways:
Embedding scripts can be extremely powerful. For example, you can associate scripts
with elements of a form to supply a field's editing rules, or with a button to perform
calculations. Replace a script and you change the rules or the formula! Depending on
your particular application, you can use this technique yourself or allow users to do
their own replacement.
If you reserve this technique for your own use, you can revise your software simply
by replacing scripts with corrected or enhanced versions. Or, if you allow your users
to change the embedded scripts, your application becomes easily customizable: users
can modify or augment your application's capabilities simply by substituting scripts.
You could even ship your application with replacement scripts, which users can
substitute for default scripts that you provide.
There are three methods of retrieving embedded scripts from files, depending on
where they're stored. Regardless of which method you choose, it's important to
remember that your program should never try to interpret the bytes of a compiled
script. However, as long as you keep the bytes intact, you can do whatever you want
with them and the script will remain intact.
Aliases to script files. This is the same technique as described above for attached
scripts. This method is used primarily for maintaining a list of scripts. You'd use it,
for instance, if you kept a collection of scripts in a folder on disk. I don't recommend
this technique if the scripts are associated with actual interface elements, because the
links that aliases provide to the script files can too easily be broken.
In the document's resource fork. Storing the scripts as resources is convenient
because you can easily use your favorite resource editor to copy a script resource
from a compiled script or script application and paste it into the special application
file or the document. It also makes it easy to grab the scripts for loading and executing,
using the method shown in Listing 1 (though in this situation I'd suggest using an ID
number other than 128 for the script resource). The drawback is that your users can
get their hands on the script with their favorite resource editor.
In the document's data fork. Maintaining the scripts within the data for a
document is a more secure method, since it makes it harder for users to extract the
scripts. It's also more difficult for you, though, because you may have to keep track of
the location within the document's data, and then convert the script into the form
required for execution. You'll want to store three pieces of information: the
four-character ID 'scpt' (typeOSAGenericStorage), the length of the script data that
follows, and the script data itself. The ID isn't essential, but it may come in handy,
especially if there are other types of data present or if you load your document's data
sequentially.
There are many ways to keep track of multiple types of data in a document file. If you
have a lot of different types of data in the file, you can even develop a small database
for the data, complete with a directory, so that you can gain quick access to particular
types of data, including the script. A simpler way is to maintain the data in one long
stream, embedding the script data within the stream. If you know the location of the
script within the stream, you can just load and execute it when a user wants to run it.
One developer I know reads all the data in the data fork (including scripts)
sequentially when the file is opened, so that he doesn't need to keep track of the script's
location within the file. Listing 2 shows an example of loading script data from the data
fork of a document file.
Listing 2. Extracting script data from a document's data fork
FUNCTION RunEmbeddedScriptFromDataFork(theAlias: AliasHandle;
scriptLoc: LongInt): OSAError;
VAR
fileSpec: FSSpec;
scriptData: Handle;
scriptDesc: AEDesc;
dataType: DescType;
scriptID, resultID: OSAID;
myErr, ignoredErr: OSAError;
refNum: Integer;
scriptLen, readLen: LongInt;
specChanged: Boolean;
BEGIN
(* Open the file. *)
myErr := ResolveAlias(NIL, theAlias, fileSpec, specChanged);
IF myErr <> noErr THEN MyErrorProc(myErr);
myErr := FSpOpenDF(fileSpec, fsRdPerm, refNum);
IF myErr <> noErr THEN MyErrorProc(myErr);
(* Grab the data. *)
IF MemError <> noErr THEN MyErrorProc(MemError);
myErr := SetFPos(refNum, fsFromStart, scriptLoc);
readLen := sizeof(dataType);
IF myErr = noErr THEN myErr := FSRead(refNum, readLen, @dataType);
(* dataType should be typeOSAGenericStorage. *)
readLen := sizeof(scriptLen);
IF myErr = noErr
THEN myErr := FSRead(refNum, readLen, @scriptLen);
IF myErr = noErr THEN scriptData := NewHandle(scriptLen);
IF MemError <> noErr THEN MyErrorProc(MemError);
myErr := FSRead(refNum, scriptLen, scriptData^);
IF myErr <> noErr THEN MyErrorProc(myErr);
myErr := FSClose(refNum);
(* Prepare and run the script. *)
myErr := AECreateDesc(typeOSAGenericStorage, scriptData^,
GetHandleSize(scriptData), scriptDesc);
DisposeHandle(scriptData);
IF myErr <> noErr THEN MyErrorProc(myErr);
myErr := OSALoad(gGenericComponent, scriptDesc, kOSAModeNull,
scriptID);
IF myErr <> noErr THEN MyErrorProc(myErr);
myErr := OSAExecute(gGenericComponent, scriptID, kOSANullScript,
kOSAModeNull, resultID);
ignoredErr := OSADispose(gGenericComponent, scriptID);
ignoredErr := AEDisposeDesc(scriptDesc);
IF myErr <> noErr THEN MyErrorProc(myErr);
(* You might want to do something with the result. *)
IF resultID <> kOSANullScript
THEN MyDealWithResult(resultID);
RunEmbeddedScriptFromDataFork := myErr;
END;
The information in this column is not offered as a complete solution, but is intended to
get you moving with implementing attachability. There are many other issues
surrounding attachability that are worth exploring, such as getting time during script
execution, using attached scripts to allow users to tinker with some of the core
functionality of your application, and providing a consistent way for your users to edit
attached and embedded scripts. I plan to delve into these other issues in upcoming
columns.
Making your application capable of attaching or embedding scripts puts new power into
your users' hands, giving them unprecedented ability to develop custom solutions to
their problems. It's not hard to do, and the benefits are enormous. Do it today.
CAL SIMONE (mainevent@his.com, AppleLink MAIN.EVENT) Few people know it, but
before Cal was in the software business, he used to produce records (the musical kind)
in Washington DC and New York. At a time when computers were used mostly to make
robotic dance music, Cal was one of the first to painstakingly create "human"
performances in pop records with about 60 MIDI synthesizers and, of course, a
Macintosh. He now works toward a day when every application will be scriptable.*