External Functions
Volume Number: 6
Issue Number: 1
Column Tag: Advanced Mac'ing
Related Info: Resource Manager
External Functions
By Mark Lankton, Boulder, CO
Note: Source code files accompanying article are located on MacTech CD-ROM or
source code disks.
XDemo--Easy External Functions
Mark Lankton is a member of the technical staff at the University of Colorado in
Boulder, where he spends his time teaching Macintoshes to talk to space-flight
instruments. Much of his work involves real-time data acquisition and display.
Why can’t I be like the big kids?
What does HyperCard have that you don’t have? External commands and
functions, that’s what. The Great Man and his team didn’t have to anticipate all the
zillions of uses their creation would end up supporting. They were bright enough to
build in a mechanism that allows (educated) users to add procedures of their own.
Functions that weren’t even a gleam in Bill Atkinson’s eye can be programmed by a
basement genius and used by HyperCard as if they had always been there.
Is that a neat trick? You bet it is. The huge supply of homegrown XCMDs and
XFCNs is ample evidence of the idea’s popularity. HyperCard isn’t alone, either. Other
big-time applications (can you say 4th Dimension?) are in on the gag too. The idea of
externally-built modular functions is going to be very important in the
object-oriented world that everyone assures us is coming.
This article describes a simple mechanism for building that kind of extensibility
into your own application. It includes an extremely rudimentary application called
XDemo, which has as its sole purpose in life the plotting of a small data set in a
window. The interesting part is that if you don’t like the looks of the data, you can
write an external function to change it and simply add the function to XDemo. You can
write a whole batch of external functions and add them all in; they will all appear in a
menu and you can call them any time you want. Your mom can write an external
function and you can add that in, too. You don’t have to recompile XDemo; you don’t even
need access to the source code. You do need a compiler and linker that will let you
create a stand-alone ‘CODE’-like resource. XDemo is written in MPW C3.0, but other
C compilers that are capable of creating things like WDEFs and XCMDs should work
fine. A copy of ResEdit is handy, too.
How Can This Be?
The whole thing works because of a nice C trick: the pointer-to-function.
Function pointers give you a way to actually execute code that lives in arbitrary
locations in memory, locations that the linker never dreamed of when it originally
built the application. (In fact, you can make the computer attempt to execute whatever
it finds at any address you specify. If there is no code there, the results can be, shall
we say, surprising. This capability is one of the reasons C has a historical reputation
for being “unforgiving”.)
A pointer-to-function allows you to run a function’s code once it’s in memory,
but how do you get it into memory in the first place? The answer is to make the
executable code of the function into a resource and call on the Resource Manager. The
Resource Manager can read that resource into memory and give us a handle to it.
Dereference the handle into a pointer and there you go!
At this point some of you will be running for the exits to try this for yourselves.
For those who remain, here are the details of how to define the interface between the
main application and the external functions, how to build the external functions
themselves, and how to load them into a menu at start-up time and use them.
Figure 1- Xdemo with a variety of XTRA functions installed.
Defining an Interface
Giving yourself and your users the capability to make up and use new functions is
wonderful, but it only works if everyone agrees on the rules. You have to specify the
parameters that will be passed to the external functions and the results that the
functions will return. Deciding what kinds of data the external functions will be
allowed to work on and what sorts of information need to be passed back and forth is
really important, and you need to give it serious thought.
The most flexible way to pass information to an external function is to define a
parameter block and pass a pointer to that block to the external function. The
parameter block itself can be large, complicated and jam-packed with data (including
other pointers and handles) or it can be very simple, depending on the needs of the
application. XDemo uses a tiny parameter block called an XTRABlock, which looks like
this:
/* 1 */
typedef struct XTRABlock{
int dataLength;
short *theData;
}XTRABlock,*XTRABlockPtr,**XTRABlockHandle;
All that is included is the length of a data set, and a pointer to that data set. This
means that external functions added to XDemo can do anything they want to the values in
a particular bunch of data, but they can’t do much else. If your application needs more
flexibility (and it probably will) you need to define a more elaborate parameter block.
You can help yourself considerably during development if you add a few unused
fields to the parameter block, thus:
/* 2 */
typedef struct XTRABlock{
int dataLength;
short *theData;
long unused1;
long unused2;
/*more if you’re really cautious...*/
}XTRABlock,*XTRABlockPtr,**XTRABlockHandle;
That way when you decide to add features to the block your space requirements
won’t change, and any external functions you have already built won’t need to be
redone. Since what the application is actually passing is a pointer to the block, you
don’t need to worry about stack space even if the size of the block gets really out of
hand.
There are three obvious ways to get information from the external function back
to the application. The first is to have the function return a result, which can be
anything from the simple Boolean used in XDemo to a fiendishly complicated data
structure. It’s often best to keep the return value simple; you’re already defining one
tricky interface with the parameter block. You can use a Boolean result to let the
external function indicate whether it succeeded in whatever it was trying, for instance.
The second way is to let the function set or change values in the parameter block, and
have the application look at them after the function returns. This is safe, and can be
defined any way you want. The third way is to allow the function access (by way of a
pointer in the parameter block) to the application’s global data. This is a simple and
dangerous method, since the application is exposing its internals to the outside world.
XDemo is brave, and uses this third method as well as the Boolean result trick.
The last step in defining the interface is the function prototype for the external
functions. Here you can inform the compiler about what to expect when one of these
functions is called. XDemo uses this prototype:
/* 3 */
pascal Boolean (*theXTRAProc)(XTRABlock *theBlockPtr);
In English, this means that “theXTRAProc” is a pointer to a function which takes
one parameter (theBlockPtr) and returns a Boolean result. The “pascal” keyword
means that parameters to the function will be pushed on the stack by the compiler in
Pascal order (left-to-right) instead of C order (right-to-left). This is vital for any
functions that might end up being used by Toolbox calls. It really isn’t needed in
XDemo, but for Macintosh programmers it’s not a bad habit to get into. (For a function
that takes only one parameter it is a rather fine distinction anyway.)
Writing the External Functions
Once the interface is clearly defined, you can start writing external functions.
Make sure to declare each function using the same prototype form as used above. There
are a couple of things to watch out for here. The external functions can’t directly use
any of the application’s globals. The external functions can’t use any global or static
data of their own, either, which can cause trouble if you want to use text strings. (If
you’re using MPW C 3.0 you can use the “-b” compiler option to get around this.)
Other than that, what you do in these functions is entirely up to you, subject to the
limitations of the interface you have specified. The code for two very simple XDemo
external functions is included below.
If you think your functions may need to use resources of their own, you may want
to partition the set of legal resource ID numbers in some clever way. If nothing else,
you can specify that any resources used by the function should have the same ID as the
resource that contains the function itself. When the resources are installed into the
application with ResEdit, make sure all the IDs agree. From inside the function itself
you can find out the proper ID like this (assuming you have already picked out a
name!):
/* 4 */
thisResource = Get1NamedResource(‘XTRA’, “\pThe Name”);
GetResInfo(thisResource,&theID,&theType,theName);
The exact method of compiling and linking these functions will depend on the
development system you are using. Here’s how to do it using MPW:
#5
c myXTRAFunction.c
link -sg “My Extra Function” myXTRAFunction.c.o ∂
-rt XTRA=128 -m MYXTRAFUNCTION ∂
{cLibraries}CRuntime.o ∂
{clibraries}cinterface.o ∂
{clibraries}CSanelib.o ∂
{libraries}interface.o ∂
-o myXTRAFunction
This assumes that you have a C source file called myXTRAFunction.c, which
contains the code for a function called myXTRAFunction. For those not familiar with
MPW, these instructions mean the following:
First, run the C compiler on my source file.
Second, run the linker on the “.o” file produced by the compiler. Link it all into
one segment named “My Extra Function”. Give it a resource type of ‘XTRA’ and a
resource ID 128. The entry point is the module “MYEXTRAFUNCTION”. (Its name is all
upper-case because it was declared to be a Pascal-type function, and Pascal changes
everything to upper-case.) Use the CRuntime, CInterface, CSanelib and Interface
libraries if you need them for the link. Finally, put the whole works into a file named
“myXTRAFunction”.
At this point if you open “myXTRAFunction” using ResEdit you will find an XTRA
resource named “My Extra Function”, ID 128, which you can copy into the main
application. If you are brave, and keep close tabs on your resource ID numbers, you
can link the function directly into the application by specifying “-o XDemo” in the last
line of the Link command above. This is fast and clean, but you will demolish any XTRA
resource with the same ID that was there before. Be careful when you do this. For your
own application you should of course invent your own resource type, and use that
instead of ‘XTRA’.
Loading and using the external functions
There are lots of ways to provide access to the external functions; here’s how it’s
done in XDemo. The external functions are automatically installed at run-time in a
menu called (in a flight of wild imagination) “Externals”. In XDemo’s SetupMenus()
function, Count1Resources() is called to see how many XTRA resources there are. (To
make life easy for the Menu Manager, XDemo will only use the first 32.) For each
XTRA resource, Get1IndResource() returns a handle, which is installed in an array so
it can be found easily. This array is 1-based, so that when the functions are installed
into the Externals menu the menu item numbers will exactly correspond to the array
indices. Calling MoveHHi() and HLock() makes sure that nobody escapes, since it is
extremely unfortunate to have a piece of code move while the machine is trying to
execute it! In a “real” program you would want to be much more sophisticated about
memory management and avoid tying up memory until you really needed to. Finally,
GetResInfo() finds the name of the XTRA resource, and AppendMenu() inserts that
name into the menu’s item list.
When all this is done, XDemo has an Externals menu consisting of the names of all
the external functions that have been attached to it, and an array of handles to those
functions. Now, at last, it can really use them. When an item is selected from the
Externals menu, XDemo grabs the corresponding handle from the handle array. The
handle is already locked, so it is safe to use it. The handle is dereferenced once to obtain
a pointer, and the pointer is jammed into theXTRAProc. To satisfy the compiler, both
sides of the assignment statement are explicitly cast to type ProcPtr:
/* 6 */
(ProcPtr)theXTRAProc = (ProcPtr)*(XTRAArray[theItem]);
Then, assuming that the proper values are already loaded into an XTRABlock
named “theBlock”, the function is called like this:
/* 7 */
theResult = (*theXTRAProc)(&theBlock);
This function call works just like a “normal” one, and XDemo take the returned
result and goes about its business, which just means replotting its data and seeing what
the external function may have done to it. If you are curious about how the
pointer-to-function call really works, see “The Dirty Details” below.
Figure 2- Looking at an XTRA resource with ResEdit.
Go For the Code
It should be clear that XDemo’s implementation of external functions is very
simple. Your use of the ideas presented here is limited only by your own cleverness.
External functions provide a flexible way for you and your users to change and extend
an application.
Are there any drawbacks? Yes, of course. Each external function chews up space
in your application heap, so you have to allow for extra memory usage, and recover
gracefully if you run out. Worse, when you give other people the power to modify your
application, as you will if you publish your interface specs, you run the risk of getting
modifications that work poorly, don’t work at all, or even crash the machine. Still, the
added capabilities are extremely seductive. It’s a way of adding to an application
without rebuilding it or changing its structure in any way. The effects are reversible;
just zap any offending externals with ResEdit. And most important, you will be able to
do something about the world’s most common complaint, “Well, why doesn’t it do
this?”
So, read the source code, put on your wizard’s hat, and have fun!
The Dirty Details (For those who can’t get enough)
How does a call using a function pointer work? The best way to understand it is to
see what the compiler does with the C code that makes the call. Using the MPW tool
DumpObj on the compiled code lets us look directly at the assembly-level instructions
that the 680x0 chip will execute. The twelve instructions below are extracted from
the dump of XDemo’s DoCommand() function. They are the result of compiling these
four lines of C:
/* 8 */
theBlock.dataLength = screenDataLength;
theBlock.theData = screenData;
(ProcPtr)theXTRAProc = (ProcPtr)*(XTRAArray[theItem]);