PREVIOUS  TABLE OF CONTENTS  NEXT 

Interprocess Communication with MacPerl

Chris Nandor

I love Macs, and I use them whenever possible. But don't worry. I'm not here to convert anybody (this time).

Mac folks have many of the same needs as Unix folks. We need to launch programs, manipulate files, shut down processes, log actions, and communicate with programs. In this article I'll talk about how you can control Mac applications from your Perl programs, using the Perl interface to AppleEvents.

A while back I wanted to send some files via FTP from my Mac to my Unix server. Of course, I could have just downloaded libnet from the CPAN and used Net::FTP (see TPJ #3). But I already have several FTP programs on my Mac that are fully-featured and allow me to interact with them directly via the mouse or keyboard. It would be nice to interact with these programmatically, as easily as I could with Net::FTP. Maybe something like this:

#!perl -w
use Mac::Apps::Anarchie;
$ftp = new Anarchie;
$file = 'test.txt';
$ftp->host('ftp.host.com');
$ftp->store("HD:Desktop Folder:$file", "/pub/$file");

Not all that different from any old FTP module you might encounter - but this code opens up Anarchie (Peter N. Lewis' popular FTP client for the Mac) and shows the progress in a window, allowing the user to interact directly with the session if need be. Simple, right? Well, that's a matter of perspective. Let's start with something you might be more familiar with.

AppleScript

AppleScript is Apple's proprietary scripting language for Mac OS. It hasn't undergone many revisions over the years; the version that shipped with Mac OS 8 is only 1.1.2. It's quite powerful.

Like most scripting languages, AppleScript is extensible. For instance, you can add in regular expression capabilities by dropping a regex "module" into the Scripting Additions folder. But the main purpose of AppleScript is to talk to other applications. The snippet of AppleScript below opens an application called Progress Bar and creates a new window named "Counting" at coordinates 100,50 (x,y). We'll use this later.

tell application "Progress Bar 1.0.1"
make new window with properties {Name:"Counting",Position:{100, 50}}
end tell

That chunk of code uses Apple Events to communicate with the Progress Bar application. Many people think of AppleScript and Apple Events as synonyms, but in fact any OSA (Open Scripting Architecture) language can use Apple Events to control the behavior of applications. (Dave Winer's Frontier is another popular OSA language for issuing Apple Events.)

But I don't like AppleScript. And I'm not crazy about Frontier. Not that they aren't powerful and enjoyed by their users, but they don't suit my preferences, and they probably don't suit yours, either. Our preference is Perl. Now, we could, if we choose, use AppleScript from MacPerl. The built-in MacPerl::DoAppleScript() takes one argument and compiles and runs that argument as AppleScript:

#!perl -w
print &MacPerl::DoAppleScript(<<EOS);
tell application "Progress Bar 1.0.1"
make new window with properties {Name:"Counting", Position:{100, 50}}
end tell
EOS

That's better than having to write a whole program in AppleScript. But it still isn't really Perl. AppleScript is very effective for controlling events, but for writing CGI scripts it's nothing short of a nightmare. I find the syntax horrible, the flow control nauseating, and the data structures entirely insufficient. I like Perl. I dream in Perl. I want to manage my computers and program in Perl whenever possible.

Personal preferences aside, the above code is ineffective in some key ways. First, AppleScript has to be compiled. Normally, you compile AppleScript and save the compiled version. But since the code is embedded in a Perl script, it has to be recompiled every time it is run. Further, the embedded AppleScript doesn't provide good error messages. In short, it's just not the best way to do things.

Using MacPerl

The current release of MacPerl as of this writing is 5.1.5, based on Perl 5.004. It requires System 7 or above (including Mac OS 8), and runs on all PowerPC and 68K computers. For 68K computers, the "BigMacPerl" archive includes extras that provide the full functionality of the PowerPC version. Installation is simple - just run the installer program, and it does all the work. No configuration required.

For more information about MacPerl, look for MacPerl: Power and Ease, by Vicki Brown and Chris Nandor, to be published by Prime Time Freeware in early 1998. To get the latest information about the book, and for additional MacPerl information - including links to the latest versions of MacPerl, the MacPerl mailing list, and the MacPerl FAQ-O-Matic - visit The MacPerl Pages at Prime Time Freeware's web site: http://www.ptf.com/macperl/.

Popular resources for MacPerl users:

Apple Events

The best way would be to use Perl to communicate directly with the applications using Apple Events. So when the author of MacPerl, Matthias Neeracher, released the Mac::AppleEvents module last year, I decided to put it to good use.

Apple Events (AEs) often intimidate novices, but to use them you have to be familiar with how they're constructed. An Apple Event is basically a data structure, similar to a hash. A typical Apple Event contains several parameters, each consisting of a keyword and descriptor record, just like the keys and values of a hash. Parameters often include information from built-in classes and properties of those classes. For instance, a window is a class, and it has properties for its name and position.

Also included in the event is the target application ID, the four-character application signature that all Mac OS applications possess. All events belong to an event suite, and that suite ID is part of the event. Finally, the ID of the event itself is given. The whole thing (which must fit within 32 kilobytes) is bundled up and sent to an event handler. After the event is processed by the system and (typically) processed by the target application, a reply event is returned to the sender.

The AEPrint() method in Mac::AppleEvents is a useful debugging tool. AEPrint() displays a string representation of an event. That illustrates what your code is doing and helps you find errors.

To be honest, Apple Events aren't too pretty, even in Perl. Eventually, I got to the point where I would do the following to achieve the same results as the AppleScript on the previous page. We'll discuss this program below.

#!perl -wl015
use Mac::AppleEvents;
$params = "kocl:type(cwin), prdt:{ pnam: "Counting", 
           ppos:[100, 50] }";
$evt = AEBuildAppleEvent('core', 'crel', 
                         typeApplSignature, 'PBar', 
                         0, 0, $params) || die $^E;
$rep = AESend($evt, kAEWaitReply) || die $^E;
print AEPrint($evt)
print AEPrint($rep);
AEDisposeDesc($evt);
AEDisposeDesc($rep);
# NOTES:
# \015 is the Mac line break character, not \012!
#
# curly quotes (option-[ and option-shift-[) surround items
# of type TEXT. These are non-ASCII characters: " and ".
#
# $^E is similar to $!, but gives platform-specific error info
# Apple Events don't do automatic garbage collection, so 
# we dispose of the event with AEDisposeDesc.

This prints:

core\crel{kocl:type(cwin), 
      prdt:{pnam:"Counting", ppos:[100, 50]}, 
      &inte:cans, &timo:3600}
aevt\ansr{}

The first line of the output (from core to 3600) is the Apple Event that was sent, and the second line is what we got back from the application. An error would appear (usually with the keyword errn or errs) if something went wrong.

Not very pretty, but a thousand times better because it's Perl instead of AppleScript. And now I can stick that in a package and do something like this:

#!perl -w
use Mac::Apps::PBar;
$bar = Mac::Apps::PBar->new('Counting','100, 50');

That's the stuff. And, of course, the code is much faster now, not only in runtime (since MacPerl doesn't have to compile the AppleScript), but in write time (since it is Perl!).

Of course, this laziness is only made possible by the Mac::Apps::PBar module. Relatively few Mac applications have such modules; communicating with applications that don't will be unpleasant because you'll need to use raw Apple Events.

Accessing Apple Events: A Tutorial

Most Mac applications understand a wide variety of Apple Events. Trouble is, it's not so easy figuring out which ones. Nearly all applications speak a standard set of Apple Events, just as most have the same basic menu functions (Open, Close, Quit, and so on). But whereas the contents of pull-down menus can be discovered just by pointing and clicking, ascertaining the Apple Events understood by an application is a bit more difficult.

The best way to find out exactly what Apple Events your application understands is to drop it onto Script Editor, the editor that comes with AppleScript. Dictionary entries for your application will appear, normally with complete descriptions of events and classes. Unfortunately, these are in AppleScript's native tongue, and aren't useful for handling Apple Events directly. For instance, Script Editor will tell you that with AppleScript you can create a window by writing "make new window," but it won't tell you that the event is named crel and is in the suite named core.

There are ways to get the specific terms, of course. One way is to use Frontier. (Go to the "Commercial Developers" suite, select "Enter your App's Name", and then type in the four-character application ID and select it in the dialog box. Frontier will give you a database of the events (verbs) for that application, including the true suite, event, and parameter names.) But the simplest way is by using a MacPerl droplet named aete.converter, created by David C. Schooley. aete.converter reads in the aete (Apple Event Terminology Extension) resources from the application and spits out the information into a text file.

Knowing that I had been working with MacPerl and Apple Events, JAMPH (Just Another MacPerl Hacker) Alan Fry contacted me about Progress Bar. This freeware application, by Gregory Dow, is nothing more than an Apple Event-capable progress bar that shows how close some task is to completion. You create a window, give it a window name and captions, some maximum and minimum values, and every once in a while an update to the value of the bar.

Using the output from aete.converter, Alan and I tried to figure out what to do. Let's revisit the simple event from the beginning of the article:

tell application "Progress Bar 1.0.1" 
make new window with properties 
           {Name:"Window",Position:{100, 250}}
end tell

For AppleScript we needed the name of the application. However, we need the full-fledged application ID for Apple Events. This can be obtained in numerous ways, such as with ResEdit.(Select the application with the "Get File/Folder Info..." command. The application ID is the "Creator.") AppleScript will try to determine the application ID by looking for an application of that name, but when it converts the AppleScript to events, it uses the application ID too.

The aete.converter output on the previous page told us some key facts about the make event. The application ID is PBar, the suite is core, and the event ID is crel (remember, these are the first three things we need to talk to our application). It tells us that the one required parameter is kocl, the type of element. Scanning further down, we find that the window class is cwin. So we have a nice little Apple Event:

$evt = AEBuildAppleEvent('core','crel',
                        typeApplSignature,
                        'PBar',0,0,
                        "kocl:type(cwin)")
       || die $^E;

Now that we can create a window, how do we give it a name and position, as in the original AppleScript? aete.converter tells us to use the properties parameter (prdt). But to use it, we first need to know which properties are available.

This is put together similarly to the AppleScript version. The keyword for the name is pnam, and the keyword for the position is ppos. Note that pnam is of type TEXT, so it's enclosed by curly quotes. ppos is of type qdPT, which is a list. Like an array reference in Perl, a list of elements is separated by commas and enclosed by square brackets. The full event corresponding to the AppleScript version is now complete:

$evt = AEBuildAppleEvent('core', 'crel', typeApplSignature, 
       'PBar', 0, 0,
       "kocl:type(cwin),prdt:{ pnam: "Window" , ppos:[100, 250] }") 
       || die $^E;

We now have a progress bar in a window with a specific name and position, shown at the top of the next column. But it's empty. How do we update the progress bar? How do we change the value of the bar, or add text over the bar? How do we give it a caption and get it to look like the image on the next page? Looking back at the output of aete.converter, we ascertain how to set data: we start out the same way, with suite core, but a new event ID, setd. This time, we have two different parameters, and both are required. The first is the object to be changed (the caption, in this case). The second is the value to give it. This value can be numeric or text, depending on what the object is; for the caption, it will be some text. For now, let's just leave 'obj ' as a variable ($miscobj) and put that in later. Note that the direct object parameter, 'obj ', is always given the keyword '----', and that 'obj ' is a four character string: three letters and a space.* Parameter names are always four characters.

A Plain Progress Bar

Figure 1: A Plain Progress Bar

$evt = AEBuildAppleEvent('core', 'setd', typeApplSignature, 
       'PBar', 0, 0, "'----':$miscobj, 
       data:"Caption Text"") || die $^E;

Now what do we do with $miscobj? (Keep in mind that the direct object parameter mentioned above isn't the same thing as an Apple Event object. An Apple Event object can be the value of the direct object parameter, or any other parameter, but it's not a parameter itself.) We have to start by understanding the format of Apple Event objects:

   obj{want:xxxx, from:xxxx, form:xxxx, seld:xxxx)}

want is the class ID. It describes what type of object it is. from is the container description - null unless this object is contained inside another object. We will see both forms below. form is the form of the data, which might be a property, an index, or something else. seld is the data itself.

This time, our object is a property of a class. The class is the progress bar (PBar), and the property that aete.converter reveals to us is Cap1. Remember that we're describing the property of the class in this object, not the class itself. So want is type(prop), form is prop, and seld is type(Cap1). We don't have the container yet, so we leave from as another variable, and we construct our object accordingly:

$miscobj = "obj{want:type(prop), from:$progobj,
            form:prop, seld:type(Cap1)}";

