Script Servers

Script Servers

Advanced AppleScripters sometimes use what might be called “modular” scripting techniques. These are techniques for organizing scripts into separate but related files, mainly to simplify complex scripts and to avoid having to reinvent the wheel for every new scripting project. This tip first summarizes two popular modular scripting techniques, script libraries and script servers, then describes a way to combine them without sacrificing their power and flexibility.

Script Libraries and Script Servers

One popular modular scripting technique is to create a “script library,” which is a compiled script that collects in one file a number of related properties and handlers that can be loaded into other scripts whenever their functionality is needed. A script library is reusable code; it can be written once and used repeatedly thereafter in other scripts to reduce development time and effort. You use the Load Script command to load a script, then use a tell block addressed to the result to run any handlers in it.

Another technique is to create a “script server,” which is a stay-open script application that, like a script library, collects related properties and handlers in a single file. A script server differs from a script library in that it runs in the background as a stay-open application and makes its functionality available to other scripts without having to be loaded into any of them. You simply use a tell block addressed to the server application to call any handlers in it.

A script server is especially useful when a number of scripts on one computer need its properties and handlers at or near the same time. Only the one script needs to take up space in memory, instead of multiple copies of a script library loaded into several scripts at once. Also, time need be taken to load only the one script, by launching the server application, rather than multiple library files. You can even save that time by including the script server application in your Login Items in the Accounts pane of System Preferences. A script library, on the other hand, is useful when multiple scripts are developed for use at different times or on multiple computers and the only issue is reusability. They both offer other advantages, mainly making it unnecessary for you to rewrite and debug the same handlers over and over again for every script where you need them.

In the classic Mac OS, it was no small part of the benefit conferred by libraries and servers that they helped to avoid the infamous 32K limit on the amount of text that could be accommodated by some script editors. In Mac OS X, this is no longer a concern. Script libraries and script servers are nevertheless still convenient in Mac OS X, for the reasons given above.

How to Use Subroutines in Libraries that are Loaded Into Servers

It is possible to combine these two modular scripting techniques, creating a script server that incorporates one or more script libraries. Scripters have done this, for example, when a project could usefully be based on a script server, and the scripter has already collected a body of reusable script libraries relevant to the project.

It has been noted on the AppleScript mailing lists from time to time, however, that there appears to be a serious limitation on the use of libraries in script servers. The handlers in a typical script library are simple; each routine stands on its own, containing a few lines of AppleScript and making no calls to other handlers within the library. Libraries like this can be loaded into script servers without difficulty. But in a large library, it can save space to call subroutines from within other subroutines. Unfortunately, it seems that subroutine handlers called from within other handlers in a script library don’t work when the library is loaded into a script server. The server’s client generates an error when the call to the library’s subroutine handler is encountered, asserting that the script does not understand the message.

This tip presents a method for overcoming this supposed limitation. This technique makes it practical to create script servers that incorporate complex script libraries.

The Problem

First, let us define the problem. Consider the following simple script library, which should be saved on your desktop as a compiled script named “TestLibrary”:


property TestString : ""

on Subroutine()
   set TestString to "I'm a Subroutine"
end Subroutine

on Main()
   set TestString to "I'm the Main handler"
   Subroutine()
end Main

Any script calling the Main() handler will normally set the TestString property to “I’m a Subroutine,” because Main()'s last step is to call Subroutine(). Here is a script to try it out. Save the following statements as a script application named “Tester” and run it. The dialog will display the phrase “I’m a Subroutine,” as expected.


set TestLib to load script alias ((path to desktop as string) & "TestLibrary.scpt")
tell TestLib to Main()
display dialog TestLib's TestString

But now load the very same script library into a script server, by saving the following statements as a stay-open script application named “TestServer”:


property TestLib : ""

choose file with prompt "Locate the TestLibrary file:"
load script result
set TestLib to result

Next, save the following statements as a script application named “TestClient.” This will be the client application; it calls Main() via the TestServer application into which the TestLibrary file has been loaded.


tell TestLib of application "TestServer"
   Main()
   display dialog its TestString as string
end tell

Run the TestServer application first, locating the TestLibrary file if asked to do so. Then run TestClient. TestClient fails with an error saying that the script “doesn’t understand the Subroutine message.” Why not? The Subroutine handler is right there, and it worked when called directly from Tester. Why does it become invisible just because it has been loaded into a script server and called from a client?

The Solution

Let’s look at the solution next, to see what light it sheds on the problem. What we have to do is to revise the original TestLibrary script library by enclosing it entirely within an explicit script object, like so:


script TestLibraryObject
   
   property TestString : ""
   
   on Subroutine()
      set TestString to "I'm a Subroutine"
   end Subroutine
   
   on Main()
      set TestString to "I'm the Main routine"
      TestLibraryObject's Subroutine()
   end Main
   
end script

You also have to revise Main()'s call to Subroutine() by writing "TestLibraryObject’s Subroutine() (or “Subroutine() of TestLibraryObject”), as shown above.

Then revise the TestServer script server to load this new script object into the server, instead of loading the library file, itself, as shown below. Notice that the final line sets the server’s TestLib propery to the new TestLibraryObject script object in the TestLibrary file.


property TestLib : ""

choose file with prompt "Locate the TestLibrary file:"
load script result
set TestLib to TestLibraryObject of result

It isn’t necessary to revise TestClient. Simply launch the new TestServer, locating the TestLibrary file if asked, and then launch TestClient. Now, magically, the whole thing works.

Why does it work?

Here’s my best understanding of why it works.

All of the TestLibrary file’s functionality is now enclosed within the TestLibraryObject script object. This structure would normally be regarded as redundant in a script library, because a script library becomes a script object, anyway, once it is loaded into a client using the Load Script command from the Standard Additions scripting addition. However, it is sometimes necessary to enclose library handlers within an explicit script object, if the library file is to be loaded into a script server and called by client applications.

Enclosing the library in an explicit script object is required only when the script library contains handlers that call other handlers within the library. I surmise that, without an explicit script object enclosing the library’s contents at the time when it was compiled, the Load Script command in the script server can find no context in which to execute the precompiled subroutine call. When the library was compiled, it was not yet a script object in the right sense; the script object comes into existence only later, when the script library is loaded into the script server. By embedding all of a script library’s handlers within a script object in the library itself, the necessary context is created at compile time, to be made available to the script server at load time.

Summary

Using this technique requires only a slight deviation from the standard use of the Load Script command. In summary:

  1. Enclose the entire script library in an explicit script object.

  2. Make sure that any calls within library handlers to other handlers within the library are qualified by a reference to the explicit script object. For example, “TestLibraryObject’s Subroutine()” or “Subroutine() of TestLibraryObject.” Using “my” or “its” won’t do the trick.

  3. Instead of copying the result of the Load Script command into a property in the script server, copy the named script object of the result. For example, not set TestLib to (load script…; but instead set TestLib to TestLibraryObject of (load script….

  4. In a client script, all calls to library handlers must be in a tell block aimed at the script object in the server, not at the server itself. For example, tell TestLib of application “TestServer” ….

  5. In a client script, properties of the script library must be qualified by “its” or “of it.” Calls to handlers in the library do not require this qualification.