Write a FilterTop Filter
Volume Number: 13
Issue Number: 7
Column Tag: Plugging In
Filter It!
by Alan Weissman, TopSoft, Inc.
Join the Plug-in Revolution: Write a FilterTop Filter
The Plug-in Revolution
In 1992 a fruitful collaboration began among a group of Mac programmers on Usenet.
Fired up by the idea of pooling their expertise to develop a killer app that would
incorporate many of the new features of System 7, these enthusiasts soon formally
organized as TopSoft, Inc., and dubbed their chief project FilterTop. Its main purpose:
to bring batch processing and pipelining from unix to the Mac, using such advanced
features as Apple events, drag-and-drop and multithreading.
Four years passed before FilterTop's first full public release. A lot happened in that
time. Popular programs had evolved into resource-hungry monsters that had to
incorporate every conceivable feature, and this tendency was stifling both
programmers and users. To let in fresh air, a contrary trend in Mac programming was
emerging, toward modularity and plug-in architecture. We see this trend gaining
momentum today as large applications increasingly rely on small, interchangeable
plug-ins to provide much of their functionality. Adobe Photoshop is the best-known of
these, but others are rapidly following suit. Well, FilterTop was ahead of the pack, and
today, in an age when software bloat and creeping featuritis still dominate, FilterTop
seems more innovative than ever. In fact, practically speaking, FilterTop is just a
framework for plug-in modules.
FilterTop offers a distinct advantage in the way it uses plug-ins. Each module, or
filter, can be linked with others in a pipeline. Then one or a batch of files can be sent
down the pipe, with each filter manipulating the files in a specified way. Instead of one
filter at a time operating on one file, you have many filters operating in quick
succession on many files, each of which might have been created by a different
application. The FilterTop framework provides the user interface and the glue that
ensures everything - batches of files and assemblages of filters - works together
smoothly.
FilterTop shares an exciting property with other applications that have a plug-in
architecture: extensibility. A new capability may be added to those already present
simply by writing a new filter and plugging it in. The filter programmer doesn't have
to know a thing about the inner workings of FilterTop. Even preexisting filters provide
a field for experimentation. Different permutations and combinations may provide new
functionality to meet specific needs without your writing one additional line of code.
Best of all is that filters are easy to write. Most of the remainder of this article will
outline the simple steps to create your own FilterTop filter. First, let's get a general
overview of FilterTop's operation from the user's point of view so you can form a
better idea of how a filter fits into the functioning of the whole.
FilterTop in Action
The building blocks of FilterTop are filters. The user can use just one at a time or pipe
together two or more. In all cases, however, they must be combined and set up by
means of SuperFilters. These in turn may be saved in files called Toplets, stand alone
applets that perform multiple operations on files - not by themselves, but by using
Apple events to pass instructions to FilterTop.
A FilterTop Toplet is somewhat analogous to an AppleScript droplet; you drop onto its
icon in the Finder other icons representing any number of files (or you can just
double-click it and add files later). A Toplet is opened, causing a SuperFilter to be
recreated according to choices previously made by the user. FilterTop is launched if it
is not already running and sent the information that enables it to display icons for the
files to be processed and for filters that represent the operations to be performed on
the files. If the option to do so has been chosen, the files are automatically processed;
otherwise, FilterTop waits for the processing to be initiated by the user.
To create a SuperFilter in the first place, you drag icons from a list (displayed in a
floating palette) to a window. Each filter has one or more ports, indicated on the top
and bottom of the icon. You use the mouse to connect the filters in custom order by
dragging from one port to another. When this is done, the filters are shown connected
by pictures of pipes. Thus the concept of pipelining - an abstract one in operating
systems without a GUI - is graphically illustrated on the Mac. In fact, with its menus,
lists, windows and icons, FilterTop is very much a Macintosh application.
Figure 1. A SuperFilter set up and ready to run.
Icons for files to be processed appear in another part of the window. Figure 1 shows
an example. Say you have a group of twenty files. Each is a SimpleText file and contains
a section of an article. The files are named "Part 01," "Part 02," etc. Your object is to
create a single plain-text file that contains the entire article and will open in
Microsoft Word. By dragging and dropping with the mouse, you create a pipeline of
several filters in the SuperFilter editor window. The first assures that the files are
sorted by their names; the next concatenates the text of all the files; the next changes
the creator code of the output file to that of Word; the next assigns a name to the output
file; and the final filter sends the result to a folder you have designated.
You click a button and witness the start of processing: the filter icons become dark as
data flows through them, the pipes swell when the data flows down to the next filter,
and so on. At least that is the GUI representation of what is going on behind the scenes.
Bruce Bennett, an amused commentator on the Internet, called FilterTop "unix batch
processing by Rube Goldberg." That is the way it looks. (It also shows that members of
the Mac community have a sense of humor!) Though FilterTop may appear to be an
ungainly contraption that you watch with a smile, it works, and with surprising speed
and efficiency.
Advantages of Writing a Filter
Writing a filter to perform a given function is easier by an order of magnitude than
creating a stand-alone application. FilterTop handles the tough stuff. It coordinates the
operations of the various filters and assembles the files passed to it by the user. It
passes to your filter the data to be processed. It allows you to retrieve the user's
preferences as well as a certain amount of data to be used in performing the operation,
such as a string to be searched for. When you are through processing - "filtering" -
the data, the FilterTop engine receives the results, which it passes on to the next filter
or saves to disk as a file.
Thus, you the filter programmer need be concerned only with the specific task you
want your filter to accomplish. This may be simple, such as counting the characters in
an input file, or much more advanced, such as applying a sophisticated compression
algorithm or translating from one graphics format to another. Professional developers
can find intriguing potential in FilterTop filters, yet programming one is simple
enough for hobbyists.
Writing Your Filter
Getting Started
For your filter to work properly you are required to follow a strict set of guidelines.
This may seem constraining at first but this system has its advantages. First of all,
FilterTop provides programmers with numerous aids to help you comply with its
restrictions and get your code working. Secondly, once you set up everything correctly
for interaction with the FilterTop engine, you are left with a great deal of latitude in
what you can do in the body of your code. In addition, you will find that, once you are
comfortable using the framework FilterTop provides, you are freed from the need to
spend time on the details of pre- and post-processing, including handling most of the
user interface and all but the simplest aspects of error trapping and reporting.
The Basic Steps
Here are the five basic steps to create a fully functioning FilterTop filter:
• Receive messages from the engine.
• Retrieve data from the engine.
• Process the data.
• Return the results to the engine.
• Send a return code back to the engine indicating the outcome of the
operation.
There are also a few things to be done with resources at different stages. At this time
support is provided for writing filters in "C," using either Metrowerks CodeWarrior
or THINK C. Additional support may be added in the future. (It should also be mentioned
that FilterTop is not currently PPC native; this will probably change with the next
major revision.)
Receive Messages
To receive messages from FilterTop, you must #include "FTFilter.h", a header file
that defines these messages. FTFilter.h, supplied by TopSoft with its other
programming tools, also defines the data structures and functions that FilterTop uses
and with which you must become familiar.
In fact, messages and everything else transmitted to you by FilterTop come through a
parameter block, a pointer to which is passed to your filters main() function when its
code is executed. In this respect as well as in others, as we'll see, a FilterTop filter
resembles a HyperCard XCMD. If you have any familiarity with the latter you can
easily understand the former. (Photoshop filters work in the same general way; if you
can write one of those, a FilterTop filter is a cinch.) The structure of this parameter
block is worth studying; once you get the hang of how it works you will have gone a
good way toward comprehending the interface between your own code and FilterTop.
typedef struct
{
FilterMessage serviceRequested; // essential
long filterShellVersion; // ignore
Callbacks *cb; // essential
DialogPtr dlg; // rarely used
EventRecord *event; // rarely used
Handle filterGlobalsH; // useful
Handle filterConfigH; // rarely used
long data; // useful
} PBCallToFilter, *PBCallToFilterPtr;
Understanding the FilterTop parameter block is easier than it at first appears, because
it is essential to deal with only two of its elements. Two others are also useful at times,
and the rest are either reserved by FilterTop or used only with certain special types of
filters. filterShellVersion is reserved by FilterTop. filterConfigH, dlg and *event are
used in filters only if the filters must interact with the user through their own
self-created dialog. This is rarely if ever necessary since FilterTop also provides a
remarkably complete means of putting up such a dialog and retrieving preferences
automatically, through a mechanism called AutoConfig. More about AutoConfig later.
All filters are reentrant; that is, if a filter has been called, it may be called a second
time (and a third, etc.) before the first instance returns. This imposes the restriction
that filters may not have global variables (otherwise, more than one instance of a
filter could struggle over the globals). It is permissible, however, for memory
allocated on the heap to be used as though it were global, and the member
filterGlobalsH is provided as a convenient way of accessing a block of these "globals.
The member data is similar to the refCon provided in Toolbox window and control
records. Basically, you can put any four-byte value you want there for convenient
later retrieval.
The absolutely essential members of the parameter block, however, are the first and
the third. (We will come back to the third a little later on.) serviceRequested is a
variable that you must examine immediately whenever your filter is called. It is
standard practice simply to set up a switch statement to handle the messages in
serviceRequested, and in most filters the main function will consist of little more than
this switch statement.
pascal FilterResult main( PBCallToFilter *pb )
{
FilterResult rc;
ENTER_FILTER;
switch (pb->serviceRequested)
{
case msgOpenFilter:
rc = filterMsgNotHandled;
break;
case msgFilterData:
rc = MyFilterFunction( pb );
break;
case msgCloseFilter:
rc = filterMsgNotHandled;
break;
default:
rc = filterMsgNotHandled;
}
EXIT_FILTER(rc);
}
(ENTER_FILTER and EXIT_FILTER are macros that enable your filter to use the A4
register to handle static data that the compiler treats as globals.) msgOpenFilter and
msgCloseFilter are occasionally useful if you want to set up some structure or
perform a particular action only once at the beginning of a filtration session and not
every time data is sent to your filter. There are other messages that you must know for
special types of filters. In most cases, however, the only message you must act on is
msgFilterData, which simply is the go-ahead that signals you to perform the action
that your filter is supposed to perform.
The variable rc, of type FilterResult, receives a return code. Normally it will be
filterOK, which indicates that your filter successfully did what it is supposed to do, or
filterMsgNotHandled, as seen here (which simply indicates that you take no action on
this particular message). On occasion you might also return filterAborted, in case you
were informed by the engine that the user has aborted the filtration, or
filterProcessingError, to inform the engine (or confirm that you have been informed)
that an unrecoverable processing error occurred. Either of these codes, or filterOK,
might be assigned to rc through MyFilterFunction in the example above, depending on
what happened during filtration. That is all you must know about the FilterTop
messages.
Retrieve the Data
The second member of the FilterTop parameter block, *cb, is extremely important. It
is, simply, a pointer to another structure, this one consisting of (at this writing)
twenty-two pointers to functions. These functions are the FilterTop callbacks, a term
that, again, you will be familiar with if you have written any HyperCard XCMDs. The
callbacks are functions that FilterTop provides for interacting with it; the engine
passes you pointers to the callbacks, and you call back to the engine by simply
dereferencing one of the pointers, passing the parameters required by that particular
routine.
static FilterResult MyFilterFunction( PBCallToFilter *pb )
{
FTErr err, writeErr;
Stream inputStream, outputStream;
FInfo fndrinfo;
FSSpec suggested_spec;
err = pb->cb->Open(input1, &inputStream, &fndrinfo,
&suggested_spec);
HANDLE_ERR(err);
Note the double use of the pointer-to-member operator to call Open, a pointer to
which is passed as part of cb, the Callbacks structure, which in turn is pointed to by
pb, the parameter-block pointer. The Open callback will become very familiar to you,
as it is used to open both input and output streams.