But we aren't done constructing our object; the property is contained by a progress bar (assigned the index number 1, since it is in the top window). Now want is an object, and we give it the class ID of the object, which is PBar (event classes and suites often take the same name as the application ID). Again, this object is contained by another object - the window itself - so we'll leave it as a variable.

$progobj = "obj{want:type(PBar), from:$windobj, 
            form:indx, seld:long(1)}";

The window object is similar to the progress bar object, but has no container (finally!), so its from is null. Putting everything together yields the code and image shown at the top of the next page.

$windobj = "obj{want:type(cwin), from:null(), 
            form:indx, seld:long(1)}";
$progobj = "obj{want:type(PBar), from:$windobj, 
            form:indx, seld:long(1)}";
$miscobj = "obj{want:type(prop), from:$progobj,
            form:prop, seld:type(Cap1)}";
$evt = AEBuildAppleEvent('core', 'setd', 
            typeApplSignature, 'PBar',  0, 0, 
            "'----':$miscobj, data:"Caption Text"")
            || die $^E;

That's a lot of work to do something that could have been done more simply in AppleScript. But now we just can stick it in a module and do cool things like this:

#!perl -w
use strict;
use Mac::Apps::PBar;
#create window
my $bar = Mac::Apps::PBar->new('FTP Download', '100, 50');
# Module sets each element of hashref in turn
$bar->data({Cap1 => 'file: BigFile.tar.gz', 
            Cap2 => 'size: 1,230K',
            MinV => '0',
            MaxV => '1230'});
# Set the value of the bar every second, for 11 seconds
for (0..10) {
    	$bar->data({ Valu => $_*123 });
    sleep(1);
}
sleep(5);
$bar->close_window;

Sometimes it's not so easy to construct these methods. Often the events are confusing, or there's no documentation about how to use them, or the documentation is just plain wrong. Help is occasionally available: the AETracker Control Panel can be used to capture Apple Events. By writing AppleScript to do what you want, invoking AETracker, running the AppleScript, and analyzing the AETracker output, you can get a better idea of how the event works.

The Progress Bar, after running the program at left

Figure 2: The Progress Bar, after running the program at left

Conclusion

Many tasks that people now attempt with AppleScript can be done better in Perl - if the proper Apple Events can be constructed. The possibilities are endless. Assemble a Quark XPress document and apply style sheets. Invoke utilities at night and have them perform particular actions. Process HTML files and then upload them to a remote server with Anarchie.

Apple Events will continue to be a part of Rhapsody, whose applications will run on any current Mac OS, Windows 95/NT, or Rhapsody box. Thus, Apple Events will probably become even more important as time goes on. Of course, Apple's new OS could die a miserable death, and Mac OS could be relegated to the ranks of the Amiga and OS/2, but we try not to think about that.

_ _END_ _


Chris Nandor (pudge@pobox.com, http://pudge.net/) works for Peterson's, an educational publisher in Princeton, New Jersey, and is writing MacPerl: Power and Ease for publication in early 1998. He lives in the Boston area and hacks Macs, Newtons, and web sites in his spare(!) time. He would like to thank Matthias Neeracher, Alan Fry, Steve Johnson, and Oliver Banta for their help.
PREVIOUS  TABLE OF CONTENTS  NEXT